13_Unix之守护进程

守护进程(daemon)是生存期长的一种进程。它们常常在系统引导装入时启动,仅在系统关闭时才终止。因为它们没有控制终端,所以说它们是在后台运行的。UNIX系统有很多守护进程,它们执行日常事务活动。


13.1 守护进程的特征

先看一些常用的系统守护进程,系统进程依赖于操作系统实现。父进程ID 为0 的各进程通常是内核进程,它们作为系统引导装入过程的一部分而启动。( init 是个例外,它是一个由内核在引导装入时启动的用户层次的命令。)内核进程是特殊的,通常存在于系统的整个生命期中。它们以超级用户特权运行,无控制终端,无命令行。

在ps 的输出实例中,内核守护进程的名字出现在方括号中。该版本的 Linux使用一个名为 kthreadd 的特殊内核进程来创建其他内核进程,所以 kthreadd 表现为其他内核进程的父进程。对于需要在进程上下文执行工作但却不被用户层进程上下文调用的每一个内核组件,通常有它自己的内核守护进程。例如,在Linux中:

  • kswapd 守护进程也称为内存换页守护进程。它支持虚拟内存子系统在经过一段时间后将脏页面慢慢地写回磁盘来回收这些页面。

  • flush 守护进程在可用内存达到设置的最小阈值时将脏页面冲洗至磁盘。它也定期地将脏页面冲洗回磁盘来减少在系统出现故障时发生的数据丢失。多个冲洗守护进程可以同时存在,每个写回的设备都有一个冲洗守护进程。

  • sync_supers 守护进程定期将文件系统元数据冲洗至磁盘。

  • jbd 守护进程帮助实现了ext4文件系统中的日志功能。

进程1通常是 init,除了其他工作外,主要负责启动各运行层次特定的系统服务。这些服务通常是在它们自己拥有的守护进程的帮助下实现的。

inetd 守护进程侦听系统网络接口,以便取得来自网络的对各种网络服务进程的请求。nfsdnfsiodlockd、rpciodrpc.idmapdrpc.statdrpc.mountd 守护进程提供对网络文件系统(Network File System,NFS)的支持。注意,前4个是内核守护进程,后3个是用户级守护进程。

cron 守护进程在定期安排的日期和时间执行命令。许多系统管理任务是通过 cron 每隔一段固定的时间就运行相关程序而得以实现的。atd 守护进程与 cron 类似,它允许用户在指定的时间执行任务,但是每个任务它只执行一次,而非在定期安排的时间反复执行。cupsd 守护进程是个打印假脱机进程,它处理对系统提出的各个打印请求。sshd 守护进程提供了安全的远程登录和执行设施。

注意,大多数守护进程都以超级用户(root)特权运行。所有的守护进程都没有控制终端,其终端名设置为问号。内核守护进程以无控制终端方式启动。用户层守护进程缺少控制终端可能是守护进程调用了 setsid 的结果。大多数用户层守护进程都是进程组的组长进程以及会话的首进程,而且是这些进程组和会话中的唯一进程(rsyslogd 是一个例外)。最后,应当引起注意的是用户层守护进程的父进程是 init 进程。


13.2 编程规则

在编写守护进程程序时需遵循一些基本规则,以防止产生不必要的交互作用。下面先说明这些规则,然后给出一个按照这些规则编写的函数 daemonize

(1)首先要做的是调用 umask 将文件模式创建屏蔽字设置为一个已知值(通常是0)。

(2)调用fork,然后使父进程exit。这样做实现了下面几点。第一,如果该守护进程是作为一条简单的shell命令启动的,那么父进程终止会让shell认为这条命令已经执行完毕。第二,虽然子进程继承了父进程的进程组 ID,但获得了一个新的进程 ID,这就保证了子进程不是一个进程组的组长进程。这是下面将要进行的 setsid调用的先决条件。

(3)调用 setsid 创建一个新会话。使调用进程:(a)成为新会话的首进程,(b)成为一个新进程组的组长进程,(c)没有控制终端。

(4)将当前工作目录更改为根目录。从父进程处继承过来的当前工作目录可能在一个挂载的文件系统中。因为守护进程通常在系统再引导之前是一直存在的,所以如果守护进程的当前工作目录在一个挂载文件系统中,那么该文件系统就不能被卸载。

(5)关闭不再需要的文件描述符。这使守护进程不再持有从其父进程继承来的任何文件描述符(父进程可能是 shell 进程,或某个其他进程)。可以使用 open_max 函数或 getrlimit 函数来判定最高文件描述符值,并关闭直到该值的所有描述符。

(6)某些守护进程打开/dev/null使其具有文件描述符0、1和2,这样,任何一个试图读标准输入、写标准输出或标准错误的库例程都不会产生任何效果。因为守护进程并不与终端设备相关联,所以其输出无处显示,也无处从交互式用户那里接收输入。即使守护进程是从交互式会话启动的,但是守护进程是在后台运行的,所以登录会话的终止并不影响守护进程。如果其他用户在同一终端设备上登录,我们不希望在该终端上见到守护进程的输出,用户也不期望他们在终端上的输入被守护进程读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <stdio.h>                                                               
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
void demonize()
{
pid_t pid;
//1. 创建子进程并退出父进程
pid = fork();
if(pid < 0)
{
perror("fork");
exit(EXIT_FAILURE);
}
else if(pid > 0)
{
// 父进程退出
exit(EXIT_SUCCESS);
}
// 2. 创建新会话
if(setsid() < 0)
{
perror("setsid");
exit(EXIT_FAILURE);
}
// 3. 改变工作目录到更目录
if(chdir("/") < 0)
{
perror("chdir");
exit(EXIT_FAILURE);
}
// 4. 重设文件权限掩码
umask(0);
// 5. 关闭所有打开的文件描述符
for(int i = 0; i < sysconf(_SC_OPEN_MAX); i++)
{
close(i);
}
// 6. 重定向标准输入、标准输出、标准错误到 /dev/null
int fd = open("/dev/null",O_RDWR);
if(fd < 0)
{
perror("open");
exit(EXIT_FAILURE);
}
dup2(fd, STDIN_FILENO);
dup2(fd, STDERR_FILENO);
dup2(fd, STDOUT_FILENO);
if(fd > STDERR_FILENO)
{
close(fd);
}
}

void demon_task()
{
// 模拟守护进程的任务
while(1)
{
// 守护进程的实际任务
sleep(10); // 每隔10秒执行一次
}
}
int main()
{
demonize();
demon_task();
return 0;
}

13.3 出错记录

守护进程存在的一个问题是如何处理出错消息。因为它本就不应该有控制终端,所以不能只是简单地写到标准错误上。需要有一个集中的守护进程出错记录设施。

大多数守护进程都使用 syslog 设施。下图显示了 syslog 设施的详细组织结构。

img

有以下3种产生日志消息的方法。

(1)内核例程可以调用 log 函数。任何一个用户进程都可以通过打开(open)并读取(read)/dev/klog 设备来读取这些消息。

(2)大多数用户进程(守护进程)调用syslog函数来产生日志消息。这使消息被发送至UNIX域数据报套接字 /dev/log

(3)无论一个用户进程是在此主机上,还是在通过TCP/IP网络连接到此主机的其他主机上,都可将日志消息发向UDP端口514。注意,syslog 函数从不产生这些UDP数据报,它们要求产生此日志消息的进程进行显式的网络编程。

通常,syslogd 守护进程读取所有3种格式的日志消息。此守护进程在启动时读一个配置文件,其文件名一般为 /etc/syslog.conf,该文件决定了不同种类的消息应送向何处。例如,紧急消息可发送至系统管理员(若已登录),并在控制台上打印,而警告消息则可记录到一个文件中。

该设施的接口是 syslog 函数。

1
2
3
4
5
6
#include <syslog.h>
void openlog(const char *ident, int option, int facility);
void syslog(int priority, const char *format, ...);
void closelog(void);
int setlogmask(int maskpri);
// 返回值:前日志记录优先级屏蔽字值

调用openlog是可选择的。如果不调用openlog,则在第一次调用syslog时,自动调用openlog。调用 closelog也是可选择的,因为它只是关闭曾被用于与syslogd 守护进程进行通信的描述符。

调用syslog产生一个日志消息。其 priority 参数是 facilitylevel 的组合。level 值按优先级从最高到最低依次排列。

img

将format参数以及其他所有参数传至 vsprintf 函数以便进行格式化。在format中,每个出现的%m字符都先被代换成与errno值对应的出错消息字符串(strerror)。

setlogmask 函数用于设置进程的记录优先级屏蔽字。它返回调用它之前的屏蔽字。当设置了记录优先级屏蔽字时,各条消息除非已在记录优先级屏蔽字中进行了设置,否则将不被记录。注意,试图将记录优先级屏蔽字设置为0并不会有什么作用。

除了 syslog,很多平台还提供它的一种变体来处理可变参数列表。

1
2
3
#include <syslog.h>
#include <stdarg.h>
void vsyslog(int priority, const char *format, va_list arg);

大多数 syslog 实现将使消息短时间处于队列中。如果在此段时间中有重复消息到达,那么 syslog 守护进程不会把它写到日志记录中,而是会打印输出一条类似于“上一条消息重复了N次”的消息。


13.4 单实例守护进程

如果每一个守护进程创建一个有固定名字的文件,并在该文件的整体上加一把写锁,那么只允许创建一把这样的写锁。在此之后创建写锁的尝试都会失败,这向后续守护进程副本指明已有一个副本正在运行。

文件和记录锁提供了一种方便的互斥机制。如果守护进程在一个文件的整体上得到一把写锁,那么在该守护进程终止时,这把锁将被自动删除。这就简化了复原所需的处理,去除了对以前的守护进程实例需要进行清理的有关操作。

图中所示的函数说明了如何使用文件和记录锁来保证只运行单实例守护进程。

img

守护进程的每个副本都将试图创建一个文件,并将其进程 ID 写到该文件中。这使管理人员易于标识该进程。如果该文件已经加了锁,那么 lockfile 函数将失败,errno 设置为 EACCESEAGAIN,图中的函数返回1,表明该守护进程已在运行。否则将文件长度截断为0,将进程ID写入该文件,图中的函数返回0。

需要将文件长度截断为0,其原因是之前的守护进程实例的进程ID字符串可能长于调用此函数的当前进程的进程ID字符串。例如,若以前的守护进程的进程ID是12345,而新实例的进程ID是9999,那么将此进程ID写入文件后,在文件中留下的是99995。将文件长度截断为0就解决了此问题。


13.5 守护进程的惯例

在UNIX系统中,守护进程遵循下列通用惯例。

  • 若守护进程使用锁文件,那么该文件通常存储在 /var/run 目录中。然而需要注意的是,守护进程可能需要具有超级用户权限才能在此目录下创建文件。锁文件的名字通常是 name.pid,其中,name 是该守护进程或服务的名字。例如,cron 守护进程锁文件的名字是 /var/run/crond.pid

  • 若守护进程支持配置选项,那么配置文件通常存放在 /etc 目录中。配置文件的名字通常是 name.conf,其中,name 是该守护进程或服务的名字。例如,syslogd 守护进程的配置文件通常是 /etc/syslog.conf

  • 守护进程可用命令行启动,但通常它们是由系统初始化脚本之一(/etc/rc*/etc/init.d/*)启动的。

  • 若一个守护进程有一个配置文件,那么当该守护进程启动时会读该文件,但在此之后一般就不会再查看它。若某个管理员更改了配置文件,那么该守护进程可能需要被停止,然后再启动,以使配置文件的更改生效。为避免此种麻烦,某些守护进程将捕捉 SIGHUP 信号,当它们接收到该信号时,重新读配置文件。因为守护进程并不与终端相结合,它们或者是无控制终端的会话首进程,或者是孤儿进程组的成员,所以守护进程没有理由期望接收 SIGHUP。于是,守护进程可以安全地重复使用 SIGHUP


13.6 客户进程-服务器进程模型

守护进程常常用作服务器进程。一般而言,服务器进程等待客户进程与其联系,提出某种类型的服务要求。下面有关进程通信的几章中,将见到大量客户进程和服务器进程之间双向通信的实例。客户进程向服务器进程发送请求,服务器进程则向客户进程回送应答。

在服务器进程中调用 fork 然后 exec 另一个程序来向客户进程提供服务是很常见的。这些服务器进程通常管理着多个文件描述符:通信端点、配置文件、日志文件和类似的文件。最好的情况下,让子进程中的这些文件描述符保持打开状态并无大碍,因为它们很可能不会被在子进程中执行的程序所使用,尤其是那些与服务器端无关的程序。最坏情况下,保持它们的打开状态会导致安全问题——被执行的程序可能有一些恶意行为,如更改服务器端配置文件或欺骗客户端程序使其认为正在与服务器端通信,从而获取未授权的信息。

解决此问题的一个简单方法是对所有被执行程序不需要的文件描述符设置执行时关闭(close-on-exec)标志。图13-9展示了一个可以用来在服务器端进程中执行上述工作的函数。

1
2
3
4
5
6
7
8
9
10
include <fcntl.h>

int set_cloexec(int fd)
{
int val;
if((val = fcntl(fd, F_GETFD, 0)) < 0)
return -1;
val |= FD_CLOEXEC;
return (fcntl(fd,F_SETFD,val));
}