美文网首页从 0 开始学习 Linux 系列程序员
从 0 开始学习 Linux 系列之「18.守护进程」

从 0 开始学习 Linux 系列之「18.守护进程」

作者: 登龙zZ | 来源:发表于2017-09-02 11:39 被阅读106次
    daemon

    版权声明:本文为 cdeveloper 原创文章,可以随意转载,但必须在明确位置注明出处!

    什么是守护进程?

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

    最后,感谢你的阅读,我们下次再见 :)

    相关文章

      网友评论

        本文标题:从 0 开始学习 Linux 系列之「18.守护进程」

        本文链接:https://www.haomeiwen.com/subject/uholjxtx.html