[ APUE ] 第八章 进程控制
2.进程标识
进程ID可唯一标识进程,可复用。大多数UNIX系统采取延迟复用算法,即最近终止的进程ID不会成为新建进程的进程ID,防止被认为是已终止的先前进程。
0号进程通常是调度进程,常常被称为交换进程。该进程是内核的一部分,它不执行磁盘上的任何程序,因此也被称为系统进程。
1号进程通常是init进程,在自举过程结束时由内核调用,负责在自举内核后启动UNIX系统。其通常读取与系统有关的初始化文件,并引导系统状态,且其绝不会终止,但它是一个普通的用户进程(而非内核中的系统进程,这点不同于0号进程),却以超级用户权限运行。
它 是所有孤儿进程的父进程。
#include
#include
pid_t getpid(void); 返回调用进程id
pid_t getppid(void); 返回调用进程的父进程id
uid_t getuid(void); 调用进程的实际用户id
uid_t geteuid(void); 调用进程的有效用户id
gid_t getgid(void); 调用进程的实际组id
gid_t getegid(void); 调用进程的有效组id
3. fork函数
#include
#include
pid_t fork(void); 子进程返回0,父进程返回子进程id;出错返回-1
子进程的进程id永不可能为0.它总是由内核交换进程使用。
子进程和父进程继续执行fork之后的指令,子进程是父进程的副本,他获得了父进程的数据、堆、栈的副本,但并不和父进程共享。他们只共享正文段。
但是现在一般不把父进程数据段、堆和栈完全复制一份副本给子进程,取而代之的是COW 写时复制技术。父子进程共享这些区域,且这些区域被内核设为只读。如果父进程和子进程中的任何一个试图修改这些区域,则内核只为被修改的一部分区域制作一个副本,通常是虚拟存储下的一页。
测试fork的性质
1 #include
2 #include
3 #include
4 int main()
5 {
6 printf("test output!\n");
7 pid_t id = fork();
8 if(id<0){
9 perror("fork");exit(0);
10 }
11 else if(id==0){
12
13 printf("son:%u\n",getpid());
14 }else{
15 printf("father:%u\n",getpid());
16 }
17 return 0;
18 }
当标准输出定向到终端时,因为他是行缓冲的,所以遇到换行符就会冲洗缓冲区。
当标准输出重定向到文件时,为全缓冲的。第六行的printf输出的数据在fork的时候还在缓冲区中,会一起复制给子进程,所以最后关闭流冲洗缓冲区的时候 "test output!\n" 会被输出两次。
fork会使得父进程的打开的文件描述符也被复制到子进程中(相当于dup),父子进程的文件描述符共享同一个文件表项。同时父子进程会共享文件偏移量。
fork的两种用法:
- 父进程希望复制自己使得子进程执行不同的代码段。
- 一个进程要执行不同的程序。
vfork产生一个轻量级的进程,很不安全。
它创建新进程,且保证子进程先运行。它不将父进程地址空间完全复制,认为子进程会立即调用exec或exit,但是一旦调用其他函数或者修改数据就会带来不可预知的后果。(一般用于没有MMU的机器或者性能要求极高的场合,查了查应该没啥用了)。
注意vfork用_exit,用exit会坑死父进程。
5.exit函数
五种正常终止方式:
- main函数中调用return。等同于调用exit
- 调用exit
- 调用_exit 或 _Exit。_exit和_Exit函数不冲洗标准IO流。
- 进程的最后一个线程在其启动例程中执行return。但该线程的返回值不作为进程的终止状态。进程的终止状态总是0.(正常情况下)
- 进程的最后一个线程调用pthread_exit 函数,进程的终止状态总是0.
三种异常终止:
- 调用abort函数,它产生SIGABRT信号,是第二种异常终止的特例。
- 当进程接收到信号时(信号的来源可以是进程自身(如调用abort函数)、其他进程或内核)。比如除0或越界,内核就会产生信号。
- 最后一个线程对 “取消” 请求做出响应。一个线程要求取消另一个线程,一段时间后目标线程终止。
无论进程如何终止,都会执行内核中的同一段代码,这段代码用于关闭所有打开的描述符、释放占用的存储器。
退出状态是传给exit或_exit或_Exit函数的参数。_exit将内核的退出状态转换成终止状态,这个终止状态会被通过wait或waitpid传递给父进程。
如果父进程在子进程之前终止,内核会逐个检查所有活动进程,若该进程为要终止的进程的子进程,就把这个子进程的父进程ID改为1,保证其有父进程。
一个已经终止,但是父进程尚未进行善后处理(原本的父进程可能还在,只是未能及时处理)的进程被称为僵尸进程。
init进程,任何时候只要有一个子进程终止,init就会调用wait获取其状态。该子进程可能是init直接产生的进程(如getty),或者由init收养的进程。
6. wait 和 waitpid 函数
当一个进程正常或异常终止,内核向其父进程发送SIGCHLD信号。
父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理函数)。对于这种信号的系统默认动作是忽略。
调用wait或waitpid函数,接下来会发生什么?
- 如果子进程还在运行,阻塞。
- 如果一个子进程已终止,正在等待父进程获取其终止状态。获取该状态后立即返回。
- 如果没有子进程,则出错返回。
#include
#include
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
成功返回进程ID,出错返回0或-1
-
wait会使调用者阻塞,waitpid可以通过选项使得调用者不阻塞。
-
waitpid 并不一定等待其调用后的第一个终止的子进程,它可以通过选项来控制它所等待的进程。
wstatus可以为NULL,如果不关心子进程的终止状态的话。
检测子进程返回状态的宏:
实例:
1 #include
2 #include
3 #include
4 #include "apue.h"
5 void pr_exit(int status){
6
7 if(WIFEXITED(status)){
8 printf("normal termination , exit code = %d\n",
9 WEXITSTATUS(status));
10 }
11 else if(WIFSIGNALED(status)){
12 printf("abnormal termination, signal number = %d%s\n",
13 WTERMSIG(status),
14 #ifdef WCOREDUMP
15 WCOREDUMP(status)? "(core file generated)":"");
16 #else
17 "");
18 #endif
19 }
20 else if(WIFSTOPPED(status)){
21 printf("child stopped, signal number = %d\n",
22 WSTOPSIG(status));
23 }
24 }
25
26
27 int main()
28 {
29 pid_t pid;
30 int status;
31
32 pid = fork();
33 if(pid < 0){
34 err_sys("fork error");
35 }else if(pid == 0){
36 exit(7); //exit退出
37 }
38
39 if(wait(&status) != pid){
40 err_sys("wait error");
41 }
42
43 pr_exit(status);
44
45 pid = fork();
46 if(pid < 0){
47 err_sys("fork error");
48 }else if(pid == 0){
49 abort(); //进程自身产生SIGABRT信号
50 }
51
52 if(wait(&status) != pid){
53 err_sys("wait error");
54 }
55 pr_exit(status);
56
57
58 pid = fork();
59 if(pid < 0){
60 err_sys("fork error");
61 }else if(pid == 0){
62 status/=0; //除0异常,内核发出异常信号SIGFPE
63 }
64
65 if(wait(&status) != pid){
66 err_sys("wait error");
67 }
68 pr_exit(status);
69 return 0;
70 }
运行结果:
waitpid 函数中的pid参数:
- pid == -1 等待任一子进程,等效于wait
- pid > 0 等待进程ID与pid相等的子进程。
- pid == 0 等待组ID等于调用进程组ID相同的任一子进程。
- pid < 0 等待组ID等于pid绝对值的任一子进程
wait 只有当调用进程没有子进程时才会出错,waitpid 若指定进程或者进程组不存在,或者pid指定进程并非调用进程的子进程id,都会出错。
options选项如下,可或运算组合。

#include
#include
#include
#include
pid_t wait3(int *wstatus, int options,
struct rusage *rusage);
pid_t wait4(pid_t pid, int *wstatus, int options,
struct rusage *rusage);
成功返回进程ID,出错返回-1
struct rusage里存放有子进程的资源统计信息,如用户CPU时间,系统CPU时间,缺页次数和接收信号次数等。
10.exec函数
exec并不创建新进程,进程ID不发生改变。exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段和堆栈段。
/*库函数:*/
#include
extern char **environ;
int execl(const char *path, const char *arg, ...
/* (char *) NULL */);
int execlp(const char *file, const char *arg, ...
/* (char *) NULL */);
int execle(const char *path, const char *arg, ...
/*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);
出错返回-1,成功不返回
/*系统调用*/
#include
int execve(const char *filename, char *const argv[],
char *const envp[]);
当以file为参数时,若file中包含 ‘/’ ,则将其视为路径名。
否则按照PATH环境变量,在它所指定的各目录中搜寻可执行文件。
l 表示 list, v 表示矢量 vector。包含 l 的exec函数要求将每个命令行参数都声明为独立的参数,且以NULL结尾。 包含 v 的exec函数要求构造一个字符串指针数组,其中包含所有参数,并以NULL结尾。
字母e表示自己取envp数组,不使用当前环境environ
exec前后实际用户ID和实际组ID保持不变,有效ID是否改变取决于
所执行程序文件的设置用户ID位和设置组ID位是否设置。如果新程序的设置用户ID位已设置,则有效用户ID变为程序所有者ID,否则有效用户ID不变;有效组ID同上。
只有execve是系统调用,其他六个都是库函数,最终都要使用execve。
fexecve函数使用/proc把文件描述符转换成路径名。
13.system函数
#include
int system(const char *command);
system 实现中调用了fork, exec和waitpid,因此有三种返回值。
- fork失败或者waitpid返回除EINTR之外的出错,则system返回-1,并且设置errno以标识错误类型。
- 如果exec失败,则其返回值如同shell执行了exit(127);
- 否则所有3个函数都成功,那么system的返回值是shell的终止状态。
如果一个进程正以特殊的权限(设置用户ID或设置组ID)运行,它又想生成另一个进程执行另一个程序,则它应当直接使用fork和exec,而且在fork之后,exec之前要更改回普通权限。
设置用户ID或设置组ID程序决不应调用system函数。(安全漏洞)
16.进程调度
nice值越高,进程越“好”——它对cpu的占用越少。
进程可以通过nice函数获取或修改其nice值,但不可以修改其他进程的nice值。
#include
int nice(int inc);
成功,返回新的nice值;出错返回-1
incr参数被加到调用进程的nice值上。如果incr太大,系统会将其降到合法值,不给出提示。如果incr太小,系统也会无声息的将其提升到最小的合法值。
返回值为-1是合法的返回值,故不可以用-1判断出错。所以在调用nice函数之前要清除errno的值,返回-1时检测errno,若errno不为0,说明nice调用失败。
#include
int getpriority(int which, id_t who);
int setpriority(int which, id_t who, int value);
返回值:若成功返回0,出错返回-1
which可以为三个值之一:
- PRIO_PROCESS 表示进程,
- PRIO_PGRP 表示进程组,
- PRIO_USER 表示用户ID,
分别将who解释成:
- 一个或多个进程,若为0则表示调用进程/进程组/用户。
- 当which为PRIO_USER 并且who为0时,使用调用进程的实际用户ID。如果which作用于多个进程,则返回所有作用进程中优先级最高的(最小的nice值)。
setpriority 将value增加到NZERO上,然后变为新的nice值。
linux中子进程会继承父进程的nice值。
17.进程时间
#include
clock_t times(struct tms *buf);
成功返回流逝的墙上时钟时间(以滴答数为单位);若出错返回-1
流逝时间相对过去某个时间来度量。
习题
8.1
啊这,居然正常输出了,迷惑。
8.2
vfork使得子进程先运行,可能让子进程有机会修改f1函数的栈帧,然后父进程获得cpu,要从这个f1函数返回,然而栈帧被干碎了,返回个p,平不了栈,直接gg。
8.4
三个进程并发,各父子进程能同步,但这三对进程之间不同步。
发信号同步。。?进程间肯定得通信。
无论是父还是子先输出都解决不了问题。
8.6
不wait子进程即可。