守护进程(daemon)是生存期长的一种进程。它们常常在系统引导装入时启动,仅在系统关闭时才终止。因为它们没有控制终端,所以说它们是在后台运行的。UNIX系统有很多守护进程,它们执行日常事务活动。
本章将说明守护进程结构,以及如何编写守护进程程序。因为守护进程没有控制终端,我们需要了解在出现问题时,守护进程如何报告出错情况。
有关守护进程这一术语被应用于计算机系统的历史背景,详见Raymond[1996]。
让我们先来看一些常用的系统守护进程,以及它们是怎样和第9章中叙述的进程组、控制终端和会话这三个概念相关联的。ps(1)命令打印系统中各个进程的状态。该命令有多个选项,有关细节请参考系统手册。为了解本节讨论中所需的信息,我们在基于BSD的系统下执行:
ps -axj
选项-a显示由其他用户所拥有的进程的状态,-x显示没有控制终端的进程状态,-j显示与作业有关的信息:会话ID、进程组ID、控制终端以及终端进程组ID。在基于System V的系统中,与此相类似的命令是ps -efj(为了提高安全性,某些UNIX系统不允许用户使用ps命令查看不属于自己的进程)。ps的输出大致是:
UID PID PPID PGID SID TTY COMD
root 1 0 1 1 ? /sbin/init
root 2 0 0 0 ? [kthreadd]
root 3 2 0 0 ? [ksoftirqd/0]
root 6 2 0 0 ? [migration/0]
root 7 2 0 0 ? [watchdog/0]
root 21 2 0 0 ? [cpuset]
root 22 2 0 0 ? [khelper]
root 26 2 0 0 ? [sync_supers]
root 27 2 0 0 ? [bdi-default]
root 29 2 0 0 ? [kblockd]
root 35 2 0 0 ? [kswapd0]
root 49 2 0 0 ? [scsi_eh_0]
root 256 2 0 0 ? [jbd2/sda5-8]
root 26464 1 26464 26464 ? rpcbind -w
root 14596 2 0 0 ? [flush-8:0]
root 13047 2 0 0 ? [kworker/1:0]
root 8196 1 8196 8196 ? /usr/sbin/sshd -D
daemon 1068 1 1068 1068 ? atd
root 1067 1 1067 1067 ? cron
root 1037 1 1037 1037 ? /usr/sbin/inetd
root 906 1 906 906 ? /usr/sbin/cupsd -F
syslog 847 1 843 843 ? rsyslogd -c5
root 257 2 0 0 ? [ext4-dio-unwrit]
statd 28490 1 28490 28490 ? rpc.statd -L
root 28561 1 28561 28561 ? rpc.idmapd
root 28554 2 0 0 ? [nfsiod]
root 28553 2 0 0 ? [rpciod]
root 28775 1 28775 28775 ? /usr/sbin/rpc.mountd --manage-gids
root 28764 2 0 0 ? [nfsd]
root 28761 2 0 0 ? [lockd]
其中,已移去了一些我们不感兴趣的列,如累计CPU时间。按照顺序,各列标题的意义分别是用户ID、进程ID、父进程ID、进程组ID、会话ID、终端名称以及命令字符串。
此ps命令在支持会话ID的系统(Linux 3.2.0)上运行,9.5节的setsid函数中曾提及会话ID。简单地说,它就是会话首进程的进程ID。但是,一些基于BSD的系统,如Mac OS X 10.6.8,将打印与本进程所属进程组对应的session结构的地址(见9.11节),而非会话ID的地址。
系统进程依赖于操作系统实现。父进程ID 为0 的各进程通常是内核进程,它们作为系统引导装入过程的一部分而启动。(init是个例外,它是一个由内核在引导装入时启动的用户层次的命令。)内核进程是特殊的,通常存在于系统的整个生命期中。它们以超级用户特权运行,无控制终端,无命令行。的服务。rsyslogd守护进程可以被由管理员启用的将系统消息记入日志的任何程序使用。可以在一台
rpcbind守护进程提供将远程过程调用(Remote Procedure Call,RPC)程序号映射为网络端口号
在ps 的输出实例中,内核守护进程的名字出现在方括号中。该版本的 Linux使用一个名为kthreadd 的特殊内核进程来创建其他内核进程,所以 kthreadd 表现为其他内核进程的父进程。对于需要在进程上下文执行工作但却不被用户层进程上下文调用的每一个内核组件,通常有它自己的内核守护进程。例如,在Linux中:
• kswapd守护进程也称为内存换页守护进程。它支持虚拟内存子系统在经过一段时间后将脏页面慢慢地写回磁盘来回收这些页面。
• flush守护进程在可用内存达到设置的最小阈值时将脏页面冲洗至磁盘。它也定期地将脏页面冲洗回磁盘来减少在系统出现故障时发生的数据丢失。多个冲洗守护进程可以同时存在,每个写回的设备都有一个冲洗守护进程。输出实例中显示出一个名为flush-8:0的冲洗守护进程。从名字中可以看出,写回设备是通过主设备号(8)和副设备号(0)来识别的。
•sync_supers守护进程定期将文件系统元数据冲洗至磁盘。
•jbd守护进程帮助实现了ext4文件系统中的日志功能。
进程1通常是init(Mac OS X中是launchd),8.2节对此做过说明。它是一个系统守护进程,除了其他工作外,主要负责启动各运行层次特定的系统服务。这些服务通常是在它们自己拥有的守护进程的帮助下实现的。实际的控制台上打印这些消息,也可将它们写到一个文件中。(13.4节将对syslog设施进行说明。)
9.3节已谈到inetd守护进程。它侦听系统网络接口,以便取得来自网络的对各种网络服务进程的请求。nfsd、nfsiod、lockd、rpciod、rpc.idmapd、rpc.statd和rpc.mountd守护进程提供对网络文件系统(Network File System,NFS)的支持。注意,前4个是内核守护进程,后3个是用户级守护进程。
cron守护进程在定期安排的日期和时间执行命令。许多系统管理任务是通过cron每隔一段固定的时间就运行相关程序而得以实现的。atd守护进程与cron类似,它允许用户在指定的时间执行任务,但是每个任务它只执行一次,而非在定期安排的时间反复执行。cupsd 守护进程是个打印假脱机进程,它处理对系统提出的各个打印请求。sshd守护进程提供了安全的远程登录和执行设施。
注意,大多数守护进程都以超级用户(root)特权运行。所有的守护进程都没有控制终端,其终端名设置为问号。内核守护进程以无控制终端方式启动。用户层守护进程缺少控制终端可能是守护进程调用了setsid的结果。大多数用户层守护进程都是进程组的组长进程以及会话的首进程,而且是这些进程组和会话中的唯一进程(rsyslogd 是一个例外)。最后,应当引起注意的是用户层守护进程的父进程是init进程。
在编写守护进程程序时需遵循一些基本规则,以防止产生不必要的交互作用。下面先说明这些规则,然后给出一个按照这些规则编写的函数daemonize。
(1)首先要做的是调用umask将文件模式创建屏蔽字设置为一个已知值(通常是0)。由继承得来的文件模式创建屏蔽字可能会被设置为拒绝某些权限。如果守护进程要创建文件,那么它可能要设置特定的权限。例如,若守护进程要创建组可读、组可写的文件,继承的文件模式创建屏蔽字可能会屏蔽上述两种权限中的一种,而使其无法发挥作用。另一方面,如果守护进程调用的库函数创建了文件,那么将文件模式创建屏蔽字设置为一个限制性更强的值(如 007)可能会更明智,因为库函数可能不允许调用者通过一个显式的函数参数来设置权限。
(2)调用fork,然后使父进程exit。这样做实现了下面几点。第一,如果该守护进程是作为一条简单的shell命令启动的,那么父进程终止会让shell认为这条命令已经执行完毕。第二,虽然子进程继承了父进程的进程组 ID,但获得了一个新的进程 ID,这就保证了子进程不是一个进程组的组长进程。这是下面将要进行的setsid调用的先决条件。
(3)调用setsid创建一个新会话。然后执行9.5节中列出的3个步骤,使调用进程:(a)成为新会话的首进程,(b)成为一个新进程组的组长进程,(c)没有控制终端。
在基于System V的系统中,有些人建议在此时再次调用fork,终止父进程,继续使用子进程中的守护进程。这就保证了该守护进程不是会话首进程,于是按照System V规则(见9.6节)可以防止它取得控制终端。为了避免取得控制终端的另一种方法是,无论何时打开一个终端设备,都一定要指定O_NOCTTY。
(4)将当前工作目录更改为根目录。从父进程处继承过来的当前工作目录可能在一个挂载的文件系统中。因为守护进程通常在系统再引导之前是一直存在的,所以如果守护进程的当前工作目录在一个挂载文件系统中,那么该文件系统就不能被卸载。
或者,某些守护进程还可能会把当前工作目录更改到某个指定位置,并在此位置进行它们的全部工作。例如,行式打印机假脱机守护进程就可能将其工作目录更改到它们的spool目录上。
(5)关闭不再需要的文件描述符。这使守护进程不再持有从其父进程继承来的任何文件描述符(父进程可能是 shell 进程,或某个其他进程)。可以使用 open_max 函数(见 2.17 节)或getrlimit函数(见7.11节)来判定最高文件描述符值,并关闭直到该值的所有描述符。
(6)某些守护进程打开/dev/null使其具有文件描述符0、1和2,这样,任何一个试图读标准输入、写标准输出或标准错误的库例程都不会产生任何效果。因为守护进程并不与终端设备相关联,所以其输出无处显示,也无处从交互式用户那里接收输入。即使守护进程是从交互式会话启动的,但是守护进程是在后台运行的,所以登录会话的终止并不影响守护进程。如果其他用户在同一终端设备上登录,我们不希望在该终端上见到守护进程的输出,用户也不期望他们在终端上的输入被守护进程读取。
实例
图13-1所示的函数可由一个想要初始化为守护进程的程序调用。
图13-1 初始化一个守护进程
若daemonize函数由main程序调用,然后main程序进入休眠状态,那么可以用ps命令检查该守护进程的状态:
$ ./a.out
$ ps -efj
UID PID PPID PGID SID TTY CMD
sar 13800 1 13799 13799 ? ./a.out
$ ps -efj | grep 13799
sar 13800 1 13799 13799 ? ./a.out
我们也可用ps命令验证,没有活动进程存在的ID是13799。这意味着,守护进程在一个孤儿进程组中(见 9.10 节),它不是会话首进程,因此没有机会被分配到一个控制终端。这一结果是在daemonize函数中执行第二个fork造成的。可以看出,守护进程已经被正确地初始化了。
守护进程存在的一个问题是如何处理出错消息。因为它本就不应该有控制终端,所以不能只是简单地写到标准错误上。我们不希望所有守护进程都写到控制台设备上,因为在很多工作站上控制台设备都运行着一个窗口系统。我们也不希望每个守护进程将它自己的出错消息写到一个单独的文件中。对任何一个系统管理人员而言,如果要关心哪一个守护进程写到哪一个记录文件中,并定期地检查这些文件,那么一定会使他感到头痛。所以,需要有一个集中的守护进程出错记录设施。
BSD syslog 设施是在伯克利开发的,广泛应用于 4.2BSD。从 BSD 派生的很多系统都支持syslog。在 SVR4 之前,System V 中从来没有一个集中的守护进程记录设施。在 Single UNIX Specification的XSI扩展中包括了syslog函数。
自4.2BSD以来,BSD的syslog设施得到了广泛的应用。大多数守护进程都使用这一设施。图13-2显示了syslog设施的详细组织结构。
图13-2 BSD的syslog设施
有以下3种产生日志消息的方法。
(1)内核例程可以调用 log 函数。任何一个用户进程都可以通过打开(open)并读取(read)/dev/klog设备来读取这些消息。因为我们无意编写内核例程,所以不再进一步说明此函数。
(2)大多数用户进程(守护进程)调用syslog(3)函数来产生日志消息。我们将在下面说明其调用序列。这使消息被发送至UNIX域数据报套接字/dev/log。
(3)无论一个用户进程是在此主机上,还是在通过TCP/IP网络连接到此主机的其他主机上,都可将日志消息发向UDP端口514。注意,syslog函数从不产生这些UDP数据报,它们要求产生此日志消息的进程进行显式的网络编程。
关于UNIX域套接字以及UDP套接字的细节,请参阅Stevens、Fenner和Rudoff[2004]。
通常,syslogd守护进程读取所有3种格式的日志消息。此守护进程在启动时读一个配置文件,其文件名一般为/etc/syslog.conf,该文件决定了不同种类的消息应送向何处。例如,紧急消息可发送至系统管理员(若已登录),并在控制台上打印,而警告消息则可记录到一个文件中。
该设施的接口是syslog函数。
#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守护进程进行通信的描述符。
调用openlog 使我们可以指定一个ident,以后,此ident将被加至每则日志消息中。ident一般是程序的名称(如cron、inetd)。option参数是指定各种选项的位屏蔽。图13-3介绍了可用的option(选项)。若在Single UNIX Specification的openlog定义中包括了该选项,则在XSI列中用一个黑点表示。
图13-3 openlog的option参数
openlog的facility参数值选取自图13-4。注意,Single UNIX Specification只定义了facility所有参数值中的一个子集,该子集一般只能用在一个给定的平台上。设置facility参数的目的是可以让配置文件说明,来自不同设施的消息将以不同的方式进行处理。如果不调用openlog,或者以facility为0来调用它,那么在调用syslog时,可将facility作为priority参数的一个部分进行说明。
调用syslog产生一个日志消息。其priority参数是facility和level的组合,它们可选取的值分别列于facility(见图13-4)和level(见图13-5)中。level值按优先级从最高到最低依次排列。
图13-4 openlog的facility参数
图13-5 syslog中的level(按序排列)
将format参数以及其他所有参数传至vsprintf函数以便进行格式化。在format中,每个出现的%m字符都先被代换成与errno值对应的出错消息字符串(strerror)。
setlogmask函数用于设置进程的记录优先级屏蔽字。它返回调用它之前的屏蔽字。当设置了记录优先级屏蔽字时,各条消息除非已在记录优先级屏蔽字中进行了设置,否则将不被记录。注意,试图将记录优先级屏蔽字设置为0并不会有什么作用。
很多系统也将logger(1)程序作为向syslog设施发送日志消息的方法。虽然Single UNIX Specification 没有定义任何可选参数,但某些实现允许将该程序的可选参数指定为 facility、level和ident。logger命令是专门为以非交互方式运行的需要产生日志消息的shell脚本设计的。
实例
在一个(假定的)行式打印机假脱机守护进程中,可能包含有下面的调用序列:
openlog("lpd", LOG_PID, LOG_LPR);
syslog(LOG_ERR, "open error for %s: %m", filename);
第一个调用将ident字符串设置为程序名,指定该进程ID要始终被打印,并且将系统默认的facility设定为行式打印机系统。对 syslog 的调用指定一个出错条件和一个消息字符串。如若不调用openlog,则第二个调用的形式可能是:
syslog(LOG_ERR | LOG_LPR, "open error for %s: %m", filename);
其中,将priority参数指定为level和facility的组合。
除了syslog,很多平台还提供它的一种变体来处理可变参数列表。
#include <syslog.h>
#include <stdarg.h>
void vsyslog(int priority, const char *format, va_list arg);
本书说明的所有4种平台都提供vsyslog,但Single UNIX Specification中并不包括它。注意,如果要使它的声明对应用程序可见,可能需要定义一个额外的符号,例如,在FreeBSD中定义__BSD_VISIBLE或在Linux中定义__USE_BSD。
大多数syslog实现将使消息短时间处于队列中。如果在此段时间中有重复消息到达,那么syslog 守护进程不会把它写到日志记录中,而是会打印输出一条类似于“上一条消息重复了N次”的消息。
为了正常运作,某些守护进程会实现为,在任一时刻只运行该守护进程的一个副本。例如,这种守护进程可能需要排它地访问一个设备。对cron守护进程而言,如果同时有多个实例运行,那么每个副本都可能试图开始某个预定的操作,于是造成该操作的重复执行,这很可能导致出错。
如果守护进程需要访问一个设备,而该设备驱动程序有时会阻止想要多次打开/dev 目录下相应设备节点的尝试。这就限制了在一个时刻只能运行守护进程的一个副本。但是如果没有这种设备可供使用,那么我们就需要自行处理。
文件和记录锁机制为一种方法提供了基础,该方法保证一个守护进程只有一个副本在运行。(文件和记录锁将在14.3节中讨论。)如果每一个守护进程创建一个有固定名字的文件,并在该文件的整体上加一把写锁,那么只允许创建一把这样的写锁。在此之后创建写锁的尝试都会失败,这向后续守护进程副本指明已有一个副本正在运行。
文件和记录锁提供了一种方便的互斥机制。如果守护进程在一个文件的整体上得到一把写锁,那么在该守护进程终止时,这把锁将被自动删除。这就简化了复原所需的处理,去除了对以前的守护进程实例需要进行清理的有关操作。
实例
图13-6所示的函数说明了如何使用文件和记录锁来保证只运行一个守护进程的一个副本。
图13-6 保证只运行一个守护进程的一个副本
守护进程的每个副本都将试图创建一个文件,并将其进程 ID 写到该文件中。这使管理人员易于标识该进程。如果该文件已经加了锁,那么lockfile函数将失败,errno设置为EACCES或EAGAIN,图13-6中的函数返回1,表明该守护进程已在运行。否则将文件长度截断为0,将进程ID写入该文件,图13-6中的函数返回0。
需要将文件长度截断为0,其原因是之前的守护进程实例的进程ID字符串可能长于调用此函数的当前进程的进程ID字符串。例如,若以前的守护进程的进程ID是12345,而新实例的进程ID是9999,那么将此进程ID写入文件后,在文件中留下的是99995。将文件长度截断为0就解决了此问题。
在UNIX系统中,守护进程遵循下列通用惯例。
•若守护进程使用锁文件,那么该文件通常存储在/var/run目录中。然而需要注意的是,守护进程可能需要具有超级用户权限才能在此目录下创建文件。锁文件的名字通常是name.pid,其中,name是该守护进程或服务的名字。例如,cron守护进程锁文件的名字是/var/run/crond.pid。
•若守护进程支持配置选项,那么配置文件通常存放在/etc目录中。配置文件的名字通常是name.conf,其中,name是该守护进程或服务的名字。例如,syslogd守护进程的配置文件通常是/etc/syslog.conf。
•守护进程可用命令行启动,但通常它们是由系统初始化脚本之一(/etc/rc*或/etc/init.d/*)启动的。如果在守护进程终止时,应当自动地重新启动它,则我们可在/etc/inittab中为该守护进程包括respawn记录项,这样,init就将重新启动该守护进程。(假定系统使用System V风格的init命令。)
•若一个守护进程有一个配置文件,那么当该守护进程启动时会读该文件,但在此之后一般就不会再查看它。若某个管理员更改了配置文件,那么该守护进程可能需要被停止,然后再启动,以使配置文件的更改生效。为避免此种麻烦,某些守护进程将捕捉SIGHUP信号,当它们接收到该信号时,重新读配置文件。因为守护进程并不与终端相结合,它们或者是无控制终端的会话首进程,或者是孤儿进程组的成员,所以守护进程没有理由期望接收SIGHUP。于是,守护进程可以安全地重复使用SIGHUP。
实例
图13-7所示的程序说明了守护进程可以重读其配置文件的一种方法。该程序使用sigwait以及多线程,对此我们已经在12.8节讨论过。
图13-7 守护进程重读配置文件
该程序调用了图13-1中的daemonize来初始化守护进程。从该函数返回后,调用图13-6中的already_running函数以确保该守护进程只有一个副本在运行。到达这一点时,SIGHUP信号仍被忽略,所以需恢复对该信号的系统默认处理方式;否则调用sigwait的线程决不会见到该信号。
如同对多线程程序所推荐的那样,阻塞所有信号,然后创建一个线程处理信号。该线程的唯一工作是等待SIGHUP和SIGTERM。当接收到SIGHUP信号时,该线程调用reread函数重读它的配置文件。当它接收到SIGTERM信号时,会记录消息并退出。
回顾图10-1,SIGHUP和SIGTERM的默认动作是终止进程。因为我们阻塞了这些信号,所以当SIGHUP和SIGTERM的其中一个被发送到守护进程时,守护进程不会消亡。作为替代,调用sigwait的线程在返回时将指示已接收到该信号。
实例
并非所有守护进程都是多线程的。图 13-8 中的程序说明一个单线程守护进程如何捕捉SIGHUP并重读其配置文件。
图13-8 守护进程重读配置文件的另一种实现
在初始化守护进程后,我们为SIGHUP和SIGTERM配置了信号处理程序。可以将重读逻辑放在信号处理程序中,也可以只在信号处理程序中设置一个标志,并由守护进程的主线程完成所有的工作。
守护进程常常用作服务器进程。确实,我们可以称图13-2中的syslogd进程为服务器进程,用户进程(客户进程)用UNIX域数据报套接字向其发送消息。
一般而言,服务器进程等待客户进程与其联系,提出某种类型的服务要求。图 13-2 中,由syslogd服务器进程提供的服务是将一条出错消息记录到日志文件中。
图13-2中,客户进程和服务器进程之间的通信是单向的。客户进程向服务器进程发送服务请求,服务器进程则不向客户进程回送任何消息。在下面有关进程通信的几章中,我们将见到大量客户进程和服务器进程之间双向通信的实例。客户进程向服务器进程发送请求,服务器进程则向客户进程回送应答。
在服务器进程中调用fork然后exec另一个程序来向客户进程提供服务是很常见的。这些服务器进程通常管理着多个文件描述符:通信端点、配置文件、日志文件和类似的文件。最好的情况下,让子进程中的这些文件描述符保持打开状态并无大碍,因为它们很可能不会被在子进程中执行的程序所使用,尤其是那些与服务器端无关的程序。最坏情况下,保持它们的打开状态会导致安全问题——被执行的程序可能有一些恶意行为,如更改服务器端配置文件或欺骗客户端程序使其认为正在与服务器端通信,从而获取未授权的信息。
解决此问题的一个简单方法是对所有被执行程序不需要的文件描述符设置执行时关闭(close-on-exec)标志。图13-9展示了一个可以用来在服务器端进程中执行上述工作的函数。
图13-9 设置执行时关闭标志
在大多数UNIX系统中,守护进程是一直运行的。为了初始化我们自己的进程,使之作为守护进程运行,需要一些审慎的思索并理解第9章中说明的进程之间的关系。本章开发了一个可由守护进程调用的能对其自身正确初始化的函数。
因为守护进程通常没有控制终端,所以本章还讨论了守护进程记录出错消息的几种方法。我们讨论了在大多数UNIX系统中,守护进程遵循的若干惯例,给出了几个如何实现某些惯例的实例。
13.1 从图13-2可以推测出,直接调用openlog或第一次调用syslog都可以初始化syslog设施,此时一定要打开用于 UNIX 域数据报套接字的特殊设备文件/dev/log。如果调用openlog前,用户进程(守护进程)先调用了chroot,结果会怎么样?
13.2 回顾13.2节中ps 输出的示例。唯一一个不是会话首进程的用户层守护进程是rsyslogd进程。请解释为什么rsyslogd守护进程不是会话首进程。
13.3 列出你系统中所有有效的守护进程,并说明它们各自的功能。
13.4 编写一段程序调用图13-1中daemonize函数。调用该函数后,它已成为守护进程,再调用getlogin(见8.15节)查看该进程是否有登录名。将结果打印到一个文件中。