目录
结束进程
孤儿进程
僵尸进程
进程回收
wait()
waitpid
进程控制是指在操作系统中对进程进行创建、终止、挂起、唤醒以及进程之间的同步、通信等操作的管理。
结束进程
exit()
和_exit()
函数都用于终止一个进程,但它们之间有一些重要的区别:
exit()
函数:
- 头文件:
#include <stdlib.h>
- 功能:
exit()
函数用于正常终止程序的执行。它执行一系列清理操作,包括调用通过atexit()
注册的终止处理程序,并将程序的返回状态传递给操作系统。- 清理操作: 会调用通过
atexit()
注册的终止处理程序,关闭文件流(通过fclose()
),刷新缓冲区等。- 使用范例:
#include <stdlib.h> int main() { // 一些代码 // 正常退出,返回状态码 0 exit(0); }
_exit()
函数:- 头文件:
#include <unistd.h>
- 功能:
_exit()
函数也用于终止程序,但它是一个较低级别的函数。它不会执行exit()
做的清理工作,包括不会调用通过atexit()
注册的函数,不会刷新 I/O 缓冲区等。- 适用场景:
_exit()
主要用于在子进程中立即终止程序而无需执行清理操作。在这种情况下,使用_exit()
可以避免执行exit()
中的一些不必要的操作。- 使用范例:
#include <unistd.h> int main() { // 一些代码 // 立即退出,不执行清理操作 _exit(0); }
在大多数情况下,如果你需要正常终止程序并执行清理操作,应该使用
exit()
函数。如果你希望在子进程中立即退出,可以使用_exit()
。
孤儿进程
孤儿进程是指在其父进程结束或者被终止后,仍然在系统中运行的子进程。孤儿进程会被操作系统的 init 进程(进程号为1)接管,并由 init 进程负责回收。这确保了孤儿进程不会成为系统资源的泄漏。
当一个进程创建了子进程,而父进程先于子进程结束,那么子进程就会变成孤儿进程。这通常发生在父进程创建子进程后,父进程先于子进程调用
exit()
终止,或者父进程意外崩溃的情况。init 进程会定期检查是否有孤儿进程,如果发现孤儿进程,就会成为孤儿进程的新的父进程,并负责回收它的资源。这种处理方式确保了操作系统的稳定性和资源管理的有效性。
下面是一个产生孤儿进程的简单示例:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> int main() { pid_t child_pid = fork(); if (child_pid < 0) { // 错误处理 perror("fork"); exit(0); } if (child_pid > 0) { // 父进程 printf("我是父进程:pid=%d\n", getpid()); exit(0); } else if (child_pid == 0) { // 子进程 sleep(2); printf("我是子进程:pid=%d,父进程:ID=%d\n", getpid(), getppid()); } return 0; }
在 Linux 中,如果一个进程的父进程终止,而它还没有被领养,那么它会被 init 进程(PID=1)领养。这确保了孤儿进程总是有一个父进程。init 进程在系统启动时由内核启动,是所有进程的祖先。
对于没有桌面环境的系统,确实是 init 进程接管孤儿进程。但是,对于有桌面环境的系统,具体情况可能有所不同,因为桌面环境通常有自己的进程管理机制。
僵尸进程
僵尸进程是已经结束执行的进程,但其在进程表中仍然保留着一定的信息,包括进程号(PID)和退出状态等。这样的进程称为僵尸进程。僵尸进程的存在可能导致系统中的进程表被占用,过多的僵尸进程可能影响系统的正常运行。
通常,一个进程在结束时会向其父进程发送一个信号,告诉父进程它已经结束。父进程接收到这个信号后,应该调用
wait
或waitpid
系统调用来获取子进程的退出状态,释放子进程的资源。如果父进程没有及时处理,子进程就会变成僵尸进程。运行下面的代码就可以得到一个僵尸进程了:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> 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; }
这段代码创建了一个父进程和5个子进程。子进程在输出一行信息后立即退出,而父进程则进入一个无限循环,在每次循环中输出一行信息。
在这个场景中,由于父进程一直在运行,而子进程在输出信息后就退出了,子进程就有可能成为僵尸进程。父进程没有调用
wait
或waitpid
函数来回收子进程,因此子进程退出后,其退出状态信息会一直保留在进程表中,形成僵尸进程。
进程回收
为了避免僵尸进程的产生,一般我们会在父进程中进行子进程的资源回收,回收方式有两种,一种是阻塞方式wait(),一种是非阻塞方式waitpid()。
wait()
这是个阻塞函数,如果没有子进程退出, 函数会一直阻塞等待, 当检测到子进程退出了, 该函数阻塞解除回收子进程资源。这个函数被调用一次, 只能回收一个子进程的资源,如果有多个子进程需要资源回收, 函数需要被调用多次。
函数原型如下:
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status);
返回值:
- 成功:返回被回收的子进程的进程ID
- 失败: -1
- 没有子进程资源可以回收了, 函数的阻塞会自动解除, 返回-1
- 回收子进程资源的时候出现了异常
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> // wait 函数回收子进程资源 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; }
waitpid
waitpid() 函数可以看做是 wait() 函数的升级版,通过该函数可以控制回收子进程资源的方式是阻塞还是非阻塞,另外还可以通过该函数进行精准打击,可以精确指定回收某个或者某一类或者是全部子进程资源。
#include <sys/wait.h> pid_t waitpid(pid_t pid, int *status, int options);
pid
参数指定要等待的子进程:
- 如果
pid > 0
,则等待进程ID等于pid
的子进程。- 如果
pid == 0
,则等待与调用进程在同一进程组的任一子进程。- 如果
pid == -1
,则等待任一子进程,与wait
效果相同。- 如果
pid < -1
,则等待进程组ID等于pid
绝对值的任一子进程。
status
是一个指向整型的指针,用于保存子进程的退出状态。
options
是一组位掩码,可以通过按位或运算来组合。常用的选项有:
WNOHANG
:以非阻塞方式等待,即使没有子进程退出也立即返回。返回值:
- 如果函数是非阻塞的, 并且子进程还在运行, 返回0
- 成功: 得到子进程的进程ID
- 失败: -1
- 没有子进程资源可以回收了, 函数如果是阻塞的, 阻塞会解除, 直接返回-1
- 回收子进程资源的时候出现了异常
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> // 和wait() 行为一样, 阻塞 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; }