Linux下守护进程的C语言实现


=Start=

缘由:

以后可能要用C语言写一个daemon程序(守护进程)运行在服务器上,以进行一些信息收集的工作。之前只是简单知道什么是守护进程,但对于一些细节以及该如何编写都不太清楚,所以需要先学习、准备一下。

正文:

参考解答:
守护进程(Daemon)是什么?

守护进程,也即通常所说的 Daemon 进程,是 Linux 下一种特殊的后台服务进程,它独立于控制终端并且周期性的执行某种任务或者等待处理某些发生的事件。守护进程通常在系统引导装入时启动,在系统关闭时终止。Linux 系统下大多数服务都是通过守护进程实现的。守护进程的名称通常以 “d” 结尾,比如 “httpd”、“crond”、“mysqld” 等。

控制终端是什么?

终端是用户与操作系统进行交流的界面。在 Linux 系统中,用户由终端登录系统登入系统后会得到一个 shell 进程,这个终端便成为这个 shell 进程的控制终端(Controlling Terminal)。shell 进程启动的其他进程,由于复制了父进程的信息,因此也都同依附于这个控制终端。

从终端启动的进程都依附于该终端,并受终端控制和影响。终端关闭,相应的进程都会自动关闭。守护进程脱离终端的目的,也即是不受终端变化的影响不被终端打断,当然也不想在终端显示执行过程中的信息。如果不想进程受到用户、终端或其他变化的影响,就必须把它变成守护进程。

如何实现守护进程?

守护进程属于 Linux 进程管理的范畴。其首要的特性是后台运行;其次,要与从启动它的父进程的运行环境隔离开来,需要处理的内容大致包括会话、控制终端、进程组、文件描述符、文件权限掩码以及工作目录等。

守护进程可以在 Linux 启动时从脚本 /etc/rc.d 启动,也可以由作业规划进程 crond 启动,还可以通过用户终端(一般是 Shell)启动。

实现一个守护进程,其实就是将普通进程按照上述特性改造为守护进程的过程。需要注意的一点是,不同版本的 Unix 系统其实现机制不同,BSD 和 Linux 下的实现细节就不同。

根据上述的特性,我们便可以创建一个简单的守护进程,这里以 Linux 系统下从终端 Shell 来启动为例。

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

编写守护进程第一步,就是要使得进程独立于终端后台运行。为避免终端挂起,将父进程退出,造成程序已经退出的假象,所有后面的工作都在子进程完成,这样控制终端也可以继续执行其他命令,从而在形式上脱离控制终端的控制。

由于父进程先于子进程退出,子进程就变为孤儿进程,并由 init 进程作为其父进程收养。

2、子进程创建新会话

经过上一步,子进程已经后台运行,然而系统调用 fork 创建子进程,子进程便复制了原父进程的进程控制块(PCB),相应地继承了一些信息,包括会话、进程组、控制终端等信息。尽管父进程已经退出,但子进程的会话、进程组、控制终端的信息没有改变。为使子进程完全摆脱父进程的环境,需要调用 setsid 函数。

这里有必要说一下两个概念:会话和进程组。

进程组:一个或多个进程的集合。拥有唯一的标识进程组 ID,每个进程组都有一个组长进程,该进程的进程号等于其进程组的 ID。进程组 ID 不会因组长进程退出而受到影响,fork 调用也不会改变进程组 ID。

会话:一个或多个进程组的集合。新建会话时,当前进程(会话中唯一的进程)成为会话首进程,也是当前进程组的组长进程,其进程号为会话 ID,同样也是该进程组的 ID。它通常是登录 shell,也可以是调用 setsid 新建会话的孤儿进程。注意:组长进程调用 setsid ,则出错返回,无法新建会话。

通常,会话开始于用户登录,终止于用户退出,期间的所有进程都属于这个会话。一个会话一般包含一个会话首进程、一个前台进程组和一个后台进程组,控制终端可有可无;此外,前台进程组只有一个,后台进程组可以有多个,这些进程组共享一个控制终端。

前台进程组:该进程组中的进程可以向终端设备进行读、写操作(属于该组的进程可以从终端获得输入)。该进程组的 ID 等于控制终端进程组 ID,通常据此来判断前台进程组。

后台进程组:会话中除了会话首进程和前台进程组以外的所有进程,都属于后台进程组。该进程组中的进程只能向终端设备进行写操作。

会话(session):如果想了解更多关于会话(session)的内容,可以好好读一下 APUE 这本书。

如果调用进程非组长进程,那么就能创建一个新会话

  • 该进程变成新会话的首进程
  • 该进程成为一个新进程组的组长进程
  • 该进程没有控制终端,如果之前有,则会被中断(会话过程对控制终端的独占性)

也就是说:组长进程不能成为新会话首进程,新会话首进程必定成为组长进程

到此为止,我们熟悉了会话与进程间的关系,那么如何新建一个会话呢?

通过调用 setsid 函数可以创建一个新会话,调用进程担任新会话的首进程,其作用有

  • 使当前进程脱离原会话的控制
  • 使当前进程脱离原进程组的控制
  • 使当前进程脱离原控制终端的控制

这样,当前进程才能实现真正意义上完全独立出来,摆脱其他进程的控制。

另外,要提一下,尽管进程变成无终端的会话首进程,但是它仍然可以重新申请打开一个控制终端。可以通过再次创建子进程结束当前进程,使进程不再是会话首进程来禁止进程重新打开控制终端。

3、改变当前工作目录

直接调用 chdir 函数将切换到根目录下。
由于进程运行过程中,当前目录所在的文件系统(如:“/mnt/usb”)是不能卸载的,为避免对以后的使用造成麻烦,改变工作目录为根目录是必要的。如有特殊需要,也可以改变到特定目录,如“/tmp”。

4、重设文件权限掩码

fork 函数创建的子进程,继承了父进程的文件操作权限,为防止对以后使用文件带来问题,需要重设文件权限掩码。

文件权限掩码,设定了文件权限中要屏蔽掉的对应位。这个跟文件权限的八进制数字模式表示差不多,将现有存取权限减去权限掩码(或做异或运算),就可产生新建文件时的预设权限。

调用 umask 设置文件权限掩码,通常是重设为 0,清除掩码,这样可以大大增强守护进程的灵活性。

5、关闭无关文件描述符

同文件权限掩码一样,子进程可能继承了父进程打开的文件,而这些文件可能永远不会被用到,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下,因此需要一一关闭它们。由于守护进程脱离了终端运行,因此标准输入、标准输出、标准错误输出这3个文件描述符也要关闭。通常按如下方式来关闭:

for (i=0; i < MAXFILE; i++)
    close(i);

这里要注意下,param.h 头文件中定义了一个常量 NOFILE,表示最大允许的文件描述符,但是我们尽量不要用它,而是通过调用函数 getdtablesize 返回进程文件描述符表中的项数(即打开的文件数目):

/* The following are not really correct but it is a value we used for a long time
and which seems to be usable. People should not use NOFILE and NCARGS anyway. */
#define NOFILE 256
#define NCARGS 131072

至此为止,一个简单的守护进程就建立起来了。

另外,有些 Unix 提供一个 daemon 的 C 库函数,实现守护进程。(BSD 和 Linux 均提供这个函数):

NAME
     daemon - run in the background
SYNOPSIS
     #include <unistd.h>
     int daemon(int nochdir, int noclose);
DESCRIPTION
     The daemon() function is for programs wishing to detach themselves from the controlling terminal and run in the background as system daemons.
守护进程的示例代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

// 守护进程初始化函数
void init_daemon()
{
    pid_t pid;
    int i = 0;

    if ((pid = fork()) == -1) {
        printf("Fork error !\n");
        exit(1);
    }
    if (pid != 0) {
        exit(0);        // 父进程退出
    }

    setsid();           // 子进程开启新会话,并成为会话首进程和组长进程
    if ((pid = fork()) == -1) {
        printf("Fork error !\n");
        exit(-1);
    }
    if (pid != 0) {
        exit(0);        // 结束第一子进程,第二子进程不再是会话首进程
    }
    chdir("/tmp");      // 改变工作目录
    umask(0);           // 重设文件掩码
    for (; i < getdtablesize(); ++i) {
       close(i);        // 关闭打开的文件描述符
    }

    return;
}

int main(int argc, char *argv[])
{
    int fp;
    time_t t;
    char buf[] = {"This is a daemon:  "};
    char *datetime;
    int len = 0;
    //printf("The NOFILE is: %d\n", NOFILE);
    //printf("The tablesize is: %d\n", getdtablesize());
    //printf("The pid is: %d\n", getpid());

    // 初始化 Daemon 进程
    init_daemon();

    // 每隔一分钟记录运行状态
    while (1) {
        if (-1 == (fp = open("/tmp/daemon.log", O_CREAT|O_WRONLY|O_APPEND, 0600))) {
          printf("Open file error !\n");
          exit(1);
        }
        len = strlen(buf);
        write(fp, buf, len);
        t = time(0);
        datetime = asctime(localtime(&t));
        len = strlen(datetime);
        write(fp, datetime, len);
        close(fp);
        sleep(60);
    }

    return 0;
}
守护进程的正确性验证

编译代码:
gcc -o firstdaemon daemonize.c

启动守护进程:
./firstdaemon

检查是否工作正常:
ps -xj | grep firstdaemon

输出应该类似于:

+------+------+------+------+-----+-------+------+------+------+-----+
| PPID | PID  | PGID | SID  | TTY | TPGID | STAT | UID  | TIME | CMD |
+------+------+------+------+-----+-------+------+------+------+-----+
|    1 | 3387 | 3386 | 3386 | ?   |    -1 | S    | 1000 | 0:00 | ./  |
+------+------+------+------+-----+-------+------+------+------+-----+

解释如下:

  • 守护进程不应该有控制终端,所以(TTY = ?)
  • 守护进程的父进程ID为1,即init进程
  • 其中 PID != SID 表明该进程不是会话的leader进程,因为第二个fork()的作用
  • 因为 PID != SID 所以该守护进程无法重新打开/控制一个TTY
参考链接:

=END=


《 “Linux下守护进程的C语言实现” 》 有 6 条评论

  1. 单个Python文件实现的daemon类 (Python daemonizer for Unix, Linux and OS X)
    https://github.com/serverdensity/python-daemon

    用Python实现的方便编写daemon程序的库 (daemonize is a library for writing system daemons in Python.)
    https://github.com/thesharp/daemonize

    A Python library for creating super fancy Unix daemons
    https://github.com/jnrbsn/daemonocle

    Daemonize anything ( Unix only )(单文件实现)
    https://github.com/martinrusev/python-daemon

    python daemon that munches on logs and sends their contents to logstash (一个用于读取log日志并将内容发送至logstash的用Python写的daemon)
    https://github.com/python-beaver/python-beaver

    Diamond is a python daemon that collects system metrics and publishes them to Graphite (and others). (用Python写的daemon,用于收集系统指标并将数据推送到Graphite等地方)
    https://github.com/python-diamond/Diamond

    File Conveyor is a daemon written in Python to detect, process and sync files. (用Python写的用于检测、处理、同步文件的daemon)
    https://github.com/wimleers/fileconveyor

    用Python写的监控文件变化的daemon
    https://github.com/gregghz/Watcher

  2. 工作线程数究竟要设置为多少 | 架构师之路
    https://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=2651960260&idx=1&sn=051fd566d43d7fd35724bdf55484ee5f
    `
    · 线程数不是越多越好
    · sleep()不占用CPU
    · 单核设置多线程不但能使得代码清晰,还能提高吞吐量
    · 站点和服务最常用的线程模型是“IO线程与工作现场通过任务队列解耦”,此时设置多工作线程可以提升吞吐量
    · N核服务器,通过日志分析出任务执行过程中,本地计算时间为x,等待时间为y,则工作线程数(线程池线程数)设置为 N*(x+y)/x,能让CPU的利用率最大化
    `

  3. daemontools
    http://cr.yp.to/daemontools.html
    https://cr.yp.to/daemontools/svscanboot.html
    `
    svscanboot starts svscan in the /service directory, with output and error messages logged through readproctitle.
    svscanboot is available in daemontools 0.75 and above.
    `
    https://cr.yp.to/daemontools/readproctitle.html
    `
    readproctitle maintains an automatically rotated log in memory for inspection by ps.
    readproctitle is available in daemontools 0.75 and above.
    `
    进程的守护神 – daemontools
    http://linbo.github.io/2013/02/24/daemontools

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注