孤儿进程、僵尸进程、守护进程
孤儿进程、僵尸进程、守护进程
首先明确系统引导后启动的第一个进程是init进程,PID=1,之后的每个进程都是它的子进程。init进程在系统中是/sbin/init
,是/usr/lib/systemd/systemd
的符号链接。注意区别与 systemd --user
这个特定于当前用户的实例(由user@.service
启动),以下说的init进程其实都是systemd --user
。
孤儿进程
孤儿进程:父进程退出,而子进程还在运行,则子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1,主流发行版是systemd)所收养,并由init进程对它们完成状态收集工作。
每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init进程,而init进程会循环地wait()
它的已经退出的子进程。因此孤儿进程并不会有什么危害。
实例
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { pid_t pid; pid = fork(); if (pid < 0) { perror("fork error:"); exit(1); } // 子进程 if (pid == 0) { printf("I am the childprocess.\n"); // 输出进程ID和父进程ID printf("pid:%d\tppid:%d\n", getpid(), getppid()); printf("childprocess will sleep 5s.\n"); // 睡眠5s,保证父进程先退出 sleep(5); printf("pid:%d\tppid:%d\n", getpid(), getppid()); printf("childprocess is exited.\n"); } // 父进程 else { printf("I am fatherprocess.\n"); // 父进程睡眠1s,保证子进程输出进程id sleep(1); printf("fatherprocess is exited.\n"); } return 0; }
上图中的1686是systemd --user
僵尸进程
僵尸进程:子进程已经终止但其父进程尚未读取其终止状态的进程,导致该子进程表项仍然保留在系统中,称为僵尸进程。
由于子进程的结束和父进程的运行是异步的(除非父进程调用wait()
或waitpid()
)子进程退出,而父进程并没有调用wait()
或waitpid()
回收子进程资源,那么子进程的进程描述符 task_struct
(包括PID、退出状态,运行时间等)仍然保存在系统中,该子进程成为僵尸进程。
在学习操作系统中进程的终止时提到OS不会立刻将进程的全部信息清除,而是先保留一会等待其他进程收集。
如果进程不调用
wait()
或waitpid()
的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。
解决僵尸进程:僵尸进程已经“死了”,不能再死一次(因此无法使用kill -9
杀死僵尸进程),其产生危害的原因是父进程没有为其善后处理。因此解决方案是kill掉父进程,从而让僵尸进程成为孤儿进程过继给init进程托管,init进程始终会负责清理僵尸进程。
注意:每个子进程在结束(调用exit()
)后,会留下一个僵尸进程(Zombie)的数据结构,并由操作系统向父进程发送一个 SIGCHLD
信号,等待父进程处理,父进程处理完这个数据结构后,子进程才会真正消失。因此父进程调用 wait()/waitpid()
可以放在 SIGCHLD
信号处理函数中。
进程的退出方式有两种:调用
exit(返回状态码)
函数或者return
返回状态码。而OS不会主动将返回状态码传递给父进程,需要父进程自己调用wait()/waitpid()
来接收。如果不接收的话OS就会一直保存着这个状态码和其他信息,导致僵尸进程一直存在。如果子进程在
exit()
之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。如果父进程在子进程结束之前退出,则子进程将由init进程接管。init进程将会以父进程的身份对僵尸状态的子进程进行处理。
实例
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> static void sig_child(int signo) { pid_t pid; int wstatus; // 处理僵尸进程 while ((pid = waitpid(-1, &wstatus, WNOHANG)) > 0) { // WNOHANG使得waitpid不会阻塞 // 检查子进程的退出状态 if (WIFEXITED(wstatus)) { printf("childprocess %d exited with status %d\n", pid, WEXITSTATUS(status)); } else { printf("childprocess %d terminated by signal %d\n", pid, WTERMSIG(wstatus)); } } } int main() { signal(SIGCHLD, sig_child); pid_t pid; pid = fork(); if (pid < 0) { perror("fork error:"); exit(1); } else if (pid == 0) { // 子进程立即退出 printf("I am childprocess %d. I am exiting.\n", getpid()); exit(0); } printf("I am fatherprocess. I will sleep 2s\n"); // 等待子进程先退出 sleep(2); // 输出进程信息 system("ps -opid,ppid,state,tty,command"); printf("fatherprocess is exiting.\n"); return 0; }
父进程调用 waitpid()
来回收子进程资源:
守护进程
守护进程:在后台运行,不与任何shell关联的进程,通常情况下守护进程在系统启动时就在运行,它们以root用户或者其他特殊用户(如apache、lightdm)运行,并能处理一些系统级、周期性的任务。习惯上守护进程的名字通常以d结尾(sshd),但这些不是必须的。
守护进程脱离于终端,是为了避免进程在执行过程中的信息在任何终端上显示,并且进程也不会被任何终端所产生的终端信息所打断。
每一个shell开始运行的进程都会依附于这个终端,这个shell就称为这些进程的控制shell,当控制shell被关闭时,相应的进程都会自动关闭。但是守护进程却能够突破这种限制,它从被执行开始运转,直到整个系统关闭时才退出。如果想让某个进程不因为用户或终端或其他的变化而受到影响,那么就必须把这个进程变成一个守护进程。
实例
创建守护进程的步骤:
-
调用
fork()
,创建新进程,它会是将来的守护进程. -
在父进程中调用
exit()
(此时子进程的父进程变成了init进程) -
调用
setsid()
创建新的session并称为session leader & group leader(避免受到原来父进程所在session的影响) -
关闭不再需要的文件描述符。子进程从父进程继承打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。
-
调用
umask()
重设文件创建权限掩码 -
将当前目录(daemon的工作目录)改成
/
(如果把当前目录作为守护进程的目录,当前目录将不能被卸载) -
将标准输入、标注输出、标准错误重定向到
/dev/null
#include <fcntl.h> #include <stdlib.h> #include <sys/stat.h> #include <unistd.h> void do_stuff() { while (1) { // 在这里添加守护进程的任务 } } int main(void) { pid_t pid; pid = fork(); // 创建一个新进程,将来会是守护进程 if (pid == -1) { exit(EXIT_SUCCESS); } else if (pid != 0) { // 父进程调用exit,保证该daemon不是进程组长 exit(EXIT_SUCCESS); } if (setsid() == -1) // 创建新的会话区 exit(EXIT_SUCCESS); if (chdir("/") == -1) // 将工作目录改成根目录 exit(EXIT_SUCCESS); for (int x = sysconf(_SC_OPEN_MAX); x >= 0; x--) {// 关闭不必要的文件描述符 close(x); } // 重设文件创建权限掩码 umask(0); // 重定向标准输入、输出和错误到 /dev/null open("/dev/null", O_RDWR); // 使用 dup(0) 复制文件描述符 0(标准输入),并将其分配给下一个可用的文件描述符,即 1(标准输出) dup(0); // 再次使用 dup(0) 复制文件描述符 0(标准输入),并将其分配给下一个可用的文件描述符,即 2(标准错误)。 dup(0); // 标准错误 (2) do_stuff(); return 0; }
启动后通过 ps
命令可以查看到该进程。
总结
僵尸进程无人收尸将会导致资源浪费,而孤儿进程有init进程托管则不会造成资源浪费。
进程退出:退出的方式主要分为两种:正常退出、异常退出
正常退出又分为5种:
- main函数调用return
- 进程调用exit(),标准c库
- 进程调用_exit()或者_Exit(),属于系统调用
/*它要检查文件的打开情况,把文件缓冲区的内容写回文件,即“清理I/O缓冲”。*/ exit();//exit(0)正常退出;exit(1)异常退出 /*直接退出*/ _exit();//_exit(0)正常退出;_exit(1)异常退出 _Exit();//_Exit(0)正常退出;_Exit(1)异常退出
与线程有关:
1.进程最后一个线程返回
2.最后一个线程调用pthread_exit
异常退出分为3种:
1.调用abort
2.当进程收到某些信号时,如ctrl+C
3.最后一个线程对取消(cancellation)请求做出响应
不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。对上述任意一种终止情形、我们都希望终止进程能够通知其父进程它是如何终止的。对于三个终止函数(exit._exit和_Exit),实现这一点的方法是,将其退出状态((exit status)作为参数传送给函数。在异常终止情况下,内核(不是进程本身)产生一个指示其异常终止原因的终止状态(termination status)。在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数(在下一节说明)取得其终止状态。
本文作者:3的4次方
本文链接:https://www.cnblogs.com/3to4/p/17361068.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步