多进程wait、僵尸进程、孤儿进程、prctl
1、概念
1、孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,从而保证每个进程都会有一个父进程。而Init进程会自动wait其子进程,因此被Init接管的所有进程都不会变成僵尸进程。
补充:孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程结束了其生命周期的时候,init进程就会处理它的一切善后工作。因此孤儿进程并不会有什么危害。
2、僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程。当用ps命令观察进程的执行状态时,看到这些进程的状态栏为defunct。僵尸进程是一个早已死亡的进程,但在进程表(processs table)中仍占了一个位置(slot)。
补充(内核):一个进程终止后,内核会释放终止进程(调用了exit系统调用)所使用的所有存储区,关闭所有打开的文件等。但内核为每一个终止子进程保存了一定量的信息,设置僵死状态来维护子进程的信息,以便父进程在以后某个时候获取,这些信息至少包括进程ID,进程的终止状态,以及该进程使用的CPU时间,所以当终止子进程的父进程调用wait或waitpid时就可以得到这些信息。任何一个子进程(init除外)在exit后并非马上就消失,而是留下一个称外僵尸进程的数据结构,等待父进程处理。这是每个子进程都必需经历的阶段。另外子进程退出的时候会向其父进程发送一个SIGCHLD信号。
严格来说,僵尸进程并不是问题的根源,罪魁祸首是产生出大量僵尸进程的那个父进程。因此,把产生大量僵尸进程的那个父进程kill掉(通过kill发送SIGTERM或者SIGKILL信号)之后,它产生的僵死进程就变成了孤儿进程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源。
2、如何避免僵尸进程?
(1)通过signal(SIGCHLD, SIG_IGN)通知内核对子进程的结束不关心,由内核回收。如果不想让父进程挂起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略SIGCHLD信号,该信号是子进程退出的时候向父进程发送的。
(2)父进程调用wait/waitpid等函数等待子进程结束,如果尚无子进程退出wait会导致父进程阻塞。waitpid可以通过传递WNOHANG使父进程不阻塞立即返回。
(3)如果父进程很忙可以用signal注册信号处理函数,在信号处理函数调用wait/waitpid等待子进程退出。
(4)通过两次调用fork。父进程首先调用fork创建一个子进程然后waitpid等待子进程退出,子进程再fork一个孙进程后退出。这样子进程退出后会被父进程等待回收,而对于孙子进程其父进程已经退出所以孙进程成为一个孤儿进程,孤儿进程由init进程接管,孙进程结束后,init会等待回收。
第一种方法忽略SIGCHLD信号,这常用于并发服务器的一个技巧,因为并发服务器常常fork很多子进程,子进程终结之后需要服务器进程去wait清理资源。如果将此信号的处理方式设为忽略,可让内核把僵尸子进程转交给init进程去处理,省去了大量僵尸进程占用系统资源。
3、wait()、waitpid()
wait()
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status)
(1)子进程结束时,系统向其父进程发送SIGCHILD信号;
(2)父进程调用wait函数后阻塞;
(3)父进程被SIGCHILD信号唤醒,然后去回收僵尸子进程;
(4)父子进程之间是异步的,SIGCHILD信号机制就是为了解决父子进程之间的异步通信问题,让父进程可以及时的去回收僵尸子进程。
(5)若父进程没有任何子进程则wait返回错误。
错误代码:(更多errno含义见cat /usr/include/asm-generic/errno-base.h和/usr/include/asm-generic/errno.h)
(1)ECHILD:没有等待的子进程;
(2)EINTR:未抓住信号,或该信号未设置,或未找到该信号。
#include <errno.h> int waitreturn; waitreturn = wait(&val); if(waitreturn == -1) { printf("errno:%d\n", errno); }
得到status信息:
由于status信息被存放在一个整数的不同二进制位中,不同平台有不同定义,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作,最常用的有:
(1)WIFEXITED(status) 这个宏用来指出子进程是否为正常退出(不是信号导致的退出),如果是,它会返回一个非零值。参数status是wait参数指针指向的整数。
(2)WEXITSTATUS(status) 当WIFEXITED返回非零值时,可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status) 就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说, WIFEXITED返回0,这个值就毫无意义。
其余:
(1)WIFSTOPPED/WSTOPSIG:当子进程是因为被一个信号暂停而返回时则WIFSTOPPED(status)为真,在这种情况下WSTOPSIG(status)返回这个暂停子进程信号的编号。
(2)WIFCONTINUED:当一个暂停的子进程被信号SIGCONT唤醒而返回状态,则WIFCONTINUED(status)为真,否则为假。
(3)WIFSIGNALED/WTERMSIG/WCOREDUMP:当程序异常终止时WIFSIGNALED(staus)为真,这种情况下WTERMSIG(status)返回终止进程的信号编号。并且程序异常终止时产生了core文件的话,则WCOREDUMP(status)为真,否则为假。
waitpid()
函数原型:
#include <sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
作用:
系统调用waitpid是wait的封装,waitpid只是多出了两个可由用户控制的参数pid和options,为编程提供了灵活性。
参数:
(1)pid
pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去
pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样
pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬
pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值
(2)options
如果使用了WNOHANG参数,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去
如果使用了WUNTRACED参数,则子进程进入暂停则马上返回,但结束状态不予以理会
Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用,比如:
ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);
如果不想使用它们,也可以把options设为0,如:ret=waitpid(-1,NULL,0);
返回值:
(1)当正常返回的时候waitpid返回收集到的子进程的进程ID;
(2)如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
(3)如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
(4)当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD。
得到status信息(和上面一样):
4、实验
以下实验了多种情况,用于理解父进程wait多个子进程
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
if(fork() == 0)
{
printf("1--This is the child process. pid =%d\n", getpid());
sleep(3);
printf("%d exit\n", getpid());
exit(0);
}
if(fork() == 0)
{
printf("2--This is the child process. pid =%d\n", getpid());
sleep(10);
printf("%d exit\n", getpid());
exit(0);
}
if(fork() == 0)
{
printf("3--This is the child process. pid =%d\n", getpid());
sleep(17);
printf("%d exit\n", getpid());
exit(0);
}
// while(1)
// {
// pid = wait(NULL);
// printf("parent:%d,return of wait:%d\n", getpid(), pid);
// if(pid == -1)
// {
// break;
// }
// }
//循环wait每一个子进程输出,最后返回值是-1表示没有子进程了,该父进程也退出
//while(wait(NULL) != -1);
//wait最后一个子进程退出后,该父进程退出
//printf("parent:%d,return of wait:%d\n", getpid(), wait(NULL));
//wait第一个返回的进程后,该父进程就返回,其他未返回进程变孤儿进程,由init进程接管
// while(wait(NULL) != -1)
// {
// printf("parent:%d,return of wait:%d\n", getpid(), wait(NULL));
// }
//while里wait第一个退出的子进程,printf里wait第二个子进程输出,while再wait第三个子进程,最后printf里是-1,表明已经没有子进程,父进程退出
signal(SIGCHLD,SIG_IGN);//忽略子进程exit,内核直接转交init处理defunct,这样就算父进程没退出也不会有僵尸进程
sleep(20);//父进程没有wait任何子进程,没有设置SIG_IGN,且还在运行中时子进程就退出了,此时所有子进程变僵尸进程,直到父进程退出后,子进程被init进程接管处理
printf("exit\n");
return 0;
}
总结:
调用一个wait,则第一个子进程返回后该父进程也返回,那其他子进程还在运行,为何没有变成僵尸进程,而是直接由init接管
-----父进程返回了,所以其他子进程变孤儿进程,直接由init接管,所以ps –ef | grep defunct没有僵尸进程
如果父进程fork多个子进程,子进程退出后,该父进程还在运行中,但没wait任何子进程,则此时的子进程会变僵尸进程,直到父进程结束,然后由init进程接管
-----父进程不wait阻塞,而且在子进程结束后还在运行。此时为了不产生僵尸进程,可以在父进程中设置signal(SIGCHLD,SIG_IGN); 使得父进程忽略子进程退出,把子进程直接交由init接管
5、父进程退出(正常/异常退出)让子进程也退出
C语言。直接在子进程中加:prctl(PR_SET_PDEATHSIG, SIGHUP);
python。在子进程中:
import signal
import prctl
prctl.set_pdeathsig(signal.SIGHUP)
需要下载对应python-prctl:
1、ubuntu: apt-get install build-essential libcap-dev pip install python-prctl
2、centos: yum install gcc glibc-devel libcap-devel easy_install python-prctl
在 Linux 中,进程可以要求内核在父进程退出的时候给自己发信号。如上在子进程中使用prctl系统调用,父进程挂了后,子进程就会收到SIGHUP信号,系统对SIGHUP信号的默认处理是终止收到该信号的进程。所以若程序中没有捕捉该信号,当收到该信号时,进程就会退出。
(1)关于linux信号
各种信号的说明参见:signal
linux下:
ctrl-c 发送 SIGINT 信号给前台进程组中的所有进程。常用于终止正在运行的程序。
ctrl-z 发送 SIGTSTP 信号给前台进程组中的所有进程。常用于挂起一个进程,暂停执行,放入后台,可通过jobs显示当前暂停的进程,使用fg让最后一个进程在前台运行(fg %N使第N个到前台,bg %N到后台)。
ctrl-d 不是发送信号,而是表示一个特殊的二进制值,表示 EOF。退出当前shell,相当于exit命令。
ctrl-\ 发送 SIGQUIT 信号给前台进程组中的所有进程,终止前台进程并生成 core 文件。
Key Function
Ctrl-c Kill foreground process
Ctrl-z Suspend foreground process
Ctrl-d Terminate input, or exit shell
Ctrl-s Suspend output
Ctrl-q Resume output
Ctrl-o Discard output
Ctrl-l Clear screen
(2)关于prctl
a.使用:
int prctl ( int option,unsigned long arg2,unsigned long arg3,unsigned long arg4,unsigned long arg5 )
option可选(各种options含义):
PR_SET_PDEATHSIG :arg2作为处理器信号pdeath被输入,正如其名,如果父进程不能再用,进程接受这个信号。
PR_SET_NAME :把参数arg2作为调用进程的进程名字。(SinceLinux 2.6.11)(此方法改变了task_struct的comm。现象是:/proc/$pid/status和pstree -p都变为新名字,/proc/$pid/cmdline和ps -aux |grep $pid没变)(其他修改进程名的方法)
b.内核源码:
SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3, unsigned long, arg4, unsigned long, arg5) { struct task_struct *me = current; unsigned char comm[sizeof(me->comm)]; long error; error = security_task_prctl(option, arg2, arg3, arg4, arg5); if (error != -ENOSYS) return error; error = 0; switch (option) { case PR_SET_PDEATHSIG: if (!valid_signal(arg2)) { error = -EINVAL; break; } me->pdeath_signal = arg2;//父进程dies,传递给子进程的signal break; ... case PR_SET_NAME: comm[sizeof(me->comm) - 1] = 0; if (strncpy_from_user(comm, (char __user *)arg2, sizeof(me->comm) - 1) < 0) return -EFAULT; set_task_comm(me, comm);//修改task_struct->comm proc_comm_connector(me); break; ... }
概念参考:
https://www.cnblogs.com/wuchanming/p/4020463.html
https://www.cnblogs.com/Anker/p/3271773.html
https://blog.csdn.net/astrotycoon/article/details/41172389