孤儿进程、僵尸进程、守护进程
孤儿进程、僵尸进程、守护进程
首先明确系统引导后启动的第一个进程是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函数(在下一节说明)取得其终止状态。