Bootstrap

Linux 高级编程 - 守护进程

什么是守护进程?

守护进程可以简单的理解为后台的服务进程,很多上层的服务器都是以守护进程为基础开发的。例如 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 下的守护进程:

mydaemon

知道了守护进程的位置,现在就可以把 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,坚持技术原创,只说真话!

CDeveloper

;