守护进程
理论
「守护进程」是 Linux 的一种长期运行的后台服务进程,也有人称它为「精灵进程」。我们常见的 httpd、named、sshd 等服务都是以守护进程 Daemon 方式运行的,通常服务名称以字母d结尾,也就是 Daemon 第一个字母。与普通进程相比它大概有如下特点:
- 无需控制终端(不需要与用户交互)
- 在后台运行
- 生命周期比较长,一般是随系统启动和关闭
守护进程是个特殊的孤儿进程,这种进程脱离终端。为什么要脱离终端呢?
之所以脱离于终端是为了避免进程被任何终端所产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。由于在 Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。
如何查看守护进程
- a 表示不仅列当前用户的进程,也列出所有其他用户的进程
- x 表示不仅列有控制终端的进程,也列出所有无控制终端的进程
- j 表示列出与作业控制相关的信息
从上图可以看出守护进行的一些特点: - 守护进程基本上都是以超级用户启动( UID 为 0 )
- 没有控制终端( TTY 为 ?)
- 终端进程组 ID 为 -1 ( TPGID 表示终端进程组 ID)
一般情况下,守护进程可以通过以下方式启动:
- 在系统启动时由启动脚本启动,这些启动脚本通常放在 /etc/rc.d 目录下;
- 利用 inetd 超级服务器启动,如 telnet 等;
- 由 cron 定时启动以及在终端用 nohup 启动的进程也是守护进程。
如何编写守护进程?
(1) 屏蔽一些控制终端操作的信号
这是为了防止守护进行在没有运行起来前,控制终端受到干扰退出或挂起。
signal(SIGTTOU,SIG_IGN);
signal(SIGTTIN,SIG_IGN);
signal(SIGTSTP,SIG_IGN);
signal(SIGHUP ,SIG_IGN);
(2) 生成一个进程,使之脱离控制终端、登录会话和进程组
- 创建子进程,父进程退出。
- 程序中的父进程一般是一个进程组的组长,fork()产生子进程(将来会生成守护进程)
- 由于在调用fork()函数时,子进程全盘复制了父进程的会话期、进程组和控制终端等,虽然父进程退出了,但原先的会话期、进程组和控制终端等并没有改变,因此,还不是真正意义上的独立;
- 由于父进程已经先于子进程退出,会造成子进程没有父进程,从而变成一个孤儿进程。在Linux中,每当系统发现一个孤儿进程,就会自动由1号进程收养。原先的子进程就会变成init进程的子进程。
- 为什么要父进程退出:进程组组长无法调用
setsid
- 调用setsid(),用于生成一个新的会话 注意如果当前进程是会话组长时,调用失败。第一点已经可以保证进程不是会话组长了,所以setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话对控制终端的独占性,进程同时与控制终端脱离;
// 2.1)创建子进程,父进程退出
if( pid=fork() ){ // 父进程
exit(0); //结束父进程,子进程继续
}else if(pid< 0){ // 出错
perror("fork");
exit(EXIT_FAILURE);
}
//以下是子进程
// 2.2)脱离控制终端、登录会话和进程组
setsid();
关于 setsid
• 首先内核会创建一个新的会话,并让该进程成为该会话的leader进程,
• 同时伴随该session的建立,一个新的进程组也会被创建,同时该进程成为该进程组的组长。
• 该进程此时还没有和任何控制终端关联。若需要则要另外调用tcsetpgrp
(3) 禁止进程重新打开控制终端
- 进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端。(只有会话组长才能打开控制终端)
// 3)禁止进程重新打开控制终端
if( pid=fork() ){ // 父进程(第一个子进程执行fork产生第一个进程的孙进程)
exit(0); // 结束第一子进程
}else if(pid< 0){ // 出错
perror("fork");
exit(EXIT_FAILURE);
}
//第二子进程继续(第二子进程不再是会话组长)
(4) 关闭打开的文件描述符
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误;
// NOFILE 为 <sys/param.h> 的宏定义
// NOFILE 为文件描述符最大个数,不同系统有不同限制
for(i=0; i< NOFILE; ++i){// 关闭打开的文件描述符
close(i);
}
(5) 改变当前工作目录
使用fork()创建的子进程继承了父进程的当前工作目录。
由于在进程运行过程中,当前目录所在的文件系统(如“/mnt/usb”等)是不能卸载的,这对以后的使用会造成诸多的麻烦(如系统由于某种原因要进入单用户模式)。
因此,通常的做法是让“/”作为守护进程的当前工作目录,这样就可以避免上述问题。当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp。改变工作目录的常见函数是chdir()。
chdir("/tmp");
(6) 重设文件创建掩模
进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0);
umask(0);
(7) 处理 SIGCHLD 信号
但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源(关于僵尸进程的更多详情, 请看《特殊进程之僵尸进程》)。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在 Linux 下可以简单地将 SIGCHLD 信号的操作设为 SIG_IGN 。关于信号的更详细用法, 请看《信号中断处理》。
signal(SIGCHLD, SIG_IGN);
总结:
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/syslog.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int init_daemon(void)
{
int pid;
int i;
// 1)屏蔽一些控制终端操作的信号
signal(SIGTTOU,SIG_IGN);
signal(SIGTTIN,SIG_IGN);
signal(SIGTSTP,SIG_IGN);
signal(SIGHUP ,SIG_IGN);
// 2)在后台运行
if( pid=fork() ){ // 父进程
exit(0); //结束父进程,子进程继续
}else if(pid< 0){ // 出错
perror("fork");
exit(EXIT_FAILURE);
}
//以下为第一个子进程的操作;
// 3)脱离控制终端、登录会话和进程组
setsid();
// 4)禁止进程重新打开控制终端
if( pid=fork() ){ // 第一子进程调用fork生成孙进程
exit(0); // 结束第一子进程
}else if(pid< 0){ // 出错
perror("fork");
exit(EXIT_FAILURE);
}
//第二子进程(孙进程)继续(第二子进程不再是会话组长),以下为第二子进程的操作;
// 5)关闭打开的文件描述符
// NOFILE 为 <sys/param.h> 的宏定义
// NOFILE 为文件描述符最大个数,不同系统有不同限制
for(i=0; i< NOFILE; ++i){
close(i);
}
// 6)改变当前工作目录
chdir("/tmp");
// 7)重设文件创建掩模
umask(0);
// 8)处理 SIGCHLD 信号
signal(SIGCHLD,SIG_IGN);
return 0;
}
int main(int argc, char *argv[])
{
init_daemon();
while(1);
return 0;
}
SIGTERM
一个daemon通常是在系统关闭时才终止。可以在系统关闭时执行特定的应用程序的脚本来停止的。
因为在系统关闭的时候 init 进程会向所有其子进程发送SIGTERM信号。默认情况下,SIGTERM信号会终止一个进程。如果daemon在终止之前需要做些清理工作,那么就需要为这个信号建立一个处理器。这个处理器必须能快速地完成清理工作,因为 init 在发完 SIGTERM 信号的 5 秒之后会发送一个 SIGKILL 信号。(这并不意味着这个daemon 能够执行 5 秒的 CPU 时间,因为 init 会同时向系统中的所有进程发送信号,而它们可能都试图在 5 秒内完成清理工作。
由于 daemon 是长时间运行的,因此要特别小心潜在的内存泄露问题和文件描述符泄露
很多 daemon 需要确保同一时刻只有一个实例处于活跃状态。如让两个 cron daemon 都试图实行计划任务毫无意义。
使用 SIGHUP 重新初始化一个 daemon
由于很多 daemon 需要持续运行,因此在设计 daemon 程序时需要克服一些障碍。
- 通常 daemon 会在启动时从相关的配置文件中读取操作参数,但有些时候需要在不重启 daemon 的情况下快速修改这些参数。
- 一些 daemon 会产生日志文件。如果 daemon 永远不关闭日志文件的话,那么日志文件就会无限制地增长,最终会阻塞文件系统。这里需要有一种机制来告诉 daemon 关闭其日志文件并打开一个新文件,这样就能够在需要的时候旋转日志文件了
logrotate 程序可以用来自动旋转 daemon 的日志文件,具体可参考 logrotate(8)手册。
解决这两个问题的方案是让 daemon 为 SIGHUP 建立一个处理器,并在收到这个信号时采取所需的措施。由于 daemon 没有控制终端,因此内核永远不会向 daemon 发送这个信号。这样 daemon就可以使用 SIGHUP 信号来达到目的。
总结
daemon是一个长时间运行并且没有控制终端的进程(即它运行在后台)。daemon执行特定的任务,如提供一个网络登录工具或服务 Web 页面。一个程序要成为 daemon 需要按序执行一组步骤,包括调用 fork()和 setsid()。
daemon应该在合适的地方处理SIGTERM和SIGHUP信号。SIGTERM信号的处理方式应该是按序关闭这个 daemon,而SIGHUP信号则是提供了一种机制让daemon通过读取器配置文件并重新打开所使用的所有日志文件来重新初始化自身