文章目录
- 普通进程和守护进程的区别
- 守护进程的编写
- setsid函数的作用
普通进程和守护进程的区别
普通进程:
- 进程有对应的终端,如果终端退出,那么对应的进程也就消失了:它的父进程是一个bash。
- 普通进程在前台时,终端被占住了,输入各种命令这个终端都没有反应。
- 当终端退出时,会向同一个会话里的所有进程发送SIGHUP信号,如果普通进程忽略这个信号,就不会退出,而是在终端退出后挂载到1号进程上。
守护进程:
- 守护进程的生存周期长,一般是操作系统启动时它就启动,操作系统关闭它才关闭。
- 大多数守护进程都是以root身份运行。
- 守护进程跟终端无关联,也就是说它们没有控制终端,因此终端退出不会导致守护进程退出。内核守护进程以无控制终端的方式启动,普通守护进程可能是守护进程调用了setsid的结果。
- 守护进程在后台运行,不会占用终端。
使用ps命令查看进程:
根据内容,我们可以把这些守护进程分为以下几种:
- PPID=0:内核进程,跟随系统启动而启动,其生命周期贯穿整个操作系统。
- CMD列名字中带有[ ],是内核守护进程。
- 1号进程:也是系统守护进程,它负责启动运行层次特定的系统服务,所以很多进程的PPID都是1。1号进程也负责收养孤儿进程。
- CMD列中不带[ ]的进程是普通的守护进程(用户级守护进程)。
守护进程的编写
具体的实现流程为:
- 调用fork或vfork生成一个进程,然后父进程退出。 这一步的作用是让子进程调用setsid能够脱离原来的进程组,让其不受终端控制。
- 子进程调用setsid,使子进程成为一个新的会话组长和进程组组长。相当于与原来的会话脱离,这样当终端退出时,就不会给子进程发SIGHUP信号。这一步很重要。
- 禁止进程重新打开控制终端(这一步不是必须的)。通过使进程不再是会话组长来实现,再一次通过fork创建新的子进程,使调用fork的进程退出 。
- 将当前进程工作目录更改为根目录(因为根目录下的文件不易被删除)。
- 关闭不再需要的文件描述符。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
//创建守护进程
int create_daemon()
{
int fd = 0;
switch (fork())
{
case -1:
//创建子进程失败,这里可以写入错误日志或者打印错误信息
return -1;
case 0:
//子进程,break,执行下面的动作
break;
default:
//父进程,直接退出
exit(0);
}
//只有子进程才会走到这里
//调用setsid脱离当前终端
if (setsid() == -1)
{
// setsid调用错误写入错误日志
return -1;
}
//chdir("/");
//关闭所有文件描述符(除了标准输出), getdtablesize函数获取进程可打开最大文件数
//也可以将标准输出进行重定向到文件中,这样就不会显示到屏幕上了
close(0);
for (int i = 2; i < getdtablesize(); i++)
{
close(i);
}
return 1;
}
int main()
{
if (create_daemon() != 1)
{
//创建守护进程失败
return 1;
}
else
{
//到这里,就是守护进程需要执行的动作,可以使用execl进行程序替换
while (1)
{
sleep(1);
//由于重定向了标准输入和输出,所以这一句不会打印到显示器
printf("休息1秒,进程id=%d\n", getpid());
}
}
return 0;
}
可以看到守护进程和它的bash不在同一个进程组中,因此bash退出并不会影响守护进程。
setsid函数的作用
当bash退出的时候,它会向会话中所有的进程发送SIGHUP信号,让它们都退出。
setsid()调用成功后,返回新的会话ID,调用setsid函数的进程成为新的会话的领头进程,并与其父进程的会话组和进程组脱离。
以下面的代码为例进行测试:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
void sig_usr(int signo)
{
printf("收到了SIGUSR1信号,进程id=%d\n", getpid());
}
int main()
{
pid_t pid;
printf("进程开始执行\n");
if (signal(SIGUSR1, sig_usr) == SIG_ERR)
{
printf("无法捕捉SIGUSR1信号\n");
exit(1);
}
//创建子进程
pid = fork();
if (pid < 0)
{
printf("子进程创建失败,程序退出\n");
exit(1);
}
while (1)
{
sleep(1);
printf("休眠一秒,进程id=%d\n", getpid());
}
printf("程序结束\n");
return 0;
}
我们用strcae来跟踪bash、父进程、子进程:
接着,我们手动关闭bash:
接下来,我们对子进程进行一些修改,让它调用setsid:
启动程序,再次查看,发现子进程的SID字段和PGRP字段都和父进程不相同:
我们再用同样的方式跟踪一下,可以看到子进程没有退出,并且被1号进程收养:
那你肯定会有这样的疑问,为什么需要子进程来调用setsid呢?父进程直接调用不行吗?
我们再将代码改一下,这一次父进程不创建子进程了,直接让父进程执行setsid:
可以看到执行失败,这主要的原因是:
- 调用setsid()的进程将成为一个新的进程组的组长,脱离原来的进程组。这从上面的实验也能看到。
- 父进程是其所在进程组的组长,如果允许一个进程组长调用setsid()的话,那父进程就会成为两个组的组长。很明显这是不合理的,因为 进程组组长的进程ID=进程组ID,那这样的话两个进程组的ID就要相同,或者进程组长有两个不同的进程ID。
另外再补充一点,既然bash退出的时候会向所有会话组的进程发送SIGHUP信号,那只需要捕捉这个信号,让其不执行原来的动作,就不会退出了。
nohup这个命令就能够让程序在运行时忽略SIGHUP信号,比如想让test程序在后台执行,将输出信息输出到log文件里,并且bash退出时test程序不退出,就可以这样写:
nohup ./test > log.txt 2>&1 &