参考资料:《Linux环境编程:从应用到内核》
守护进程是Linux中的后台服务进程,是一个生存周期较长的进程,通常独立于终端并且周期性的处理某些任务或者等待处理某些发生的事情。守护进程是一个特殊的孤儿进程,这种进程脱离终端,是为了避免被终端的任何信息所打断,其执行过程中也不会在任何终端显示信息。
创建守护进程的步骤:
1、屏蔽一些终端的信号:
守护进程在后台执行,并且不与任何控制终端相关联。即使守护进程是从终端命令行启动的,终端相关的信号如SIGINT、SIGQUIT和SIGTSTP,以及关闭终端,都不会影响到守护进程的继续执行。
2、执行fork()函数,父进程退出,子进程继续:
执行这一步,原因有二:
- 父进程有可能是进程组的组长(在命令行启动的情况下),从而不能够执行后面要执行的setsid函数,子进程继承了父进程的进程组ID,并且拥有自己的进程ID,一定不会是进程组的组长,所以子进程一定可以执行后面要执行的setsid函数。
- 如果守护进程是从终端命令行启动的,那么父进程退出会被shell检测到,shell会显示shell提示符,让子进程在后台执行。
3、修改进程的当前目录为根目录:
chdir("/");
因为守护一直在运行,如果当前工作路径上包含有根文件系统以外的其他文件系统,那么这些文件系统将无法卸载。因此,常规是将当前工作目录切换成根目录,当然也可以是其他目录,只要确保该目录所在的文件系统不会被卸载即可。
4、调用setsid函数:
这个函数的目的是切断与控制终端的所有关系,并且创建一个新的会话。这一步比较关键,因为这一步确保了子进程不再归属于控制终端所关联的会话。因此无论终端是否发送SIGINT、SIGQUIT或SIGTSTP信号,也无论终端是否断开,都与要创建的daemon进程无关,不会影响到daemon进程的继续执行。
5、设置文件模式创建掩码为0:
umask(0);
这一步的目的是让守护进程创建文件的权限属性与shell脱离关系。因为默认情况下,进程的umask来源于父进程shell的umask。如果不执行umask(0),那么父进程shell的umask就会影响到守护进程进程的umask。如果用户改变了shell的umask,那么也就相当于改变了守护进程的umask,就会造成守护进程每次执行的umask信息可能会不一致。
6、再次执行fork,父进程退出,子进程继续:
原因是,守护进程有可能会打开一个终端设备,即守护进程可能会根据需要,执行类似如下的代码:
int fd = open("/dev/console", O_RDWR);
这个打开的终端设备是否会成为daemon进程的控制终端,取决于两点:
- daemon进程是不是会话的首进程。
- 系统实现。(BSD风格的实现不会成为daemon进程的控制终端,但是POSIX标准说这由具体实现来决定)。
既然如此,为了确保万无一失,只有确保守护进程不是会话的首进程,才能保证打开的终端设备不会自动成为控制终端。因此,不得不执行第二次fork,fork之后,父进程退出,子进程继续。这时,子进程不再是会话的首进程,也不是进程组的首进程了。
7、关闭标准输入(stdin)、标准输出(stdout)和标准错误(stderr):
因为文件描述符0、1和2指向的就是控制终端。守护进程已经不再与任意控制终端相关联,因此这三者都没有意义。一般来讲,关闭了之后,会打开/dev/null,并执行dup2函数,将0、1和2重定向到/dev/null。这个重定向是有意义的,防止了后面的程序在文件描述符0、1和2上执行I/O库函数而导致报错。
完整代码
// 守护进程示例
#include <iostream>
#include <strings.h>
#include <string.h>
#include <ctime>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/stat.h>
// 文件绝对路径
#define OUTFILE "/usr/vscode/进程示例/test3/out.txt"
#define MODE S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH
int fd_out;
// 关闭守护进程
void signalHandler(int signum)
{
if (signum == SIGTERM)
{
std::cout << "Received SIGTERM signal. Exiting..." << std::endl;
close(fd_out);
exit(EXIT_SUCCESS);
}
}
// 创建守护进程
bool InitDaemon()
{
// 1、屏蔽一些控制终端操作的信号
signal(SIGTTOU, SIG_IGN);
signal(SIGINT, SIG_IGN);
signal(SIGQUIT, SIG_IGN);
signal(SIGTSTP, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
// 2、执行fork()函数,父进程退出,子进程继续
pid_t pid = fork();
if (pid < 0)
{
std::cout << "第一进程创建失败" << std::endl;
return false;
}
else if (pid > 0) // 父进程退出
exit(EXIT_SUCCESS);
// 3、修改进程的当前目录为根目录
if (chdir("/") == -1)
{
std::cout << "修改进程的当前目录为根目录失败" << std::endl;
return false;
}
// 4、调用setid()函数,切断与控制终端的所有关系,并重新创建一个新的会话
if (setsid() == -1)
{
std::cout << "调用setid()函数失败" << std::endl;
return false;
}
// 5、设置文件模式创建掩码为0
umask(0);
// 6、再次执行fork,父进程退出,子进程继续
if ((pid = fork()))
exit(0); // 结束第一子进程,第二子进程继续(第二子进程不再是会话组长)
else if (pid < 0)
{
std::cout << "第二进程创建失败" << std::endl;
return false;
}
// 7、关闭标准输入(stdin)、标准输出(stdout)、标准错误(stderr)
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// 设置信号处理函数
signal(SIGTERM, signalHandler);
return true;
}
int main()
{
if (!InitDaemon())
{
std::cout << "守护进程创建失败" << std::endl;
return -1;
}
// 打开文件
fd_out = open(OUTFILE, O_WRONLY | O_CREAT | O_TRUNC, MODE);
if (fd_out < 0)
{
std::cout << "文件打开失败" << std::endl;
return -1;
}
char timeBuffer[80];
memset(timeBuffer, 0, 80);
// 守护进程执行的任务
while (1)
{
// 获取当前时间
time_t current_time = time(nullptr);
tm* time_info = localtime(¤t_time);
strftime(timeBuffer, 80, "%Y-%m-%d %H:%M:%S\n", time_info);
write(fd_out, timeBuffer, strlen(timeBuffer));
memset(timeBuffer, 0, 80);
sleep(1);
}
return 0;
}
示例中,我们通过InitDaemon()函数创建守护进程,然后周期性地朝out.txt文件里写入当前时间用来说明守护进程在执行。
注意:out.txt路径是绝对路径,因为创建守护进程的步骤中有一条是“设置当前进程目录为根目录”,所以如果是相对路径,就找不到这个文件
编译运行
守护进程不停的写入当前时间
发送信号杀死守护进程
查看守护进程test的进程号,使用kill -SIGTERM命令杀死。