什么是守护进程?
守护进程可以简单的理解为后台的服务进程,很多上层的服务器都是以守护进程为基础开发的。例如 Linux 上运行的 Apache 服务器,Android 系统的 Service 服务,它们的底层都由 Linux 的守护进程提供服务。
这篇文章介绍的是在 Linux 编写守护进程的方法。
编写守护进程的 6 个步骤
先来看看整体的编写步骤:
1. 重新设置 umask(0)
2. 执行 fork 并脱离父进程
3. 重启 session 会话
4. 改变当前工作目录
5. 关闭文件描述符
6. 固定文件描述符 0, 1, 2 到 /dev/null
下面我们来写一个守护进程的初始化程序 daemon_init.c
来学习这 6 个步骤。
1. 重新设置 umask
进程从创建他的父进程那里继承文件创建的掩码,它可能修改守护进程所创建的文件的权限,这里清除它:
void daemon_init(void) {
// 1. 重新设置 umask
umask(0);
}
2. 执行 fork 并脱离父进程
既然是服务进程,意味着是可以独立运行的,不会因为父进程退出而销毁,在 Linux 系统中有一个进程号为 1 的 init 进程,这个进程一直存在与系统中,当我们的子进程从父进程脱离后,子进程变为孤儿进程,随之系统的 init 进程会接管对这个进程的控制。我们看看代码:
void daemon_init(void) {
// 1. 重新设置 umask
// 2. 脱离父进程
pid_t pid = fork() ;
if(pid < 0)
exit(1); // 进程创建失败
else if(pid > 0)
exit(0); // 退出父进程
return 0;
}
3. 重启 session 会话
使用 setsid
重新开启一个 session 会话,防止脱离父进程的子进程重新受控于字符终端进程,尽量加上这一步防备工作让 fork 的子进程直接受控于 init 进程,要知道编写守护进程的核心是让子进程直接受控于 init 进程:
void daemon_init(void) {
// 1. 重新设置 umask
// 2. 脱离父进程
// 3. 重启 session 会话
setsid();
}
4. 改变工作目录
进程活动时,其工作目录所在的文件系统不能卸载,一般需要将守护进程工作目录改变到根目录 /
:
void daemon_init(void) {
// 1. 重新设置 umask
// 2. 脱离父进程
// 3. 重启 session 会话
// 4. 改变工作目录
chdir("/");
}
5. 关闭文件描述符
进程从创建它的父进程哪里继承了打开的文件描述符,若不关闭将会造成资源浪费,造成进程所在的文件系统无法卸下以及引起无法预料的错误:
void daemon_init(void) {
// 1. 重新设置 umask
// 2. 脱离父进程
// 3. 重启 session 会话
// 4. 改变工作目录
// 5. 得到并关闭文件描述符
struct rlimit rl;
getrlimit(RLIMIT_NOFILE, &rl);
if (rl.rlim_max == RLIM_INFINITY)
rl.rlim_max = 1024;
for(int i = 0; i < rl.rlim_max; i++)
close(i);
}
6. 固定文件描述符 0, 1, 2 到 /dev/null
守护进程在后台运行,不会与用户发生直接的交互,我们不希望在终端上看到守护进程的输出,用户也不期望他们在终端上的输入被守护进程读取,因此我们将文件描述符 0, 1, 2 定位到 /dev/null
:
void daemon_init(void) {
// 1. 重新设置 umask
// 2. 脱离父进程
// 3. 重启 session 会话
// 4. 改变工作目录
// 5. 得到并关闭文件描述符
// 6. 固定文件描述符 0, 1, 2 到 /dev/null
int fd0 = open("/dev/null", O_RDWR);
int fd1 = dup(0);
int fd2 = dup(0);
}
这样,我们 daemon_init 就写好了,可以用这个函数来初始化一个守护进程了,我们来测试测试。
测试 daemon_init 初始化函数
我们编写一个 printlg.c
循环向文件中输出信息:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
void daemon_init(void);
int main(void) {
// 初始化守护进程
daemon_init();
// 守护进程逻辑
char *msg = "I'm printlg process...\n" ;
int msg_len = strlen(msg);
int fd = open("/tmp/test_printlg.log", O_RDWR | O_CREAT | O_APPEND, 0666);
if(fd < 0) {
printf("open /tmp/test_printlg.log fail.\n");
exit(1);
}
while(1) {
// 每隔 3s 输出 msg 到 /tmp/test_printlg.log 文件中
write(fd, msg, msg_len);
sleep(3);
}
close(fd);
return 0;
}
完整的代码参考:printlg.c,下面我们来测试这个守护进程能够工作。
测试 printlg.c
守护进程
编译
gcc printlg.c -o printlg
运行
./printlg
结果最终写入到 /tmp/test_printlg.log
文件中,我们每隔 3 s 查看这个文件的内容,发现内容在不断增多的:
cat /tmp/test_printlg.log
# 结果
I'm printlg process...
I'm printlg process...
I'm printlg process...
到此为止,一个守护进程就写好了,但是很多的守护进程都可以开机自启动,我们的可不可以呢?
让守护进程开机自启动
很多的守护进程都设置了开机自启动,我们也来让 printlg
能够开机自启动,先来了解自启动的原理。
每个系统启动级别的守护进程分别在 /etc/rcN.d
下,比如我的图形界面的启动级别是 5,那么在这个启动级别下自动运行和禁止启动守护进程都在 /etc/rc5.d
下。这里我的 ubuntu 系统的守护进程目录是 /etc/rcN.d
,如果你是其他的 Linux 可能会不太一样,你可以使用 whereis rc[N].d
来看看具体的目录位置,不要死记硬背。
这是我的 ubuntu 的 /etc/rc5.d
下的守护进程:
知道了守护进程的位置,现在就可以把 printlg
放在 /etc/rc5.d/
下,并且还要改名称,因为系统需要根据指定的名称来使用 for 循环来启动或者关闭每个程序,命名规则如下:
1. S[num][name]
:启动守护进程 name,例如:S01printlg
2. K[num][name]
:禁止启动守护进程 name,例如:K01printlg
我们这里肯定要启动了,所以将 printlg
命名为 S01printlg
:
mv printlg /etc/rc5.d/S01printlg
之后我们重启机器,再次查看 /tmp/test_printlg
文件,就可以看到服务已经启动了,这样守护进程就成功自启动啦。
结语
这次我们学习了如何在 Linux 编写一个守护进程,守护进程其实就是一个后台的服务程序,学习如何在 Linux 创建服务程序还是非常有必要的,因为我们无时无刻都在使用很多系统提供的服务,了解原理以后再使用 Linux 的服务会更加得心应手。
最后,感谢你的阅读,我们下次再见 :)
推荐关注我的微信公众号 CDeveloper,坚持技术原创,只说真话!