进程控制主要是指进程的退出
, 进程的回收
和进程的特殊状态 孤儿进程
和僵尸进程
。
1. 结束进程
想要直接退出某个进程可以在程序的任何位置调用exit()
或者_exit()
函数。
函数的参数相当于退出码, 如果参数值为 0 程序退出之后的状态码就是0, 如果是100退出的状态码就是100。
// 专门退出进程的函数, 在任何位置调用都可以 // 标准C库函数 #include <stdlib.h> void exit(int status); // Linux的系统函数 // 可以这么理解, 在linux中 exit() 函数 封装了 _exit() #include <unistd.h> void _exit(int status);
在 main 函数中直接使用 return
也可以退出进程,
假如是在一个普通函数中调用 return 只能返回到调用者的位置,而不能退出进程。
// ***** return 必须要在main()函数中调用, 才能退出进程 ***** // 举例: // 没有问题的例子 int main() { return 0; // 进程退出了 } ////////////////////////// 不能退出的例子 ////////////////////////// int func() { return 666; // 返回到调用者调用该函数的位置, 返回到 main() 函数的第19行 } int main() { // 调用这个函数, 当前进程不能退出 int ret = func(); }
2. 孤儿进程
在一个启动的进程中创建子进程,这时候父子进程同时运行,但是父进程由于某种原因先退出了,子进程还在运行,这时候这个子进程就可以被称之为孤儿进程。
操作系统是非常关爱运行的每一个进程的,当检测到某一个进程变成了孤儿进程,这时候系统中就会有一个固定的进程领养这个孤儿进程。 如果使用Linux没有桌面终端,这个领养孤儿进程的进程就是 init 进程(PID=1) 如果有桌面终端,这个领养孤儿进程就是桌面进程。
那么问题来了,系统为什么要领养这个孤儿进程呢?
在子进程退出的时候, 进程中的用户区可以自己释放, 但是进程内核区的pcb资源自己无法释放,必须要由父进程来释放子进程的pcb资源,孤儿进程被领养之后,这件事爹就可以代劳,避免系统资源的浪费。
下面这段代码就可以得到一个孤儿进程:
int main() { // 创建子进程 pid_t pid = fork(); // 父进程 if(pid > 0) { printf("我是父进程, pid=%d\n", getpid()); } else if(pid == 0) { sleep(1); // 强迫子进程睡眠1s, 这个期间, 父进程退出, 当前进程变成了孤儿进程 // 子进程 printf("我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid()); } return 0; }
# 程序输出的结果 $ ./a.out 我是父进程, pid=22459 我是子进程, pid=22460, 父进程ID: 1 # 父进程向退出, 子进程变成孤儿进程, 子进程被1号进程回收
3. 僵尸进程
在一个启动的进程中创建子进程,这时候就有了父子两个进程,父进程正常运行, 子进程先与父进程结束, 子进程无法释放自己的PCB资源, 需要父进程来做这个件事儿, 但是如果父进程也不管, 这时候子进程就变成了僵尸进程。
僵尸进程不能将它看成是一个正常的进程,这个进程已经死亡,用户区资源已经被释放,只是还占用一些内核资源(PCB)。
运行下面的代码就可以得到一个僵尸进程了:
int main() { pid_t pid; // 创建子进程 for(int i=0; i<5; ++i) { pid = fork(); if(pid == 0) { break; } } // 父进程 if(pid > 0) { // 需要保证父进程一直在运行 // 一直运行不退出, 并且也做回收, 就会出现僵尸进程 while(1) { printf("我是父进程, pid=%d\n", getpid()); sleep(1); } } else if(pid == 0) { // 子进程, 执行这句代码之后, 子进程退出了 printf("我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid()); } return 0; }
# ps aux 查看进程信息 # Z+ --> 这个进程是僵尸进程, defunct, 表示进程已经死亡 robin 22598 0.0 0.0 4352 624 pts/2 S+ 10:11 0:00 ./app robin 22599 0.0 0.0 0 0 pts/2 Z+ 10:11 0:00 [app] <defunct> # 子进程 robin 22600 0.0 0.0 0 0 pts/2 Z+ 10:11 0:00 [app] <defunct> # 子进程 robin 22601 0.0 0.0 0 0 pts/2 Z+ 10:11 0:00 [app] <defunct> # 子进程 robin 22602 0.0 0.0 0 0 pts/2 Z+ 10:11 0:00 [app] <defunct> # 子进程 robin 22603 0.0 0.0 0 0 pts/2 Z+ 10:11 0:00 [app] <defunct> # 子进程
消灭僵尸进程的方法是杀死这个僵尸进程的父进程,这样僵尸进程的资源就被系统回收了.
kill -9僵尸进程PID的方式是不能消灭僵尸进程的
,这个命令只对活着的进程有效,僵尸进程已经死了,鞭尸是不能解决问题的。
4. 进程回收
为了避免僵尸进程的产生,一般我们会在父进程中进行子进程的资源回收
回收方式有两种,一种是阻塞方式wait()
,一种是非阻塞方式waitpid()
。
4.1 wait()
这是个阻塞函数,如果没有子进程退出, 函数会一直阻塞等待 当检测到子进程退出了, 该函数阻塞解除回收子进程资源。 这个函数被调用一次, 只能回收一个子进程的资源,如有多个子进程需要资源回收, 函数需被调用多次。
// 函数原型 // man 2 wait #include <sys/wait.h> pid_t wait(int *status);
- 参数:传出参数,通过传递出的信息判断回收的进程是怎么退出的,如果不需要该信息可以指定为 NULL。取出整形变量中的数据需要使用一些宏函数,具体操作方式如下:
-
WIFEXITED(status)
: 返回1, 进程是正常退出的 -
WEXITSTATUS(status)
:得到进程退出时候的状态码,相当于 return 后边的数值, 或者 exit()函数的参数 -
WIFSIGNALED(status)
: 返回1, 进程是被信号杀死了 -
WTERMSIG(status)
: 获得进程是被哪个信号杀死的,会得到信号的编号
-
- 返回值:
- 成功:返回被回收的子进程的进程ID
- 失败: -1
- 没有子进程资源可以回收了, 函数的阻塞会自动解除, 返回-1
- 回收子进程资源的时候出现了异常
下面代码演示了如何通过 wait()回收多个子进程资源:
// wait 函数回收子进程资源 #include <sys/wait.h> int main() { pid_t pid; // 创建子进程 for(int i=0; i<5; ++i) { pid = fork(); if(pid == 0) { break; } } // 父进程 if(pid > 0) { // 需要保证父进程一直在运行 while(1) { // 回收子进程的资源 // 子进程由多个, 需要循环回收子进程资源 pid_t ret = wait(NULL); if(ret > 0) { printf("成功回收了子进程资源, 子进程PID: %d\n", ret); } else { printf("回收失败, 或者是已经没有子进程了...\n"); break; } printf("我是父进程, pid=%d\n", getpid()); } } else if(pid == 0) { // 子进程, 执行这句代码之后, 子进程退出了 printf("我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid()); } return 0; }
4.2 waitpid()
waitpid() 函数可以看做是 wait() 函数的升级版,通过该函数可以控制回收子进程资源的方式是阻塞还是非阻塞,另外还可以通过该函数进行精准打击,可以精确指定回收某个或者某一类或者是全部子进程资源。
// 函数原型 // man 2 waitpid #include <sys/wait.h> // 这个函数可以设置阻塞, 也可以设置为非阻塞 // 这个函数可以指定回收哪些子进程的资源 pid_t waitpid(pid_t pid, int *status, int options);
- 参数:
- pid:
- -1:回收所有的子进程资源, 和wait()是一样的, 无差别回收,并不是一次性就可以回收多个, 也是需要循环回收的
- 大于0:指定回收某一个进程的资源 ,pid是要回收的子进程的进程ID
- 0:回收当前进程组的所有子进程ID
- 小于 -1:pid 的绝对值代表进程组ID,表示要回收这个进程组的所有子进程资源
- status: NULL, 和wait的参数是一样的
- options: 控制函数是阻塞还是非阻塞
- 0: 函数行为是阻塞的 ==> 和wait一样
- WNOHANG: 函数行为是非阻塞的
- pid:
- 返回值
- 如果函数是非阻塞的, 并且子进程还在运行, 返回0
- 成功: 得到子进程的进程ID
- 失败: -1
- 没有子进程资源可以回收了, 函数如果是阻塞的, 阻塞会解除, 直接返回-1
- 回收子进程资源的时候出现了异常
下面代码演示了如何通过 waitpid()
阻塞回收多个子进程资源:
// 和wait() 行为一样, 阻塞 #include <sys/wait.h> int main() { pid_t pid; // 创建子进程 for(int i=0; i<5; ++i) { pid = fork(); if(pid == 0) { break; } } // 父进程 if(pid > 0) { // 需要保证父进程一直在运行 while(1) { // 回收子进程的资源 // 子进程由多个, 需要循环回收子进程资源 int status; pid_t ret = waitpid(-1, &status, 0); // == wait(NULL); if(ret > 0) { printf("成功回收了子进程资源, 子进程PID: %d\n", ret); // 判断进程是不是正常退出 if(WIFEXITED(status)) { printf("子进程退出时候的状态码: %d\n", WEXITSTATUS(status)); } if(WIFSIGNALED(status)) { printf("子进程是被这个信号杀死的: %d\n", WTERMSIG(status)); } } else { printf("回收失败, 或者是已经没有子进程了...\n"); break; } printf("我是父进程, pid=%d\n", getpid()); } } else if(pid == 0) { // 子进程, 执行这句代码之后, 子进程退出了 printf("===我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid()); } return 0; }
下面代码演示了如何通过 waitpid()
非阻塞回收多个子进程资源:
// 非阻塞处理 #include <sys/wait.h> int main() { pid_t pid; // 创建子进程 for(int i=0; i<5; ++i) { pid = fork(); if(pid == 0) { break; } } // 父进程 if(pid > 0) { // 需要保证父进程一直在运行 while(1) { // 回收子进程的资源 // 子进程由多个, 需要循环回收子进程资源 // 子进程退出了就回收, // 没退出就不回收, 返回0 int status; pid_t ret = waitpid(-1, &status, WNOHANG); // 非阻塞 if(ret > 0) { printf("成功回收了子进程资源, 子进程PID: %d\n", ret); // 判断进程是不是正常退出 if(WIFEXITED(status)) { printf("子进程退出时候的状态码: %d\n", WEXITSTATUS(status)); } if(WIFSIGNALED(status)) { printf("子进程是被这个信号杀死的: %d\n", WTERMSIG(status)); } } else if(ret == 0) { printf("子进程还没有退出, 不做任何处理...\n"); } else { printf("回收失败, 或者是已经没有子进程了...\n"); break; } printf("我是父进程, pid=%d\n", getpid()); } } else if(pid == 0) { // 子进程, 执行这句代码之后, 子进程退出了 printf("===我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid()); } return 0; }