用我们的决心、信心和毅力来培植我们的生命之|

3的4次方

园龄:2年1个月粉丝:5关注:89

📂Linux
🔖linux
2023-04-28 10:04阅读: 9评论: 0推荐: 0

孤儿进程、僵尸进程、守护进程

孤儿进程、僵尸进程、守护进程

首先明确系统引导后启动的第一个进程是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;
}

image

上图中的1686是systemd --user

僵尸进程

僵尸进程:子进程已经终止但其父进程尚未读取其终止状态的进程,导致该子进程表项仍然保留在系统中,称为僵尸进程。

由于子进程的结束和父进程的运行是异步的(除非父进程调用wait()waitpid())子进程退出,而父进程并没有调用wait()waitpid()回收子进程资源,那么子进程的进程描述符 task_struct (包括PID、退出状态,运行时间等)仍然保存在系统中,该子进程成为僵尸进程。

img

在学习操作系统中进程的终止时提到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;
}

image

父进程调用 waitpid() 来回收子进程资源:

image

守护进程

守护进程:在后台运行,不与任何shell关联的进程,通常情况下守护进程在系统启动时就在运行,它们以root用户或者其他特殊用户(如apache、lightdm)运行,并能处理一些系统级、周期性的任务。习惯上守护进程的名字通常以d结尾(sshd),但这些不是必须的。

守护进程脱离于终端,是为了避免进程在执行过程中的信息在任何终端上显示,并且进程也不会被任何终端所产生的终端信息所打断。

每一个shell开始运行的进程都会依附于这个终端,这个shell就称为这些进程的控制shell,当控制shell被关闭时,相应的进程都会自动关闭。但是守护进程却能够突破这种限制,它从被执行开始运转,直到整个系统关闭时才退出。如果想让某个进程不因为用户或终端或其他的变化而受到影响,那么就必须把这个进程变成一个守护进程。

实例

创建守护进程的步骤

  1. 调用 fork(),创建新进程,它会是将来的守护进程.

  2. 在父进程中调用 exit() (此时子进程的父进程变成了init进程)

  3. 调用 setsid() 创建新的session并称为session leader & group leader(避免受到原来父进程所在session的影响)

  4. 关闭不再需要的文件描述符。子进程从父进程继承打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。

  5. 调用 umask() 重设文件创建权限掩码

  6. 将当前目录(daemon的工作目录)改成 / (如果把当前目录作为守护进程的目录,当前目录将不能被卸载)

  7. 将标准输入、标注输出、标准错误重定向到 /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种:

  1. main函数调用return
  2. 进程调用exit(),标准c库
  3. 进程调用_exit()或者_Exit(),属于系统调用
/*它要检查文件的打开情况,把文件缓冲区的内容写回文件,即“清理I/O缓冲”。*/
exit();//exit(0)正常退出;exit(1)异常退出
/*直接退出*/
_exit();//_exit(0)正常退出;_exit(1)异常退出
_Exit();//_Exit(0)正常退出;_Exit(1)异常退出

image

与线程有关:
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 中国大陆许可协议进行许可。

posted @   3的4次方  阅读(9)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起