Linux Daemon Writing HOWTO
这里视频讲的很清楚
牛客网-c/C++Linux课程-守护进程
int main() {
// 1.创建子进程,退出父进程
pid_t pid = fork();
if(pid > 0) {
exit(0);
}
// 2.将子进程重新创建一个会话
setsid();
// 3.设置掩码
umask(022);
// 4.更改工作目录
chdir("/home/nowcoder/");
// 5. 关闭、重定向文件描述符
int fd = open("/dev/null", O_RDWR);
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
// 6.业务逻辑
// 捕捉定时信号
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = work;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, NULL);
struct itimerval val;
val.it_value.tv_sec = 2;
val.it_value.tv_usec = 0;
val.it_interval.tv_sec = 2;
val.it_interval.tv_usec = 0;
// 创建定时器
setitimer(ITIMER_REAL, &val, NULL);
// 不让进程结束
while(1) {
sleep(10);
}
return 0;
}
1
什么是守护进程?
守护进程是后台运行,自动运行,少交互或不交互。
守护进程(或服务)是一个后台进程,设计为自动运行,几乎不需要用户干预。ApacheWeb服务器http守护程序(httpd)就是这样一个守护程序示例。它在后台监听特定端口,并根据请求类型提供页面或处理脚本。
在Linux中创建守护进程会按照给定的顺序使用一组特定的规则。了解它们是如何工作的将有助于您了解守护进程在userland Linux中是如何运行的,但也可以通过对内核的调用来运行。事实上,一些守护进程与与与硬件设备(如外部控制器板、打印机和PDA)一起工作的内核模块接口。它们是Linux的基本构建块之一,为Linux提供了难以置信的灵活性和强大功能。
在本教程中,将用C语言构建一个非常简单的守护进程。随着我们的深入,将添加更多代码,显示启动和运行守护进程所需的正确执行顺序。
2.开始
首先,您需要在Linux机器上安装以下软件包来开发守护进程,具体来说:
GCC 3.2.2或更高版本
Linux开发头和库
如果您的系统尚未安装这些组件(不太可能,但还是要检查),则需要它们来开发本指南中的示例。要了解您安装的GCC版本,请使用:
gcc——版本
3.规划你的守护进程
3.1它将做什么?
守护进程应该做一件事,并且做得很好。这一点可能与管理多个域上的数百个邮箱一样复杂,也可能与编写报告并调用sendmail将其发送给管理员一样简单。
在任何情况下,你都应该有一个很好的计划来指导守护进程应该做什么。如果它将与你可能或可能不写的其他守护进程进行交互操作,那么这也是另一个需要考虑的问题。
3.2有多少互动?
守护进程永远不应该通过终端与用户直接通信。事实上,守护进程根本不应该与用户直接通信。所有通信都应该通过某种类型的接口(您可能需要也可能不需要编写),这种接口可以像GTK+GUI一样复杂,也可以像信号集一样简单。
4.基本守护程序结构
当守护进程启动时,它必须做一些低级的家务来为自己真正的工作做好准备。这包括几个步骤:
Fork off the parent process
Change file mode mask (umask)
Open any logs for writing
Create a unique Session ID (SID)
Change the current working directory to a safe place
Close standard file descriptors
Enter actual daemon code
放弃父进程
更改文件模式掩码(umask)
打开所有日志进行写入
创建唯一的会话ID(SID)
将当前工作目录更改为安全位置
关闭标准文件描述符
输入实际的守护程序代码
4.1分叉父进程
守护进程由系统本身或终端或脚本中的用户启动。当它启动时,这个过程就像系统上的任何其他可执行文件一样。为了使其真正自治,必须在执行实际代码的地方创建子进程。这就是所谓的forking,它使用fork()函数:
pid_t pid;
/* Fork off the parent process */
pid = fork();
if (pid < 0) {
exit(EXIT_FAILURE);
}
/* If we got a good PID, then
we can exit the parent process. */
if (pid > 0) {
exit(EXIT_SUCCESS);//
//避免儿子成为僵尸进程 父进程先退出 子进程就是后台进程了
}
注意调用fork()之后的错误检查。在编写守护程序时,必须尽可能地编写防御代码。事实上,守护进程中有很大一部分代码只包含错误检查。
函数的作用是:返回子进程的进程id(PID)(不等于零),或者在失败时返回-1。如果进程不能派生子进程,那么守护进程应该在这里终止。
如果从fork()返回的PID成功,则父进程必须正常退出。PID >0 返回子进程的id 此时需要exit退出父进程,也避免儿进程成为僵尸进程。子进程就是后台进程了。对于没有见过它的人来说,这可能看起来很奇怪,但通过分叉,子进程将继续从代码中执行。
4.2更改文件模式掩码(Umask)
为了写入守护进程创建的任何文件(包括日志),必须更改文件模式掩码(umask),以确保它们可以正确写入或读取。这类似于从命令行运行umask,但我们在这里是通过编程实现的。我们可以使用umask()函数来实现这一点:
pid_t pid, sid;
/* Fork off the parent process */
pid = fork();
if (pid < 0) {
/* Log failure (use syslog if possible) */
exit(EXIT_FAILURE);
}
/* If we got a good PID, then
we can exit the parent process. */
if (pid > 0) {
exit(EXIT_SUCCESS);
}
/* Change the file mode mask */
umask(0);
通过将umask设置为0,我们可以完全访问守护进程生成的文件。即使您不打算使用任何文件,也最好在这里设置umask,以防您访问文件系统上的文件。
4.3打开日志进行书写
这部分是可选的,但建议您在系统中的某个位置打开日志文件进行写入。这可能是您可以查找有关守护进程的调试信息的唯一地方。
4.4创建唯一会话ID(SID)
从这里开始,子进程必须从内核获得唯一的SID才能运行。否则,子进程将成为系统中的孤立进程。上一节中声明的pid_t类型也用于为子进程创建新的SID:
pid_t pid, sid;
/* Fork off the parent process */
pid = fork();
if (pid < 0) {
exit(EXIT_FAILURE);
}
/* If we got a good PID, then
we can exit the parent process. */
if (pid > 0) {
exit(EXIT_SUCCESS);
}
/* Change the file mode mask */
umask(0);
/* Open any logs here */
/* Create a new SID for the child process */
sid = setsid();
if (sid < 0) {
/* Log any failure */
exit(EXIT_FAILURE);
}
同样,setsid()函数的返回类型与fork()相同。我们可以在这里应用相同的错误检查例程,以查看函数是否为子进程创建了SID。
4.5更改工作目录
当前的工作目录应该更改为保证始终存在的位置。由于许多Linux发行版并不完全遵循Linux文件系统层次结构标准,因此唯一保证存在的目录是根目录(/)。我们可以使用chdir()函数来实现这一点:
pid_t pid, sid;
/* Fork off the parent process */
pid = fork();
if (pid < 0) {
exit(EXIT_FAILURE);
}
/* If we got a good PID, then
we can exit the parent process. */
if (pid > 0) {
exit(EXIT_SUCCESS);
}
/* Change the file mode mask */
umask(0);
/* Open any logs here */
/* Create a new SID for the child process */
sid = setsid();
if (sid < 0) {
/* Log any failure here */
exit(EXIT_FAILURE);
}
/* Change the current working directory */
if ((chdir("/")) < 0) {
/* Log any failure here */
exit(EXIT_FAILURE);
}
再一次,你可以看到防御性编码正在发生。chdir()函数在失败时返回-1,因此在更改到守护进程中的根目录后,一定要检查是否返回了-1。
4.6关闭标准文件描述符
设置守护进程的最后一步是关闭标准文件描述符(STDIN、STDOUT、STDERR)。由于守护进程无法使用终端,这些文件描述符是冗余的,存在潜在的安全隐患。
close()函数可以为我们处理这个问题:
pid_t pid, sid;
/* Fork off the parent process */
pid = fork();
if (pid < 0) {
exit(EXIT_FAILURE);
}
/* If we got a good PID, then
we can exit the parent process. */
if (pid > 0) {
exit(EXIT_SUCCESS);
}
/* Change the file mode mask */
umask(0);
/* Open any logs here */
/* Create a new SID for the child process */
sid = setsid();
if (sid < 0) {
/* Log any failure here */
exit(EXIT_FAILURE);
}
/* Change the current working directory */
if ((chdir("/")) < 0) {
/* Log any failure here */
exit(EXIT_FAILURE);
}
/* Close out the standard file descriptors */
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
坚持使用为文件描述符定义的常量是一个好主意,以实现系统版本之间的最大可移植性。
5.编写守护程序代码
5.1初始化
现在,您基本上已经告诉Linux您是一个守护程序,所以现在是时候编写实际的守护程序代码了。初始化是这里的第一步。因为这里可以调用许多不同的函数来设置守护进程的任务,所以我不会在这里深入讨论。
这里最重要的一点是,在初始化守护进程中的任何内容时,这里也适用相同的防御性编码准则。在写入系统日志或您自己的日志时,请尽可能详细。如果没有足够的关于守护进程状态的信息,调试守护进程可能会非常困难。
5.2大循环
守护进程的主代码通常位于无限循环中。从技术上讲,它不是一个无限循环,但它的结构是一个循环:
pid_t pid, sid;
/* Fork off the parent process */
pid = fork();
if (pid < 0) {
exit(EXIT_FAILURE);
}
/* If we got a good PID, then
we can exit the parent process. */
if (pid > 0) {
exit(EXIT_SUCCESS);
}
/* Change the file mode mask */
umask(0);
/* Open any logs here */
/* Create a new SID for the child process */
sid = setsid();
if (sid < 0) {
/* Log any failures here */
exit(EXIT_FAILURE);
}
/* Change the current working directory */
if ((chdir("/")) < 0) {
/* Log any failures here */
exit(EXIT_FAILURE);
}
/* Close out the standard file descriptors */
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
/* Daemon-specific initialization goes here */
/* The Big Loop */
while (1) {
/* Do some task here ... */
sleep(30); /* wait 30 seconds */
}
这种典型的循环通常是一个while循环,它有一个无限终止条件,在那里调用sleep使其以指定的间隔运行。
把它想象成心跳:当你的心跳时,它会执行一些任务,然后等待下一次心跳的发生。许多守护进程都遵循相同的方法。
6.把所有这些放在一起
6.1完整样品
下面列出的是一个完整的示例守护程序,它显示了设置和执行所需的所有步骤。要运行它,只需使用gcc编译,并从命令行开始执行。要终止,请在找到PID后使用kill命令。
我还加入了正确的include语句,用于与syslog进行接口,除了使用自己的日志进行fopen()/fwrite()/fclose()函数调用之外,建议至少发送start/stop/pause/die log语句。
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(void) {
/* Our process ID and Session ID */
pid_t pid, sid;
/* Fork off the parent process */
pid = fork();
if (pid < 0) {
exit(EXIT_FAILURE);
}
/* If we got a good PID, then
we can exit the parent process. */
if (pid > 0) {
exit(EXIT_SUCCESS);
}
/* Change the file mode mask */
umask(0);
/* Open any logs here */
/* Create a new SID for the child process */
sid = setsid();
if (sid < 0) {
/* Log the failure */
exit(EXIT_FAILURE);
}
/* Change the current working directory */
if ((chdir("/")) < 0) {
/* Log the failure */
exit(EXIT_FAILURE);
}
/* Close out the standard file descriptors */
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
/* Daemon-specific initialization goes here */
/* The Big Loop */
while (1) {
/* Do some task here ... */
sleep(30); /* wait 30 seconds */
}
exit(EXIT_SUCCESS);
}
从这里,您可以使用这个框架来编写自己的守护进程。确保添加自己的日志(或使用syslog工具),并进行防御性编码、防御性编码、防御性编码!