创建 SysV 风格的 linux daemon 程序

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介:

创建 SysV 风格的 linux daemon 程序

本文介绍如何使用 C 语言创建 Linux 系统中 SysV 风格的 daemon 程序。注意:这是一种旧式的 daemon 程序写法,进入 systemd 时代后是不需要通过这样的方式创建 daemon 程序的。 本文的演示环境为 ubuntu 18.04。

创建 daemon 程序的流程
通过前文《Linux session(会话)》我们了解到,如果要让程序运行在后台,必须处理好进程的 session。所以在创建 daemon 程序的过程中处理 session 问题是很重要的一步,当然除此之外还需要其它的步骤。下面是在 Linux 系统中创建一个 SysV 风格的 daemon 的基本流程:

从父进程 fork 出一个子进程
为子进程创建新的 session ID
在子进程中再 fork 一次
修改 umask
修改进程的当前工作目录
关闭进程中的文件描述符
接下来我们通过代码来介绍这些操作的含义。

创建 daemon 程序
从父进程 fork 出一个子进程
创建一个子进程,如果成功就让父进程退出,此时的子进程已经成为了 init 进程的子进程:

pid_t pid;

pid = fork();
if (pid < 0)

exit(EXIT_FAILURE);

if (pid > 0)

exit(EXIT_SUCCESS);

为子进程创建新的 session ID
运行在后台的进程需要摆脱 session 终端的束缚,通过 setsid() 函数为进程设置新的 session ID 可以做到这一点:

pid_t pid;

pid = fork();
if (pid < 0)

exit(EXIT_FAILURE);

if (pid > 0)

exit(EXIT_SUCCESS);

if (setsid() < 0)

exit(EXIT_FAILURE);

执行到这里时,PID==PGID==SID


在子进程中再 fork 一次
这次 fork 的目的是防止进程再次获得终端。因为只有 session leader 才能获得终端,而这次 fork 使子进程变成了非 session leader:

pid_t pid;

pid = fork();
if (pid < 0)

exit(EXIT_FAILURE);

if (pid > 0)

exit(EXIT_SUCCESS);

if (setsid() < 0)

exit(EXIT_FAILURE);

/ 第二次 fork /
pid = fork();
if (pid < 0)

exit(EXIT_FAILURE);

if (pid > 0)

exit(EXIT_SUCCESS);

执行到这里时,PGID==SID 但是已经不等于 PID 了,说明进程已经不是 session leader


修改 umask
为了能够向 daemon 进程创建的任何文件中写入内容(包括日志),必须重置 umask(file mode mask, umask),以确保能够正确地写入或读取这些文件:

umask(0);
修改进程的当前工作目录
必须保证进程的当前工作目录是存在的。因为众多的 Linux 发行版中很多都没有完全遵守标准的文件目录结构,所以最好是把进程的当前工作目录设置为 /,这样可以避免因设置了某个目录而导致它无法被 unmount:

chdir("/");
关闭进程中的文件描述符
关闭进程中所有打开的文件描述符:

int x;
for (x = sysconf(_SC_OPEN_MAX); x>=0; x--)
{

close (x);

}
把日志写入 syslog
Daemon 程序的日志非常重要,我们可以通过 openlog、syslog 和 closelog 三个函数把日志内容写入到 syslog  中:

openlog ("daemondemo", LOG_PID, LOG_DAEMON);
syslog (LOG_NOTICE, "Daemon demo is running, number: %d", count);
closelog();
本文 demo 输出的日志如下所示:

完整的代码

include

include

include

include

include

include

include

static void demo_daemon()
{

pid_t pid;

/* Fork off the parent process */
pid = fork();

/* An error occurred */
if (pid < 0)
    exit(EXIT_FAILURE);

/* Success: Let the parent terminate */
if (pid > 0)
    exit(EXIT_SUCCESS);

/* On success: The child process becomes session leader */
if (setsid() < 0)
    exit(EXIT_FAILURE);

/* Catch, ignore and handle signals */
//TODO: Implement a working signal handler */
//signal(SIGCHLD, SIG_IGN);
//signal(SIGHUP, SIG_IGN);

/* Fork off for the second time*/
pid = fork();

/* An error occurred */
if (pid < 0)
    exit(EXIT_FAILURE);

/* Success: Let the parent terminate */
if (pid > 0)
    exit(EXIT_SUCCESS);

/* Set new file permissions */
umask(0);

/* Change the working directory to the root directory */
/* or another appropriated directory */
chdir("/");

/* Close all open file descriptors */
int x;
for (x = sysconf(_SC_OPEN_MAX); x>=0; x--)
{
    close (x);
}

/* Open the log file */
openlog ("daemondemo", LOG_PID, LOG_DAEMON);

}

int main()
{

int count = 0;
demo_daemon();

while (1)
{
    //TODO: Insert daemon code here.
    count ++;
    syslog (LOG_NOTICE, "Daemon demo is running, number: %d", count);
    sleep (5);
    if(count > 5)
    {
        break;
    }
}

syslog (LOG_NOTICE, "Daemon demo terminated.");
closelog();

return EXIT_SUCCESS;

}

把上面的代码保存到文件 daemondemo.c 中(也可以从这里下代码),然后执行下面的命令进行编译就可以得到可执行文件 daemondemo:

$ gcc -Wall daemondemo.c -o daemondemo
关于 fork 两次
这是一个很有意思的话题,有人说需要 fork 两次,有人说第二次是可选的,究竟该如何做呢?当我们理解了第二次 fork 的用途后就可以自行决定是否需要第二次 fork 了。
这还需要从 session 的控制终端说起。控制终端是进程的一个属性,通过 fork 系统调用创建的子进程会从父进程那里继承控制终端。这样,session 中的所有进程都从 session 领头进程那里继承控制终端。前面已经说过了,要把程序变成 daemon,就得让进程摆脱 session 的终端。而这些在第一次 fork 后调用 setsid() 函数就搞定了。那么如果接下来不小心再给进程添加了终端该怎么办?答案是不让你添加!这就是第二次 fork 的作用。只有 session leader 才能获得终端,而第二次 fork 使子进程变成了非 session leader,你想犯错也不给你机会了。

像 nginx 和 gblic 的 daemon 函数的实现都是 fork 一次,所以说第二次 fork 是可选的,你可以根据自己的实际情况来决定。

参考:
Linux Daemon Writing HOWTO
Creating a daemon in Linux
daemon man page
daemon 函数
Unix Daemon Server Programming
glibc daemon.c

作者:sparkdev
出处:http://www.cnblogs.com/sparkdev/

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
2月前
|
安全 Linux Shell
Linux上执行内存中的脚本和程序
【9月更文挑战第3天】在 Linux 系统中,可以通过多种方式执行内存中的脚本和程序:一是使用 `eval` 命令直接执行内存中的脚本内容;二是利用管道将脚本内容传递给 `bash` 解释器执行;三是将编译好的程序复制到 `/dev/shm` 并执行。这些方法虽便捷,但也需谨慎操作以避免安全风险。
179 6
|
3月前
|
网络协议 Linux
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
638 2
|
3月前
|
Linux Python
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
67 2
|
1月前
|
运维 Java Linux
【运维基础知识】Linux服务器下手写启停Java程序脚本start.sh stop.sh及详细说明
### 启动Java程序脚本 `start.sh` 此脚本用于启动一个Java程序,设置JVM字符集为GBK,最大堆内存为3000M,并将程序的日志输出到`output.log`文件中,同时在后台运行。 ### 停止Java程序脚本 `stop.sh` 此脚本用于停止指定名称的服务(如`QuoteServer`),通过查找并终止该服务的Java进程,输出操作结果以确认是否成功。
35 1
|
2月前
|
消息中间件 分布式计算 Java
Linux环境下 java程序提交spark任务到Yarn报错
Linux环境下 java程序提交spark任务到Yarn报错
42 5
|
3月前
|
NoSQL Linux C语言
嵌入式GDB调试Linux C程序或交叉编译(开发板)
【8月更文挑战第24天】本文档介绍了如何在嵌入式环境下使用GDB调试Linux C程序及进行交叉编译。调试步骤包括:编译程序时加入`-g`选项以生成调试信息;启动GDB并加载程序;设置断点;运行程序至断点;单步执行代码;查看变量值;继续执行或退出GDB。对于交叉编译,需安装对应架构的交叉编译工具链,配置编译环境,使用工具链编译程序,并将程序传输到开发板进行调试。过程中可能遇到工具链不匹配等问题,需针对性解决。
|
3月前
|
Linux API
在Linux中,程序产生了库日志虽然删除了,但磁盘空间未更新是什么原因?
在Linux中,程序产生了库日志虽然删除了,但磁盘空间未更新是什么原因?
|
3月前
|
网络协议 Linux Shell
在Linux中,如何通过一个端口找到程序?
在Linux中,如何通过一个端口找到程序?
|
3月前
|
Linux Windows Python
最新 Windows\Linux 后台运行程序注解
本文介绍了在Windows和Linux系统后台运行程序的方法,包括Linux系统中使用nohup命令和ps命令查看进程,以及Windows系统中通过编写bat文件和使用PowerShell启动隐藏窗口的程序,确保即使退出命令行界面程序也继续在后台运行。
|
3月前
|
存储 安全 Linux
【Azure 应用服务】App Service For Linux 怎么安装Composer,怎么安装PHP扩展,怎么来修改站点根路径启动程序?
【Azure 应用服务】App Service For Linux 怎么安装Composer,怎么安装PHP扩展,怎么来修改站点根路径启动程序?