Linux -- 多进程编程之 - 守护进程

内容概要

一、守护进程概述

  守护进程是一个生存期较长的进程,他常常在系统引导装入是启动,仅仅在系统关闭的才终止。也就是通常所说的 Daemon 进程,是 Linux 中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。 Linux 中很多系统服务都是通过守护进程实现的。

  在Linux中,可以根据指令 ps 命令打印进程的状态,在终端输入指令ps -ajx 可以查看当前进程的状态,如下所示(已删除部分内容)。

ubuntu@songshuai:~$ ps -ajx 
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
    0     1     1     1 ?           -1 Ss       0   0:03 /sbin/init splash
    0     2     0     0 ?           -1 S        0   0:00 [kthreadd]
    2     3     0     0 ?           -1 I        0   0:00 [kworker/0:0]
    2     4     0     0 ?           -1 I<       0   0:00 [kworker/0:0H]
    2     5     0     0 ?           -1 I        0   0:00 [kworker/u128:0]
    2     6     0     0 ?           -1 I<       0   0:00 [mm_percpu_wq]
    2     7     0     0 ?           -1 S        0   0:00 [ksoftirqd/0]
    2     8     0     0 ?           -1 I        0   0:00 [rcu_sched]
    2     9     0     0 ?           -1 I        0   0:00 [rcu_bh]
    1  1215  1215  1215 ?           -1 SLsl     0   0:00 /usr/sbin/lightdm
    1  1782  1782  1782 tty1      1782 Ss+      0   0:00 /sbin/agetty --noclear tty1 linux
 1763  1864  1863  1863 ?           -1 S     1000   0:00 upstart-udev-bridge --daemon --user
 1763  1874  1874  1874 ?           -1 Rs    1000   0:00 dbus-daemon --fork --session --address=unix:abstract=/tmp/dbus-den4cotw4
 1763  1886  1886  1886 ?           -1 Ss    1000   0:00 /usr/lib/x86_64-linux-gnu/hud/window-stack-bridge
 1763  1911  1910  1910 ?           -1 S     1000   0:00 upstart-dbus-bridge --daemon --system --user --bus-name system
 1763  1913  1912  1912 ?           -1 S     1000   0:00 upstart-dbus-bridge --daemon --session --user --bus-name session
 1763  1917  1916  1916 ?           -1 S     1000   0:00 upstart-file-bridge --daemon --user
 1763  1919  1918  1918 ?           -1 Sl    1000   0:04 /usr/bin/fcitx
 1763  1943  1943  1943 ?           -1 Ssl   1000   0:00 /usr/lib/x86_64-linux-gnu/bamf/bamfdaemon
 2589  2594  2594  2594 pts/2     2677 Ss    1000   0:00 bash
 2594  2677  2677  2594 pts/2     2677 R+    1000   0:00 ps -ajx
ubuntu@songshuai:~$ 

  在 ps 输出示例中,内核守护进程的名字出现在方括弧中,改版本的 Linux 使用一个名为 kthreadd 的特殊内核进程来创建其他内核进程,所以 kthreadd 表现为其他内核进程的父进程。

  进程 init 的进程通常为 1,他是一个系统守护进程,除了其他工作外,主要负责各个运行层次特定的系统服务。

  在Linux 中,每一个系统与用户进行交流的界面称为 终端。每一个从此终端开始运行的进程都会依附于该终端,这个终端称为这些进程的 控制终端。当控制终端关闭时,相应的进程都会自动结束。但是守护进程却能够突破这种限制,不受终端关闭的影响。反之,如果希望某个进程不因为用户、终端或者其他的变化而受到影响,那么就必须把这个进程变成一个 守护进程

二、守护进程创建

  创建守护进程,需要遵循特定的流程,以防产生不必要的交互过程。下面就分几个步骤来创建一个简单的守护进程。

2.1、创建子进程,父进程退出

  由于守护进程是脱离控制终端的,因此完成第一步后子进程变成后台进程。之后的所有工作都在子进程中完成。而用户通过 shell 可以执行其他的命令,从而在形式上做到了与控制终端的脱离。

  另外一方面虽然子进程继承了父进程的进程组 ID,但获得了一个新的进程 ID,这样保证了子进程不是一个进程组的组长进程。这也是下面要进程的第三步的先决条件。

说明:
  由于父进程已经先于子进程退出,会造成子进程没有父进程,从而变成一个孤儿进程(孤儿进程可以查看博文:Linux – 多进程编程之 – 基础实现、孤儿进程)。在 Linux 中,每当系统发现一个孤儿进程,就会自动由 1号进程(也就是 init 进程)收养它,这样,原先的子进程就会变成 init 进程的子进程了。

2.2、在子进程中创建新会话

  这个步骤是创建守护进程中最重要的一步,在这里使用的函数是 setsid()

2.2.1、进程组和会话期

  这里先要明确两个概念:进程组和会话期。

进程组
  进程组是一个或多个进程的集合。进程组由进程组 ID 来唯一标识。除了进程号( PID )之外,进程组 ID 也是一个进程的必备属性。
每个进程组都有一个组长进程,其组长进程的进程号等于进程组 ID ,且进程组 ID 不会因组长进程的退出而受到影响。

会话期
  会话组是一个或多个进程组的集合。通常一个会话开始于用户登录,终止于用户退出;或者说开始于终端打开,结束于终端关闭。会话期的第一个进程称为会话组长。在此期间该用户运行的所有进程都属于这个会话期。

  进程组和会话期之间的关系如图2.1所示。

《Linux -- 多进程编程之 - 守护进程》
图2.1 进程组和会话期之间的关系图

2.2.2、setsid()函数说明

1、setsid()函数原型

  setsid()函数原型如下所示(使用指令 man 2 setsid 即可显示如下代码)。

#include <sys/types.h>
#include <unistd.h>

pid_t setsid(void);

功能:
  如果调用进程不是进程组长,则 setsid() 将创建一个新会话。调用进程将成为新会话的会话组组长(即,其会话 ID 与其进程 ID 相同)。同时调用进程也将成为会话中新进程组的进程组组长(即,其进程组 ID 与其进程 ID 相同)。调用进程将是新进程组和新会话中的唯一进程。
参数:无
返回:
  成功:返回调用进程的(新)会话ID
  失败:返回(pid_t)-1,并设置 errno

2、setsid()函数作用

  上面已经提到,setsid() 函数用于创建一个新的会话,并担任该会话的组长,所以调用 setsid() 有下面 3 个作用。

1、让进程摆脱原会话的控制
2、让进程摆脱原进程组的控制
3、让进程摆脱原控制终端的控制

  由于在调用 fork() 函数时,子进程 全盘复制 了父进程的会话期进程组和控制终端等。所以虽然父进程退出了,但原先的 会话期进程组控制终端等并没有改变,因此,子进程并不是真正意义上的独立,而 setsid() 函数能够使进程完全独立出来,从而脱离所有其他进程的控制。

2.3、改变当前工作目录

  使用 fork() 函数创建的子进程是完全继承了父进程的当前工作目录,所以从父进程继承过来的当前工作目录可能是一个挂载的文件系统中。因为守护进程有一般情况是在系统在引导之前是一直从在的,所以在进程工作的过程中当前目录所在的文件系统(比如“/mnt/usb” 等)是不能卸载的。

  因此,一般的做法是将根目录作为守护进程的当前工作目录,这样就可以避免上述问题。当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如“/tmp”

  改变工作目录的函数是 chdir() 函数,其函数原型如下所示。

#include <unistd.h>

int chdir(const char *path);

功能:
  改变调用者的工作目录
参数:
  path:新的工作目录的路径
返回:
  成功:返回0
  失败:返回-1,同时设置errno

2.4、重设文件权限掩码

  文件权限掩码(通常用八进制表示)的作用是屏蔽文件权限中的对应位。例如,如果文件权限拖码是050,它表示屏蔽了文件组拥有者的可读与可执行权限。由于使用 fork() 函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了一定的影响。如果守护进程需要创建文件,那么他可能需要设置特定的权限。因此,把文件权限掩码设置为一个已知的值(通常设置为0),可以增强该守护进程的灵活性。

  设置文件权限掩码的函数是 umask()。在这里,通常的使用方法为 umask(0)。其函数原型如下所示。

#include <sys/types.h>
#include <sys/stat.h>

mode_t umask(mode_t mask);

功能:
  umask() 将调用进程的文件模式创建掩码( umask)设置为 mask & 0777(即仅使用掩码的文件权限位)。
参数:
  mask:要设置的权限值,用八进制表示
返回:
  此系统调用始终成功,并返回掩码的上一个值。

2.5、关闭不需要的文件描述符

  同样地,用 fork() 函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程访问,但它们一样占用系统资源,而且还可能导致所在的文件系统无法被卸载。

  特别是守护进程和终端无关,所以指向终端设备的标准输入、标准输出和标准错误流等已经不再使用,应当被关闭。

  可以使用函数 getdtablesize() 来获取当前金成文件描述符表的大小,并通过使用 close() 来依次关闭。

函数原型如下。

#include <unistd.h>
int getdtablesize(void);

getdtablesize()函数返回进程可以打开的最大文件数,比文件描述符的最大可能值多一个。

#include <unistd.h>
int close(int fd);

close() 用于关闭文件描述符,关闭成功则返回 0,失败则返回 -1 并设置 errno

  所以关闭文件描述符的代码可以如下写法。

int num = getdtablesize(); // 获取当前进程文件描述符表大小

for (int i = 0; i < num; i++)  
{ 
    close (i);
}

2.6、某些特殊的守护进程打开/dev/null

  某些特殊的守护进程打开/dev/null,使其具有文件描述符0、1、2,这样任何一个试图读标准输入、标准输出、标准出错时都不会有任何效果,这样符合了守护进程不与终端设备相关联的属性。

  综上所述,一个守护进程就可以创建成功了,所以创建守护进程的流程可以总结为如图2.2所示。

《Linux -- 多进程编程之 - 守护进程》
图2.2 创建守护进程流程图

三、守护进程代码示例

  用下面的一个简单的程序,用来示例守护进程的完整的创建过程。守护进程的主要工作则是每隔一定时间向日志文件“/daemon.log”文件中写入内容。

#include <fcntl.h> // for O_APPEND ..
#include <stdio.h> // for perror ..
#include <stdlib.h> // for exit ..
#include <string.h> // for strlen
#include <sys/stat.h> // for umask
#include <sys/types.h> // for setsid
#include <unistd.h> // for setsid

int main(int argc, const char *argv[])
{ 
    pid_t pid = 0;
    int i = 0;
    char *filePath = "daemon.log";

    /* 第一步:创建子进程,父进程退出 */
    pid = fork();
    if (pid == -1) /* fork出错 */
    { 
        perror("fork error");
        exit(EXIT_FAILURE);
    }
    else if (pid == 0) /* 子进程 */
    { 
        pid_t temp_pid = 0;
        /* 周期计数的变量 */
        int cycleCnt = 0;
        /* 第二步:创建新的会话 */
        temp_pid = setsid();
        /* 第三部:改变当前的工作路径*/
        chdir("/");
        /* 第四步:改变进程本身的umask */
        umask(0);
        /* 第五步:关闭所有可能已打开的文件描述符 */
        int num = getdtablesize(); /* 获取当前进程文件描述符表大小 */
        for (i = 0; i < num; i++)
        { 
            close(i);
        }

        /* 至此,守护进程创建完成,以下正式开始守护进程的工作 */
        /* 1、打开要操作的文件 */
        int fd = open(filePath, O_RDWR | O_CREAT | O_APPEND, 0600);
        if (fd == -1) /* open操作失败 */
        { 
            perror("open error");
            exit(EXIT_FAILURE);
        }

        /* 2、在文件中循环写入测试数据 */
        while (1)
        { 
            /* 写入内容的缓冲区定义 */
            char writeBuff[128] = { 0};
            /* 周期运行计数自加 */
            cycleCnt++;
            /* 写入的数据拼接 */
            sprintf(writeBuff, "I'm Daemon Process, Running %d\n", cycleCnt);
            /* 写入到文件中 */
            write(fd, writeBuff, strlen(writeBuff));
            /* 休眠片刻 */
            sleep(1);
        }
    }
    else /* 父进程 */
    { 
        /* 父进程退出 */
        exit(EXIT_SUCCESS);
    }

    return 0;
}

  编译并运行上述程序,运行效果及进程状态如图3.1所示。

《Linux -- 多进程编程之 - 守护进程》
图3.1 运行效果及进程状态效果图

  需要注意的是,因为守护进程的工作目录已经修改为“\”,并且程序中需要在根目录中创建文件并写入,所以需要root权限,运行程序使用 sudo 命令(下同)。

  此时查看程序记录的文件,使用指令sudo cat daemon.log 即可显示,文件内容如图3.2所示。

《Linux -- 多进程编程之 - 守护进程》
图3.2 文件内容显示效果图

  至此为止,守护进程相关的主要知识基本上总结的差不多了。基于守护进程的特征,那么在记录守护进程的异常处理方面也需要特殊的处理,那么将会下一篇进程守护进程的出错的处理。

  好啦,废话不多说,总结写作不易,如果你喜欢这篇文章或者对你有用,请动动你发财的小手手帮忙点个赞,当然 关注一波 那就更好了,就到这儿了,么么哒(*  ̄3)(ε ̄ *)。

《Linux -- 多进程编程之 - 守护进程》
《Linux -- 多进程编程之 - 守护进程》

上一篇:Linux – 多进程编程之 – 僵尸进程
下一篇:Linux – 多进程编程之 – 守护进程的出错处理

    原文作者:青椒*^_^*凤爪爪
    原文地址: https://blog.csdn.net/zhemingbuhao/article/details/121153375
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞