1. 概念
守护进程(daemon)是生存期长的一种进程。它们常常在系统引导装入时启动,仅在系统关闭时才终止。因为它们没有控制终端,所以说它们是在后台运行的。
Linux的大多数服务就是用守护进程实现的。这些守护进程名通常以d
结尾,如inetd提供网络服务,sshd提供ssh登录服务,httpd提供web服务等待。
2. 守护进程编程规则
编写守护进程程序时需要遵循一些基本规则,以防止产生不必要的交互作用。
-
调用umask将文件模式创建屏蔽字设置为一个已知值(通常是0)。由继承(如fork)得来的文件模式创建屏蔽字可能会被设置为拒绝某些权限。如果守护进程要创建文件,那么它可能要设置特定的权限。
-
调用fork,然后使父进程exit退出,这样会实现以下几点:
- 如果该守护进程是shell命令启动的,那么父进程终止会让shell认为这条命令已经执行完毕
- 虽然子进程继承了父进程的进程组ID,但是获得了一个新的进程ID,因此子进程不是该进程组的组长进程,这是接下来进行setsid调用的先决条件
-
调用setsid创建一个新会话,这样会使调用进程:
- 成为新会话的首进程
- 成为新进程组的组长进程
- 没有控制终端
有些建议此时再次调用fork,终止父进程,继续使用子进程中的守护进程,这就保证了该守护进程不是会话首进程,可以防止它取得控制终端。
为了避免取得控制终端的另一种方法是:当用open函数打开终端设备时,设置O_NOCTTY标志
-
将当前工作目录更改为根目录。
从父进程处继承过来的当前工作目录可能在一个挂载的文件系统处。因为是守护进程通常在系统再引导前一直存在,所以如果守护进程的当前工作目录在一个挂载文件系统中,那么该文件系统就不能被卸载。
-
关闭不再需要的文件描述符。这使得守护进程不再持有从其父进程继承来的任何文件描述符:可以通过getrlimit函数判定最高文件描述符值,并关闭直到该值的所有描述符。
-
某些守护进程打开/dev/null使文件描述符0/1/2指向该文件。这样使得任何一个试图读标准输入、写标准输出或标准错误的例程都不会产生任何效果。因为守护进程不与终端设备关联,因此其输出无处显式,也无处从交互式用户那里接收输入。
/dev/null文件:
一个字符设备文件。称为空设备,它丢弃一切写入其中的数据(但报告写入操作成功),读取它则会立即得到一个EOF。
/dev/null 被称为位桶(bit bucket)或者黑洞(black hole)。空设备通常被用于丢弃不需要的输出流,或作为用于输入流的空文件。这些操作通常由重定向完成。
/dev/zero文件:
一个字符设备文件。当你读它的时候,它会提供无限的空字符(NULL, 即0x00)。写入/dev/zero的内容会丢失不见。
/dev/random和/dev/urandom文件:
字符设备文件。随机数设备,提供不间断的随机字节流。二者的区别是/dev/random产生随机数据依赖系统中断,当系统中断不足时,/dev/random设备会“挂起”,因而产生数据速度较慢,但随机性好;/dev/urandom不依赖系统中断,数据产生速度快,但随机性较低。
**示例程序,初始化一个守护进程:**其中关于出错记录部分函数在下一节讲解
void daemonize(const char * cmd) {
//清空文件模式创建屏蔽字
umask(0);
//获取最大文件描述符
struct rlimit rl;
if(getrlimit(RLIMIT_NOFILE,&rl) < 0) {
perror("无法获取最大文件描述符");
}
//成为会话首进程并失去控制终端
pid_t pid;
if(fork() != 0) { // 父进程
exit(0);
}
setsid();
//确保该守护进程不会有控制终端
struct sigaction sa;
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGHUP,&sa,NULL); // 忽略SIGHUP信号
if(fork() != 0) {
exit(0);
}
//更改工作目录为根目录
chdir("/");
//更改所有打开的文件描述符
if(rl.rlim_max == RLIM_INFINITY)
rl.rlim_max = 1024;
for(int i = 0 ; i < rl.rlim_max ; ++i) {
close(i);
}
//将文件描述符0/1/2指向/dev/null
int fd0 = open("/dev/null",O_RDWR);
int fd1 = dup(0);
int fd2 = dup(0);
//初始化log文件
openlog(cmd,LOG_CONS,LOG_DAEMON);
if(fd0 != 0 || fd1 != 1 || fd2 !=2) {
syslog(LOG_ERR,"unexpected file descriptors %d %d %d",fd0,fd1,fd2);
exit(1);
}
}
3. 出错记录
3.1 syslog设施
因为守护进程不应该有控制终端,所以不能只是将出错消息写到标准错误上。我们不希望所有守护进程都写到控制台设备上,也不希望每个守护进程将它自己的出错消息写到一个单独的文件中。
因此需要关心哪一个守护进程写到哪一个记录文件中,可以通过一个集中的守护进程出错记录设施来进行这种管理操作。
大多数守护进程使用syslog设施,其组织结构如下
有以下3种产生日志消息的方法:
-
内核例程调用log函数向产生日志消息
-
大多数用户守护进程调用syslog函数产生日志消息,该函数将消息发送至UNIX域数据报套接字/dev/log。/dev/log是一个套接字类型文件
- 无论一个用户进程在此主机上,还是在通过TCP/IP网络连接到此主机的其他主机上,都可以将日志消息发送到UDP端口514
其中syslogd是一个守护进程。不同的进程(client)都可以将log 输送给syslogd(server),由syslogd 集中收集。syslogd可以将log保存到本地,也可以发送到共享内存或远程服务器。
syslogd守护进程读取所有3种格式的日志消息。syslogd在启动时读一个配置文件/etc/syslog.conf,该文件决定了不同种类消息应该送往何处。如一个紧急消息可在控制台上打印,而警告信息记录到一个文件中。
3.1 syslog设施的接口函数
void openlog(const char *ident, int option, int facility);
void syslog(int priority, const char *format, ...);
void closelog(void);
int setlogmask(int mask);
调用openlog是可选择的。如果不调用openlog,则在第一次调用syslog时,自动调用openlog。
closelog也是可选的,因为它只是关闭曾被用于与syslogd守护进程进行通信的描述符
openlog函数:
-
ident参数:此参数是一个字符串,将被加至每一则日志消息中。(类比perror函数)
-
option参数:指定的标志用来控制openlog()操作和syslog()的后续调用。他的值为下列值或运算的结果
- LOG_CONS:若日志消息不能通过UNIX域数据报套接字送至syslogd守护进程,则将该消息写至控制台
- LOG_NDELAY:立即打开至syslogd守护进程的UNIX域数据包套接字,不要等到第一条消息已经被记录时再打开。(通常在记录第一条消息前不打开该套接字文件)
- LOG_NOWAIT:在记录日志信息时,不等待可能的子进程的创建
- LOG_ODELAY:在第一条消息被记录之前延迟打开至syslogd守护进程的连接
- LOG_PERROR:除了将日志消息发送给syslogd以外,还将它写至标准错误stderr
- LOG_PID:每条消息都包含进程PID
-
facility参数:指定记录消息程序的类型。syslogd通过指定的配置文件,将以不同的方式来处理来自不同设施的消息(即这个要与syslogd守护进程的配置文件对应,日志信息会写入syslog.conf文件的指定位置)。
如果不调用openlog或者该参数值为0,则在调用syslog时,可以将facility参数作为syslog的priority参数的一部分。
syslog函数:产生一个日志消息
-
priority参数:其priority参数是facility和level的组合
-
format参数:其中%m字符被替换成errno值对应的出错消息字符串(strerror)
例如,在一个行式打印机假脱机守护进程中,可能有下列调用序列
openlog("lpd",LOG_PID,LOG_LPR);
syslog(LOG_ERR,"open error for %s:%m",filename);
若不调用openlog,则可能会是以下形式
syslog(LOG_LPR|LOG_ERR,"open error for %s:%m",filename);
setlogmask函数:设置进程的记录优先级屏蔽字,并返回之前的屏蔽字
-
mask参数:日志优先级掩码,在该掩码中的消息才会被真正记录。该掩码是level中各个常量的按位或
4. 单实例守护进程
有时候需要在任意时刻只运行该守护进程的一个副本。如果同时运行该守护进程的多个实例,则会出现错误。
可以通过文件和记录锁机制,该方法保证一个守护进程只有一个副本在运行。如果每一个守护进程创建一个有固定名字的文件,并在该文件的整体上加一个写锁,那么只允许创建一把这样的写锁。再此之后创建写锁的尝试都会失败,这向后续守护进程副本指明已有一个副本正在运行。
文件和记录锁提供了一种方便的互斥机制。如果守护进程在一个文件的整体上得到一把写锁,那么在该是守护进程终止时,这把锁将被自动删除。这就简化了复原所需的操作。
以下程序说明了如何使用文件和记录锁来保证只运行守护进程的一个副本
#define LOCKFILE "/var/run/daemon.pid"
#define LOCKMODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)
extern int lockfile(int);
int already_running(void) {
int fd = open(LOCKFILE,O_RDWR|O_CREAT,LOCKMODE);
if(fd < 0) {
syslog(LOG_ERR,"cannot open %s:%m",LOCKFILE);
exit(1);
}
if(lockfile(fd) < 0) {
if(errno == EACCES || errno == EAGAIN) {
//守护进程实例已经存在
close(fd);
return 1;
}
syslog(LOG_ERR,"cannot lock %s:%m",LOCKFILE);
exit(1);
}
//说明该进程是守护进程的唯一副本
ftruncate(fd,0);
char buf[16];
sprintf(buf,"%ld",(long)getpid());
write(fd,buf,strlen(buf)+1);
return 0;
}
该函数会使得守护进程将自己PID写入到指定文件中。如果该文件已经加了锁,那么lockfile函数将会返回失败,errno设为EACCES或EAGAIN(lockfile函数的具体实现见下一章)
5. 守护进程的惯例
-
若守护进程使用锁文件,那么该文件通常存储在/var/run目录中(/var 包括系统运行时要改变的数据)。守护进程可能需要超级用户权限才能在此目录下创建文件。锁文件的名字通常是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就重新启动该守护进程。
-
若一个守护进程有一个配置文件,那么当该守护进程启动时会读该文件,但在之后一般就不会再查看它。若某个管理员更改了配置文件,那么该守护进程可能需要被停止,然后再启动,以使配置文件生效。
为避免这种麻烦,某些守护进程将捕捉SIGHUP信号,当它们收到该信号时重新读配置文件。
6. 客户进程-服务器进程模型
守护进程通常被用作服务器进程。
例如syslogd进程就是服务器进程,而用户进程(客户进程)用UNIX域数据报套接字向其发送消息。syslogd服务器进程提供的服务就是将一条出错消息记录到日志文件中。
7. 补充:ls -l命令第一列结果
8. 补充:inittab文件
转自https://www.cnblogs.com/uestc-mm/p/11985696.html
Linux在完成核内引导(内核镜像已被载入内存,开始运行,并已初始化所有的设备驱动程序和数据结构等)之后,接着通过启动一个用户级程序init来启动其他用户级的进程或服务。
init程序需要读取配置文件/etc/inittab来作为进程运行的参数。inittab是一个不可执行的文本文件,它有若干行指令所组成。
/etc/inittab文件中每个登记项的结构都是一样的,共分为以冒号:
分隔的4个字段.具体如下:
<id>:<runlevels>:<action>:<process>
其中,各字段以及与其相关的说明如下:
id: 登记项标识符,最多为4个字符.用于唯一地标识/etc/inittab文件中的每一个登记项。
**run_level:**系统运行级,即执行登记项的init级别。用于指定相应的登记项适用于哪一个运行级,即在哪一个运行级中被处理。如果该字段为空,那么相应的登记项将适用于所有的运行级。在该字段中,可以同时指定一个或多个运行级:
Linux有7个运行级别,如下:
- 0:关机。
- 1:单用户字符界面。
- 2:不具备网络文件系统(NFS)功能的多用户字符界面。
- 3:具有网络功能的多用户字符界面。
- 4: 保留不用。
- 5:具有网络功能的图形用户界面。
- 6:重新启动系统。
action: 动作关键字。用于指定init命令或进程对相应进程(在“process”字段定义)所实施的动作。具体动作包括:
- ***boot:***只有在引导过程中,才执行该进程,但不等待该进程的结束;当该进程死亡时,也不重新启动该进程.
- ***bootwait:***只有在引导过程中,才执行该进程,并等待进程的结束:当该进程死亡时,也不重新启动该进程.实际上,只有在系统被引导后,并从单用户方式进入多用户方式时,这些登记项才被处理;如果系统的默认运行级设置为2(即多用户方式),那么这些登记项在系统引导后将马上被处理.
- ***initdefault:***指定系统的默认运行级.系统启动时,init将首先查找该登记项.如果存在init将据此决定系统最初要进入的运行级.具体来说,init将指定登记项“run_level"字段中的最大数字(即最高运行级)为当前系统的默认运行级;如果该字段为空,那么将其解释为“0123456”,并以“6”作为默认运行级.如果不存在该登记项,那么init将要求用户在系统启动时指定一个最初的运行级.
- ***off:***如果相应的进程正在运行,那么就发出一个警告信号,等待20秒后,再通过杀死信号强行终止该进程.如果相应的进程并不存在那么就忽略该登记项.
- ***once:***启动相应的进程,但不等待该进程结束便继续处理/etc/inittab文件中的下一个登记项;当该进程死亡时,init也不重新启动该进程.注意:在从一个运行级进入另一个运行级时,如果相应的进程仍然在运行,那么init就不重新启动该进程.
- ***ondemand:***与“respawn”的功能完全相同,但只用于运行级为a、b或c的登记项.
- ***powerfail:***只在init接收到电源失败信号时执行相应的进程,但不等待该进程结束.
- ***powerwait:***只在init接收到电源失败信号时执行相应的进程,并在继续对/etc/inittab文件进行任何处理前等待该进程结束.
- ***respawn:***如果相应的进程还不存在,那么init就启动该进程,同时不等待该进程的结束就继续扫描/etc/inittab文件;当该进程死亡时,init将重新启动该进程.如果相应的进程已经存在,那么init将忽略该登记项并继续扫描/etc/inittab文件.
- ***sysinit:***只有在启动或重新启动系统并首先进入单用户时,init才执行这些登记项.而在系统从运行级1-6进入单用户方式时,init并不执行这些登记项."action”字段为“sysinit”的登记项在“run_level”字段不指定任何运行级.
- ***wait:***启动进程并等待其结束,然后再处理/etc/inittab文件中的下一个登记项.
process: 所要执行的shell命令.任何合法的shell语法均适用于该字段.
9. daemon函数
用于将调用进程(其实是daemon函数中fork的子进程)变为守护进程,脱离控制台,在后台运行。
int daemon(int nochdir, int noclose);
其大致实现类似于前文的daemonize函数,父进程在fork后终止,子进程在daemon函数返回后成为守护进程,继续执行后面的指令。