LINUX环境编程之进程控制(上)

一、进程标识

每个进程都有一个非负整型表示的唯一进程ID。虽然该id是唯一的,但是进程ID是可复用的。当一个进程终止后,其进程id就成为复用的候选者。

系统中有一些专用进程,但是具体细节随实现而不同。ID为0的进程通常是调度进程,常常被称为交换进程。该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被成为系统进程。进程ID 1通常是init进程,在自举过程结束时有内核调用。init进程绝不会终止,他是一个普通用户的用户进程(与交换进程不同,他不是内核中的系统进程),但是它以超级用户权限运行。

每个unix系统实现都有它自己的一套提供操作系统服务的内核进程,比如,在某些unix的虚拟存储器中,进程id 2是页守护进程(page daemnon),此进程负责支持虚拟存储系统的分页操作。

附:进程相关函数

1 #include <sys/types.h>
2 #include <unistd.h>
3 pid_t getpid(void); 返回:调用进程的进程 I D
4 pid_t getppid(void); 返回:调用进程的父进程 I D
5 uid_t getuid(void); 返回:调用进程的实际用户 I D
6 uid_t geteuid(void); 返回:调用进程的有效用户 I D
7 gid_t getgid(void); 返回:调用进程的实际组 I D
8 gid_t getegid(void); 返回:调用进程的有效组 I D

二、fork函数

#include<unistd.h>
pid_t fork(void);

解释:有fork函数创建的新进程成为子进程,fork函数被调用一次,但返回两次,两次返回的区别是子进程返回值是0,父进程的返回值则是新建子进程的进程id。子进程和父进程继续执行fork调用之后的指令,子进程是父进程的副本。例如,子进程获取父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本,父进程和子进程并不共享这些存储空间部分。该部分执行写时复制。

 1 #include<sys/types.h>
 2 #include "ourhdr.h"
 3 int glob=6;
 4 char buf[]="a write to stdout\n";
 5 int main(void)
 6 {
 7
 8         int var;
 9         pid_t pid;
10         var=88;
11         if(write(STDOUT_FILENO,buf,sizeof(buf)-1)!=sizeof(buf)-1)
12                 printf("write error");
13         printf("before fork\n");
14         if((pid=fork())<0)
15                 printf("fork error\n");
16         else if(pid==0)
17                    {
18                    glob++;
19                    var++;
20                 }
21         else
22                         sleep(2);
23         printf("pid=%d,glob=%d,var=%d\n",getpid(),glob,var);
24         exit(0);
25 }

可以看到子进程对变量所做的改变并不影响父进程中该变量的值。

一般来讲,在fork之后是父进程还是子进程先执行是不确定的,这取决于内核所使用的调度算法,如果要求父进程和子进程之间相互同步,则要求某种形式的进程间通信。

文件共享

在fork之后处理文件描述符有以下两种常见的情况:

(1)父进程等待子进程 完成。(2)父进程和子进程各自执行不同的程序段。

除了打开文件之外,父进程的很多其他属性也有子进程继承,包括:

实际用户I D、实际组I D、有效用户I D、有效组I D。
• 添加组I D。
• 进程组I D。
• 对话期I D。
• 控制终端。
• 设置-用户- I D标志和设置-组- I D标志。
• 当前工作目录。
• 根目录。
• 文件方式创建屏蔽字。
• 信号屏蔽和排列。
• 对任一打开文件描述符的在执行时关闭标志。
• 环境。
• 连接的共享存储段。
• 资源限制。
父、子进程之间的区别是:
• fork的返回值。
• 进程I D。
• 不同的父进程I D。
• 子进程的t m s _ u t i m e , t m s _ s t i m e , t m s _ c u t i m e以及t m s _ u s t i m e设置为0。
• 父进程设置的锁,子进程不继承。
• 子进程的未决告警被清除。
• 子进程的未决信号集设置为空集。
fork失败的两个原因:
(a)系统中已经有了太多的进程(通常意味着某个方面出了问题)

(b)该实际用户id的进程总数超过了系统限制。

fork的用法:

(1)一个父进程希望赋值自己,是父进程和子进程同时执行不同的代码段。

(2)一个进程要执行一个不同的程序。

函数vfork

(1)vfork函数用于创建一个新的进程,而该进程的目的是exec一个新程序。vfork与fork的区别:(1)v f o r k与f o r k一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用 e x e c (或e x i t ),于是也就不会存访该地址空间。不过在子进程调用 e x e c或e x i t之前,它在父进程的空间中运行。这种工作方式在某些 U N I X的页式虚存实现中提高了效率(与上节中提及的,在 f o r k之后跟随e x e c,并采用在写时复制技术相类似)。

(2)v f o r k保证子进程先运行,在它调用 e x e c或e x i t之后父进程才可能被调度运行。(如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。)
函数exit

进程有5种正常终止和3种异常终止方式

5种正常终止方式为:

在main函数内执行return语句,调用exit函数,调用_exit函数或者_Exit函数,进程的最后一个线程在其启动例程中执行return语句,进程的最后一个线程调用pthread_exit函数。

3种异常终止方式如下:

调用abort,当进程接收到某些信号时,最后一个线程对“取消”请求做出响应。

在任意一种终止情况下,该终止进程的父进程都可以用wait或waitpid函数取得其终止状态。

对于父进程已经终止的所有进程,它们的父进程都改变为init进程,我们称这些进程由init进程收养。

僵死进程:一个已经终止、但是其父进程尚未对其进行善后处理的进程成为僵死进程。

函数wait和waitpid

#include<sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid,int *statloc,int options);

调用wait和waitpid的进程会发生什么

• 阻塞(如果其所有子进程都还在运行)。
• 带子进程的终止状态立即返回(如果一个子进程已终止,正等待父进程存取其终止状态 )。
• 出错立即返回(如果它没有任何子进程)。
这两个函数的区别是:
• 在一个子进程终止前, wait 使其调用者阻塞,而 waitpid 有一选择项,可使调用者不阻
塞。
• waitpid并不等待第一个终止的子进程—它有若干个选择项,可以控制它所等待的进程。

检查w a i t和w a i t p i d所返回的终止状态的宏

对于waitpid函数中pid参数的作用的解释如下:

pid == -1 等待任一子进程。于是在这一功能方面 w a i t p i d与w a i t等效。
pid > 0 等待其进程I D与p i d相等的子进程。
pid == 0 等待其组I D等于调用进程的组I D的任一子进程。
pid < -1 等待其组I D等于p i d的绝对值的任一子进程。
对于waitpid函数中options参数的作用的解释如下:

options或者是0或者是表中常量按位或者运算的结果。

waitpid函数提供了wait函数没有提供的三个功能:
(1) waitpid等待一个特定的进程 (而wait则返回任一终止子进程的状态 )。
(2) waitpid提供了一个wait的非阻塞版本。有时希望取得一个子进程的状态,但不想阻塞。
(3) waitpid支持作业控制(以WUNTRACED选择项)。

 1 #include<sys/wait.h>
 2 #include<unistd.h>
 3 #include<stdio.h>
 4 #include<stdlib.h>
 5 int main(void)
 6 {
 7         pid_t pid;
 8         if((pid=fork())<0)
 9         {
10                 perror("fork");
11         }else if(pid==0)
12         {
13                 if((pid=fork())<0)
14                 perror("fork");
15                 else if(pid>0)
16                 exit(0);
17                 sleep(2);
18                 printf("second child,parent pid is:%d\n",(long)getppid());
19                 exit(0);
20         }
21         if(waitpid(pid,NULL,0)!=pid)
22         perror("waitpid");
23         exit(0);
24 }
25 ~    

以上程序实现的功能是:一个进程fork一个子进程,但不要它等待子进程终止,也不希望子进程处于僵死状态知道父进程终止,通过调用fork两次来实现。

在第二个子进程中调用 sleep以保证在打印父进程 ID时第一个子进程已终止。在 fork之后,父、子进程都可继续执行——我们无法预知哪一个会先执行。如果不使第二个子进程睡眠,则在fork之后,它可能比其父进程先执行,于是它打印的父进程 ID将是创建它的父进程,而不是init进程。

时间: 2024-11-18 05:29:41

LINUX环境编程之进程控制(上)的相关文章

Linux环境编程之进程(七):守护进程

守护进程也是一种进程,它由如下特性: 1.生存期较长,在系统自举时启动,仅在系统关闭时终止. 2.没有控制终端,在后台运行. 系统中有很多守护进程,它们执行日常事务活动.如日志进程syslogd.web服务器httpd.邮件服务器sendmail和数据块服务器mysqld等.大多数守护进程都是以超级用户(用户ID为0)特权运行.没有一个守护进程具有控制终端,其终端设置为问号(?),终端前台进程组ID设置为-1.内核守护进程以无控制终端方式启动.用户层守护进程缺少控制终端可能是守护进程调用了set

Linux环境编程之进程(五):竞争条件以及exec函数

(一) 当多个进程企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,就认为它们发生了竞争关系.避免竞争的条件,给出apue上的一个代码吧: #include "apue.h" static void charatatime(char *); int main(void) { pid_t pid; TELL_WAIT(); /*set things up for TELL_XXX & WAIT_XXX*/ if((pid == fork()) < 0){ e

Linux环境编程之进程(一):main函数调用、进程终止以及命令行参数和环境表

(一)main函数调用 main函数作为程序运行时的入口函数,它是如何被调用的呢?首先必须清楚一点,main函数也是一个函数,它只有被调用才能够执行.其实,在执行可执行程序时,在调用main函数之前,内核会先调用一个特殊的启动例程,将此启动例程作为可执行程序的起始地址.启动例程是如何作为可执行程序的起始地址的?这是由链接编译器设置的,而链接编译器则是由C编译器(如gcc编译器)调用的.启动例程作为可执行程序的起始地址主要做哪些工作呢?启动例程从内核取得命令行参数和环境变量值,以此来为main函数

Linux环境编程之进程(二):程序的存储空间布局

引言: 一个写好的程序一般要存放在存储器中,那么程序中的代码.数据等各部分,是如何有规律的存放在存储器中的呢? (一) 一个存储的程序可分为五部分:正文段.初始化数据段.非初始化数据段.栈.堆.其典型的存储安排如下图: 正文段:这是由CPU执行的机器指令的部分.通常,正文段是可共享的,所以即使是频繁执行的程序在存储器中也只需要一个副本,另外正文段常常是只读的,以防止程序由于意外而修改其自身的指令. 初始化数据段:通常称为数据段,它包含了程序中需要明确地赋初值的变量. 非初始化数据段:通常称为bs

Linux环境编程之进程(四):创建新进程、执行程序和进程终止

引言: 对于每个进程,都有一个非负整数表示的唯一进程ID.虽然进程的ID是唯一的,但却是可重用的.系统中有一些专用的进程.如ID为0的进程通常是调度进程,也成交换进程或系统进程(它是内核进程).进程ID为1通常是init进程,它是一个普通的用户进程.一些与进程ID有关的函数: #include <unistd.h> pid_t getpid(void);   //返回值:调用进程的进程ID pit_t getppid(void); //返回值:调用进程的父进程ID uid_t getuid(v

Linux环境编程之进程(六):进程组

进程组 每个进程除了有一个进程ID之外,还属于一个进程组.进程组是一个或多个进程的集合.每个进程组有一个唯一的进程组ID.进程组ID类似于进程ID--它是一个整数,并可存放在pid_t数据类型中.函数getpgrp返回调用进程的进程组ID. 每个进程组都可以有一个组长进程.组长进程的标识是,其进程组ID等于其进程ID.组长进程可以创建一个进程组,创建该组中的进程,然后终止.只要在某个进程组中有一个进程存在,则进程组就存在,这与其组长进程是否终止无关.从进程组创建开始到其中最后一个进程离开为止的时

Linux环境编程之进程(三):函数间跳转

引言:在编写程序时,经常在函数内部使用goto语句来跳转,从而进行出错处理,那么如果想要在函数之间进行跳转该怎么做呢?使用setjmp和longjmp函数. 给出示例程序: #include <stdio.h> #include <stdlib.h> #include <setjmp.h> static void f1(int, int, int, int); static void f2(void); static jmp_buf jmpbuffer; static

Linux系统编程_8_进程控制之fork_wait_waitpid函数

fork函数: #include <unistd.h> pid_t fork(void); fork用来创建一个子进程: 特点: fork调用后会返回两次,子进程返回0,父进程返回子进程的进程ID:fork返回后,子进程和父进程都从fork函数的下一条语句開始运行: 注意: fork之后,操作系统会复制一个与父进程全然同样的子进程,虽说是父子关系.可是在操作系统看来,他们更像兄弟关系,这两个进程共享代码空间,可是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝.指令指针也全然同样

Linux系统编程_9_进程控制之exec 函数

exec函数 当进程调用exec函数时,该进程的执行程序完全的替换为新程序.新程序从它的main函数开始执行: 使用fork函数创建一个子进程后,子进程往往会使用exec函数去执行另一个程序. 注意:调用exec函数并不会创建新进程,所以创建前后的进程ID不会改变,exec只是用一个全新的程序替换了当前正在运行的程序的代码段.数据段.堆.栈. #include <unistd.h> extern char **environ; int execl(const char *path, const