大纲
wait
函数waitpid
函数在Linux中正常情况下,子进程是通过父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步过程,也就是说,父进程永远无法预知到子进程会在什么时候结束。当一个进程完成工作终止后,它的父进程需要调用wait()
或waitpid()
系统调用来获取子进程的终止状态。
子进程有两种比较重要的状态,分别是孤儿进程和僵尸进程。
孤儿进程是指父进程先于子进程结束,通俗来讲也就是爹比儿子死得早,此时子进程成为孤儿进程,此时父进程会成为init
进程(进程号pid
为1的进程),init
进程可视为进程孤儿院用于领养孤儿进程。
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.1 0.4 225808 9512 ? Ss 00:43 0:02 /sbin/init splash
$ ps aux | grep init
root 1 0.1 0.4 225808 9512 ? Ss 00:43 0:02 /sbin/init splash
jc 3613 0.0 0.0 21536 1000 pts/0 S+ 01:18 0:00 grep --color=auto init
当一个父进程退出而它的子进程还在运行,此时这些子进程将会成为孤儿进程,孤儿进程将被init
进程收养,并由init
进程对它们完成状态收集工作。
危害
孤儿进程是没有父进程的子进程,因此孤儿进程的管理重任会落到init
进程身上,init
进程好像一个民政局或孤儿所,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程时内核就会把孤儿进程的父进程设置为init
进程,init
进程会循环地wait
已经退出的子进程。这样,当一个孤儿进程的生命周期结束时,init
进程会代表党和政府处理善后工作,因此孤儿进程不会有什么危害。
实例1
$ vim orphan.c
#include
#include
#include
int main(void)
{
pid_t pid;
pid = fork();//创建子进程
//如果当前进程是init进程则报错退出
if(pid == 1)
{
perror("init process");
exit(1);
}
else if(pid > 0)
{
sleep(1);
printf("parent pid = %d, parent parent pid is %d\n", getpid(), getppid());
}
else if(pid == 0)
{
printf("child pid is %d, parent pid is %d\n", getpid(), getppid());
sleep(3);
printf("child pid is %d, parent pid is %d\n", getpid(), getppid());
}
return 0;
}
$ gcc orphan.c -o orphan -Wall -g
$ ./orphan
child pid is 3652, parent pid is 3651
parent pid = 3651, parent parent pid is 3633
root@junchow:/home/jc/projects/c# pachild pid is 3652, parent pid is 2700
$ ps aux | grep 2700
jc 2700 0.0 0.3 77120 8292 ? Ss 00:57 0:00 /lib/systemd/systemd --user
root 3672 0.0 0.0 21536 1052 pts/0 S+ 01:21 0:00 grep --color=auto 2700
实例2
$ vim orphan.c
#include
#include
#include
int main(void)
{
pid_t pid;
pid = fork();
if(pid == 0)
{
while(1)
{
printf("child: parent pid=%d\n", getppid());
sleep(1);
}
}
else if(pid > 0)
{
printf("parent: pid=%d\n", getpid());
sleep(10);
printf("parent: going to die\n");
}
else
{
perror("fork");
return 1;
}
return 0;
}
$ gcc orphan.c -o orphan -Wall -g
$ ./orphan
僵尸进程是如何产生的呢?
当运行一个程序时会产生一个父进程以及多个子进程,所有子进程都会消耗内核分配的内存和CPU资源。子进程完成执行后会发送一个exit
信号然后死掉。这个exit
型号需要被父进程读取。父进程需要随后调用wait
命令来读取进程的退出状态,并将子进程从进程表中移除。
在UNIX系统中一旦进程结束,如果父进程没有使用wait
或waitpid
系统调用等待子进程结束,又没有显式的忽略SIGCHILD
信号,那么它将变成一个僵尸进程并一直存在。僵尸进程是一个早已死亡的进程,但在进程表中任占据着一个位置。
如果子进程的父进程已先行结束,那么子进程就不会变成僵尸进程,因为每个进程结束的时候,系统会扫描当前操作系统中所有运行的进程,检查有没有哪个进程是刚刚结束的进程的子进程。若有则由init
进程来接管并作为它的父进程,从而保证每个进程都会有一个父进程。init
进程会自动wait
其子进程,因此被init
接管的进程都不会变成僵尸进程。
僵尸进程是指进程终止,父进程尚未回收,子进程残留资源PCB
存放于内核kernel
中变为僵尸zombie
进程。
父进程有义务将子进程回收,如果子进程死亡后父进程不帮子进程收尸,此时子进程就会变成僵尸。
如何查找僵尸进程呢?
$ ps -ef | grep defunct
$ ps aux | grep Z
僵尸进程存在会有什么样的危害呢?
正常进程死亡以后,0到4G的进程地址空间会主动释放,但是PCB依然会残留在内核中,其目的是为了让父进程为它报仇。如果子进程死亡后没有任何痕迹留下的话,父进程将无法知道子进程是由于什么原因造成的死亡,是自杀还是他杀呢?子进程死亡后会在内核中残留PCB,父进程通过PCB可以获取子进程的死亡状态。
任何一个子进程(init
进程除外)在exit
退出后并非立即消失,而会留下一个称为僵尸进程的数据结构,以等待父进程处理。
UNIX提供了一种机制让父进程获取子进程结束时的状态信息,这种机制就是在每个子进程退出的时候,内核会释放该进程所有的资源,包括打开的文件、占用的内存等。但内核仍然会保留一定的信息,如进程号、退出状态、运行时间等信息。直到父进程通过wait
或waitpid
系统调用来获取时才完全释放。
这样就导致了一个问题,如果进程不调用 wait
或waitpid
,那么保留在内核中的残留信息将不会主动被释放,进程号会一直被占用。由于系统所能使用的进程号是有限的,如果大量产生僵尸进程的话,系统将没有可用的进程号,从而导致系统不能产生新的进程。
此时如果使用ps
命令查看会发现子进程的状态为Z
,即僵尸进程。
严格来说,僵尸进程并非罪魁祸首,问题的根源在于产生大量僵尸进程的那个父进程。可以使用kill
命令发送信号SIGTERM
或SIGKILL
信号枪毙元凶进程。枪毙完成后,僵尸进程将会变成了孤儿进程,这些孤儿进程会被ini进程接管。
init进程会
wait`这些孤儿进程,释放他们占用的系统资源。
需要注意的是僵尸进程不能使用kill
命令清除,因为kill
命令只是用来终止进程,而僵尸进程已经是终止的。
如何避免僵尸进程呢?
僵尸进程的产生是因为父进程没有wait
子进程,当系统中出现了僵尸进程,是无法通过kill
命令来清除的,但可以杀死僵尸进程的父进程,让其变成孤儿进程,系统会统一管理和清理孤儿进程。
有什么办法可以清除掉僵尸进程呢?
正常情况下可以使用SIGKILL
信号来杀死进程,由于僵尸进程已经早死了,所以不能使用kill
命令杀死已经死掉的进程。
$ kill -s SIGCHILD pid
可使用wait
和waitpid
回收僵尸进程
实例
$ vim zoombie.c
#include
#include
#include
int main(void)
{
pid_t pid;
//创建子进程
pid = fork();
printf("pid=%d\n", pid);
//若子进程为调度进程
if(pid == 0)
{
printf("child: parent=%d\n", getppid());
printf("child: going to sleep\n");
sleep(10);
printf("child: child die\n");
}
else if(pid > 0)
{
while(1)
{
printf("parent: pid=%d child=%d\n", getpid(), pid);
sleep(1);
}
}
else
{
perror("fork error");
return 1;
}
return 0;
}
$ gcc zoombie.c -o zoombie -Wall -g
$ ./zoombie
运行结果
pid=3654
parent: pid=3653 child=3654
pid=0
child: parent=3653
child: going to sleep
parent: pid=3653 child=3654
parent: pid=3653 child=3654
parent: pid=3653 child=3654
parent: pid=3653 child=3654
parent: pid=3653 child=3654
parent: pid=3653 child=3654
parent: pid=3653 child=3654
parent: pid=3653 child=3654
parent: pid=3653 child=3654
child: child die
parent: pid=3653 child=3654
parent: pid=3653 child=3654
parent: pid=3653 child=3654
...
注意若运行出现implicit declaration of function ‘fork’ [-Wimplicit-function-declaration]
则需注意是否引入了unistd.h
头文件。
一个进程在终止时会关闭所文件描述符,释放用户空间并分配内存,只是子进程的PCB还保留着,内核在其中保持努着一些信息。如果是正常终止或保存退出状态。如果是异常终止则保存着导致该进程的信号,这个进程可使用Shell中特殊。
父进程调用wait
系统调用可以回收子进程的终止信息,然后彻底清除掉这个进程,一个进程的状态可以在Shell中的特殊变量$?
来查看。
wait
函数的三个功能分别是
pid_t wait(int *status)
wait
函数成功则清理掉子进程ID,若失败则返回-1表示没有子进程。
当进程终止时操作系统的隐式回收机制
进程终止时内核的PCB仍旧存在,其中保存该进程的退出状态,简单来说,正常终止时返回退出值,异常终止时返回终止信号。
实例:父进程回收子进程
$ vim wait.c
#include
#include
#include
#include
int main(void)
{
pid_t pid;//创建子进程的进程ID
pid_t wpid;//回收子进程的返回值
//创建子进程
pid = fork();
printf("pid=%d\n", pid);
//若子进程为调度进程
if(pid == 0)
{
//子进程休眠10秒后死亡
printf("child: parent=%d\n", getppid());
printf("child: going to sleep\n");
sleep(10);
printf("child: child die\n");
}
else if(pid > 0)
{
//回收子进程若失败则提示错误
wpid = wait(NULL);
if(wpid == -1)
{
perror("wait error");
exit(1 );
}
while(1)
{
printf("parent: pid=%d child=%d\n", getpid(), pid);
sleep(1);
}
}
else
{
perror("fork error");
return 1;
}
return 0;
}
$ gcc wait.c -o wait -Wall -g
$ ./wait
pid=3757
pid=0
child: parent=3756
child: going to sleep
child: child die
parent: pid=3756 child=3757
parent: pid=3756 child=3757
parent: pid=3756 child=3757
...
此时若查看进程状态会发现并未出现僵尸进程
$ ps aux|grep wait
jc 3756 0.0 0.0 4508 740 pts/0 S+ 22:42 0:00 ./wait
jc 3767 0.0 0.0 21536 1060 pts/1 S+ 22:42 0:00 grep --color=auto wait
子进程死亡状态
当进程终止时刻使用wait
函数传出参数status
来保存进程的推出状态,借助宏函数来进一步判断进程终止的具体原因。宏函数可分为三组:
WIFEXITED(status)
:全称wait if exited
表示判断是否是正常退出的WEXITSTATUS(status)
:全称wait exit status
表示退出状态WIFEXITED(status)
为真则使用宏WEXITSTATUS(status)
以获取进程退出的状态,即exit
的参数。#include
#include
#include
#include
int main(void)
{
pid_t pid;//创建子进程的进程ID
pid_t wpid;//回收子进程的返回值
int status;//回收子进程返回的状态
//创建子进程
pid = fork();
printf("pid=%d\n", pid);
//若子进程为调度进程
if(pid == 0)
{
//子进程休眠3秒后死亡
printf("child-%d: parent=%d\n", pid, getppid());
printf("child-%d: going to sleep 3 seconds\n", pid);
sleep(3);
printf("child-%d: child die\n", pid);
exit(76);//退出值不超过128
}
else if(pid > 0)
{
//回收子进程
wpid = wait(&status);
//回收子进程若失败则提示错误
if(wpid == -1)
{
perror("wait error");
exit(1);
}
//若子进程回收成功
printf("child-%d: wait if existed with %d\n", pid, WIFEXITED(status));
if(WIFEXITED(status) != 0)
{
//获取子进程成功退出的状态值
printf("child-%d: wait exit status with %d\n", pid, WEXITSTATUS(status));
}
while(1)
{
printf("parent-%d: child=%d\n", getpid(), pid);
sleep(1);
}
}
else
{
perror("fork error");
return 1;
}
return 0;
}
编译运行程序并查看状态
$ gcc wait.c -o wait -Wall -g
$ ./wait
pid=3934
pid=0
child-0: parent=3933
child-0: going to sleep 3 seconds
child-0: child die
child-3934: wait if existed with 1
child-3934: wait exit status with 76
parent-3933: child=3934
parent-3933: child=3934
parent-3933: child=3934
...
注意:此处使用exit(76);
也可以使用return 76;
进行替换。
WIFSIGNALED(status)
:全称wait if signaled
WTERMSIG(status)
:全称wait term signal
WIFSIGNALED(status)
为真则使用宏WTERMSIG(status)
以获得使进程终止的那个信号的编号。#include
#include
#include
#include
int main(void)
{
pid_t pid;//创建子进程的进程ID
pid_t wpid;//回收子进程的返回值
int status;//回收子进程返回的状态
//创建子进程
pid = fork();
printf("pid=%d\n", pid);
//若子进程为调度进程
if(pid == 0)
{
//子进程休眠后死亡
printf("child-%d: parent=%d\n", pid, getppid());
printf("child-%d: going to sleep 60 seconds\n", pid);
sleep(60);
printf("child-%d: child die\n", pid);
exit(76);//退出值不超过128
}
else if(pid > 0)
{
//回收子进程
wpid = wait(&status);
//回收子进程若失败则提示错误
if(wpid == -1)
{
perror("wait error");
exit(1 );
}
//若子进程正常退出
printf("child-%d: wait if existed with %d\n", pid, WIFEXITED(status));
if(WIFEXITED(status) != 0)
{
//获取子进程成功退出的状态值
printf("child-%d: wait exit status with %d\n", pid, WEXITSTATUS(status));
}
//若子进程异常退出
printf("child-%d: wait if signaled with %d\n", pid, WIFSIGNALED(status));
if(WIFSIGNALED(status) != 0)
{
printf("child-%d: wait term sig with %d\n", pid, WTERMSIG(status));
}
while(1)
{
printf("parent-%d: child=%d\n", getpid(), pid);
sleep(1);
}
}
else
{
perror("fork error");
return 1;
}
return 0;
}
编译并运行程序
$ gcc wait.c -o wait -Wall -g
$ ./wait
此时让子进程休眠60秒,为什么呢,在60秒内新开命令行窗口并杀死子进程和父进程查看输出状态。
$ ps aux|grep wait
root 1139 0.0 0.7 194320 17560 ? Ssl 22:03 0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
jc 4030 0.0 0.0 4508 856 pts/0 S+ 23:44 0:00 ./wait
jc 4031 0.0 0.0 4508 72 pts/0 S+ 23:44 0:00 ./wait
jc 4034 0.0 0.0 21536 1148 pts/1 S+ 23:44 0:00 grep --color=auto wait
此时会发现有两个进程正在运行,4030对应的应该是父进程,4031对应的应该是子进程,接下来分别杀死进程。
$ kill -9 4031
$ kill -9 4030
最后查看日志输出信息
pid=4031
pid=0
child-0: parent=4030
child-0: going to sleep 60 seconds
child-4031: wait if existed with 0
child-4031: wait if signaled with 1
child-4031: wait term sig with 9
parent-4030: child=4031
parent-4030: child=4031
parent-4030: child=4031
parent-4030: child=4031
parent-4030: child=4031
parent-4030: child=4031
parent-4030: child=4031
parent-4030: child=4031
parent-4030: child=4031
parent-4030: child=4031
已杀死
此处需注意child-4031: wait term sig with 9
中的9是由于使用kill -9 4031
,也就是向进程发送的9号信号。表示什么意思呢?
$ kill -L
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
根据命令可以知道9号信号表示SIGKILL
WIFSTOPPED(status)
:全称wait if stopped
进程处于暂停状态WSTOPSIG(status)
WIFSTOPPED(status)
为真则使用宏WSTOPSIG(status)
以获取使进程暂停的那个信号的编号。WIFCONTINUED(status)
WIFCONTINUED(status)
为真则表示进程暂停后已经继续运行子进程死亡一般会存在两种情况,一种是正常死亡寿终正寝,此时程序将返回0。另一种情况是异常退出,在Linux中所有的异常退出都是由于信号导致的,由于子进程收到了某个特殊信号它才异常退出。所以异常终止时父进程需要回收子进程异常终止的信号。
waitpid
函数作用于wait
函数相同,但可以指定pid
进程清理且不阻塞。
pid_t waitpid(pid_t pid, int *status, int options)
waitpid
若成功则返回清理掉的进程ID,若失败则返回-1表示无子进程。
wait
函数与waitpid
的区别在于一次wait
调用只能回收一个子进程,而waitpid
可以指定进程ID进行回收更加灵活。
waitpid
参数pid
返回情况
pid
小于-1表示回收指定进程组中的任意子进程pid
等于0表示回收和当前调用waitpid
一个组的所有子进程pid
等于-1表示回收任意子进程,作用相当于wait
函数。pid
大于0表示回收指定进程ID的子进程waitpid
参数options
可以设置是否阻塞,设置非阻塞可使用WNOHANG
宏,设置非阻塞后需要使用轮询的方式定时查看子进程是否回收成功。
$ vim waipid.c
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int n = 5;//默认创建5个子进程
int i;//循环变量
pid_t pid;//子进程ID
pid_t tmp;
//若命令行参数个数是2个
if(argc == 2)
{
n = atoi(argv[1]);
}
//循环创建子进程
for(i=0; i
$ gcc waitpid.c -o waitpid -Wall -g
$ ./waitpid
tmp=5043
tmp=5043
tmp=0
tmp=0
tmp=0
tmp=0
child1: pid=5040
child2: pid=5041
child3: pid=5042
child4: pid=5043
child5: pid=5044
parent: pid=5039
$ ps aux|grep waitpid
jc 5039 81.6 0.0 4508 756 pts/0 R+ 03:52 0:25 ./waitpid
jc 5041 0.0 0.0 0 0 pts/0 Z+ 03:52 0:00 [waitpid]
jc 5042 0.0 0.0 0 0 pts/0 Z+ 03:52 0:00 [waitpid]
jc 5043 0.0 0.0 0 0 pts/0 Z+ 03:52 0:00 [waitpid]
jc 5044 0.0 0.0 0 0 pts/0 Z+ 03:52 0:00 [waitpid]
可以查看到当前存在4个僵尸进程Z+
,使用wait
回收时每次只回收了一个进程,如果需要同时回收5个进程,应该怎么办呢?
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int n = 5;//默认创建5个子进程
int i;//循环变量
pid_t pid;//子进程ID
pid_t tmp;
//若命令行参数个数是2个
if(argc == 2)
{
n = atoi(argv[1]);
}
//循环创建子进程
for(i=0; i
此处会发现如果需要回收多个进程,可使用while
循环的方式。
如果现在需要指定某个进程进行回收呢?应该怎么办?
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int n = 5;//默认创建5个子进程
int i;//循环变量
pid_t pid;//子进程ID
pid_t tmp;
//若命令行参数个数是2个
if(argc == 2)
{
n = atoi(argv[1]);
}
//循环创建子进程
for(i=0; i
$ gcc waitpid.c -o waitpid -Wall -g
$ ./waitpid
tmp=5097
tmp=5097
tmp=0
tmp=0
tmp=0
tmp=0
child1: pid=5094
child2: pid=5095
child3: pid=5096
child4: pid=5097
child5: pid=5098
parent: pid=5093
$ ps aux|grep waitpid
jc 5093 54.5 0.0 4508 740 pts/0 R+ 04:05 0:06 ./waitpid
jc 5094 0.0 0.0 0 0 pts/0 Z+ 04:05 0:00 [waitpid]
jc 5095 0.0 0.0 0 0 pts/0 Z+ 04:05 0:00 [waitpid]
jc 5096 0.0 0.0 0 0 pts/0 Z+ 04:05 0:00 [waitpid]
jc 5098 0.0 0.0 0 0 pts/0 Z+ 04:05 0:00 [waitpid]
jc 5100 0.0 0.0 21536 1028 pts/1 S+ 04:05 0:00 grep --color=auto waitpid
此处会发现pid=5087
的子进程被回收了
未完待续...