[ 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的两种用法:

  1. 父进程希望复制自己使得子进程执行不同的代码段
  2. 一个进程要执行不同的程序。

vfork产生一个轻量级的进程,很不安全。

它创建新进程,且保证子进程先运行。它不将父进程地址空间完全复制,认为子进程会立即调用exec或exit,但是一旦调用其他函数或者修改数据就会带来不可预知的后果。(一般用于没有MMU的机器或者性能要求极高的场合,查了查应该没啥用了)。

注意vfork用_exit,用exit会坑死父进程。

5.exit函数

五种正常终止方式:

  1. main函数中调用return。等同于调用exit
  2. 调用exit
  3. 调用_exit 或 _Exit。_exit和_Exit函数不冲洗标准IO流。
  4. 进程的最后一个线程在其启动例程中执行return。但该线程的返回值不作为进程的终止状态。进程的终止状态总是0.(正常情况下)
  5. 进程的最后一个线程调用pthread_exit 函数,进程的终止状态总是0.

三种异常终止:

  1. 调用abort函数,它产生SIGABRT信号,是第二种异常终止的特例。
  2. 当进程接收到信号时(信号的来源可以是进程自身(如调用abort函数)、其他进程或内核)。比如除0或越界,内核就会产生信号。
  3. 最后一个线程对 “取消” 请求做出响应。一个线程要求取消另一个线程,一段时间后目标线程终止。

无论进程如何终止,都会执行内核中的同一段代码,这段代码用于关闭所有打开的描述符、释放占用的存储器。

退出状态是传给exit或_exit或_Exit函数的参数。_exit将内核的退出状态转换成终止状态,这个终止状态会被通过wait或waitpid传递给父进程。

如果父进程在子进程之前终止,内核会逐个检查所有活动进程,若该进程为要终止的进程的子进程,就把这个子进程的父进程ID改为1,保证其有父进程。

一个已经终止,但是父进程尚未进行善后处理(原本的父进程可能还在,只是未能及时处理)的进程被称为僵尸进程。

init进程,任何时候只要有一个子进程终止,init就会调用wait获取其状态。该子进程可能是init直接产生的进程(如getty),或者由init收养的进程。

6. wait 和 waitpid 函数

当一个进程正常或异常终止,内核向其父进程发送SIGCHLD信号。

父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理函数)。对于这种信号的系统默认动作是忽略。

调用wait或waitpid函数,接下来会发生什么?

  1. 如果子进程还在运行,阻塞。
  2. 如果一个子进程已终止,正在等待父进程获取其终止状态。获取该状态后立即返回。
  3. 如果没有子进程,则出错返回。
#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选项如下,可或运算组合。

![image-20200916004739173](第八章 进程控制.assets/image-20200916004739173.png)

#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,因此有三种返回值。

  1. fork失败或者waitpid返回除EINTR之外的出错,则system返回-1,并且设置errno以标识错误类型。
  2. 如果exec失败,则其返回值如同shell执行了exit(127);
  3. 否则所有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可以为三个值之一:

  1. PRIO_PROCESS 表示进程,
  2. PRIO_PGRP 表示进程组,
  3. PRIO_USER 表示用户ID,

分别将who解释成:

  1. 一个或多个进程,若为0则表示调用进程/进程组/用户。
  2. 当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子进程即可。