进程间的信号处理
进程状态
Linux系统下进程通常存在6种不同的状态,分为:就绪态、运行态、僵尸态、可中断睡眠状态(浅度睡眠)、不可中断睡眠状态(深度睡眠)以及暂停态。
就绪态(Ready):指该进程满足被 CPU 调度的所有条件但此时并没有被调度执行,只要得到 CPU就能够直接运行;意味着该进程已经准备好被 CPU 执行,当一个进程的时间片到达,操作系统调度程序会从就绪态链表中调度一个进程;
运行态:指该进程当前正在被 CPU 调度运行,处于就绪态的进程得到 CPU 调度就会进入运行态;
僵尸态:僵尸态进程其实指的就是僵尸进程,指该进程已经结束、但其父进程还未给它"收尸";
可中断睡眠状态:可中断睡眠也称为浅度睡眠,表示睡的不够"死",还可以被唤醒,一般来说可以通过信号来唤醒;
不可中断睡眠状态:不可中断睡眠称为深度睡眠,深度睡眠无法被信号唤醒,只能等待相应的条件成立才能结束睡眠状态。把浅度睡眠和深度睡眠统称为等待态(或者叫阻塞态),表示进程处于一种等待状态,等待某种条件成立之后便会进入到就绪态,所以处于等待态的进程是无法参与进程系统调度的;
暂停态:暂停并不是进程的终止,表示进程暂停运行,一般可通过信号将进程暂停,譬如SIGSTOP信号,处于暂停态的进程是可以恢复进入到就绪态的,譬如收到 SIGCONT 信号。
进程间关系
进程间还存在着其它一些层次关系,譬如进程组和会话,所以由此可知进程间存在着多种不同的关系,主要包括:无关系(相互独立)、父子进程关系、进程组以及会话。
a.无关系
两个进程间没有任何关系,相互独立。
b.父子进程关系
两个进程间构成父子进程关系,譬如一个进程 fork()创建出了另一个进程,那么这两个进程间就构成了父子进程关系,调用 fork()的进程称为父进程、而被 fork()创建出来的进程称为子进程;当然,如果父进程先于子进程结束,那么 init 进程就会成为子进程的父进程,它们之间同样也是父子进程关系。
c.进程组
每个进程除了有一个进程 ID、父进程 ID 之外,还有一个进程组 ID,用于标识该进程属于哪一个进程组,进程组是一个或多个进程的集合,这些进程并不是孤立的,它们彼此之间或者存在父子、兄弟关系,或者在功能上有联系。
假设为了完成一个任务,需要并发运行 100个进程,但当处于某种场景时需要终止这 100 个进程,若没有进程组就需要一个一个去终止,这样非常麻烦且容易出现一些问题,有了进程组的概念之后,就可以将这 100 个进程设置为一个进程组,这些进程共享一个进程组 ID,这样一来,终止这 100 个进程只需要终止该进程组即可。
父子进程
创建子进程
有两种模式,第一种复制父进程的大部分内容(一般直接调用fork函数),第二种重新开始,不需要去继承父进程的数据段、堆、栈以及继承了父进程打开的文件描述符(一般是调用fork函数后,子进程调用exec)。
第一种(需要继承)
一个现有的进程可以调用 fork()函数创建一个新的进程,调用 fork()函数的进程称为父进程
由 fork()函数创建出来的进程被称为子进程,每个进程都会从 fork()函数的返回处继续执行,
会导致调用 fork()返回两次值,可通过返回值来区分是子进程还是父进程。
子进程是父进程的一个副本,子进程拷贝了父进程的数据段、堆、栈以及继承了父进程打开的文件描述符,父进程与子进程并不共享这些存储空间
fork()调用成功后,将会在父进程中返回子进程的 PID,而在子进程中返回值是 0
父子进程文件共享
因为调用 fork()函数之后,子进程会获得父进程所有文件描述符的副本
这也意味着父、子进程对应的文件描述符均指向相同的文件表
一方更新了文件偏移量,另一方也会进行更新(相当于父子进程交替写文件,会进行往下更新,而不是覆盖)
监视子进程
父进程需要知道子进程于何时被终止,并且需要知道子进程的终止状态信息,是正常终止还是异常终止亦或者被信号终止等
所以需要父进程对子进程的监视
wait()函数(有些限制最好用waitpid)
wait()可以等待任意子进程终止,获得子进程终止状态信息
调用时和之前没有子进程终止,就会阻塞
调用之前有一个或多个子进程终止,调用wait()不会阻塞,除了返回终止信息,还会给子进程"收尸",一次wait只能处理一次
子进程的终止状态可以用宏来进行检查
WIFEXITED(stat_val)
如果子进程为正常终止则为非零值。
WEXITSTATUS(stat_val)
如果WIFEXITED返回的是非零值,WEXITSTATUS可以返回子进程调用_exit()或 exit()时指定的退出状态
WIFSIGNALED(stat_val)
由于接收到未捕获的信号而终止的子进程返回为非零值
WTERMSIG(stat_val)
WIFSIGNALED返回为非零值,WTERMSIG可以返回导致子进程终止的信号编号
void signal_handler(int signo) { pid_t pid = 0; int isExited, isSignaled, status, sigNum; // 打印信号信息 printf("[ ABORT ] pid=%d recv signo=%d", getpid(), signo); // 信号处理 switch (signo) { case SIGCHLD: { // SIGCHLD信号处理 while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { isExited = WIFEXITED(status); isSignaled = WIFSIGNALED(status); sigNum = WTERMSIG(status); printf("[ABORT] pid=%d, sig=%d, recv sigNum=%d, isSignaled=%d, isExited=%d", pid, signo, sigNum, isSignaled, isExited); } } break; default: break; } }
孤儿进程
父进程先于子进程结束,也就是意味着,此时子进程变成了一个"孤儿"
在 Linux 系统当中,所有的孤儿进程都自动成为 init 进程(进程号为 1)的子进程
某一子进程的父进程结束后,该子进程调用 getppid()将返回 1,init 进程变成了孤儿进程的"养父"
僵尸进程
进程结束之后,通常需要其父进程为其"收尸",回收子进程占用的一些内存资源,父进程通过调用
wait()(或其变体 waitpid()、waitid()等)函数回收子进程资源,归还给系统。
如果父进程并没有调用 wait()函数然后就退出了,那么此时 init 进程将会接管它的子进程并
自动调用 wait(),故而从系统中移除僵尸进程。
但是父进程有自己的事情做,不能总在wait()阻塞,或者轮询的使用waitpid()
这时候因为子进程的结束是异步的,所以使用信号机制来处理
SIGCHLD 信号
当父进程的某个子进程终止时,父进程会收到 SIGCHLD 信号;
当父进程的某个子进程因收到信号而停止(暂停运行)或恢复时,内核也可能向父进程发送该信号。
这时候我们可以用信号捕获它,再调用wait()处理
当调用信号处理函数时,会暂时将引发调用的信号添加到进程的信号掩码中(除非 sigaction()指定了 SA_NODEFER 标志),这样一来,当 SIGCHLD 信号处理函数正在为一个终止的子进程"收尸"时,如果相继有两个子进程终止,即使产生了两次 SIGCHLD 信号,父进程也只能捕获到一次 SIGCHLD 信号,结果是,父进程的 SIGCHLD 信号处理函数每次只调用一次 wait(),那么就会导致有些僵尸进程成为"漏网之鱼"。
父进程收到了SIGCHLD 信号,在使用信号处理函数的时候,会把这个信号放入信号掩码中,因为SIGCHLD是不可靠信号,后面的SIGCHLD将会被丢弃。