1.守护进程
1.1 何为守护进程
守护进程(Daemon)也称为精灵进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些事情的发生,主要表现为以下两个特点:
- 长期运行。
守护进程是一种生存期很长的一种进程,它们一般在系统启动时开始运行,除非强行终止,否则直到系统关机都会保持运行。与守护进程相比,普通进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但守护进程不受用户登录注销的影响,它们将会一直运行着、直到系统关机。 - 与控制终端脱离。
在 Linux 中,系统与用户交互的界面称为终端,每一个从终端开始运行的进程都会依附于这个终端,当控制终端被
关闭的时候,该会话就会退出,由控制终端运行的所有进程都会被终止,这使得普通进程都是和运行该进程的终端相绑定的;但守护进程能突破这种限制,它脱离终端并且在后台运行,脱离终端的目的是为了避免进程在运行的过程中的信息在终端显示并且进程也不会被任何终端所产生的信息所打断。
守护进程是一种很有用的进程。Linux 中大多数服务器就是用守护进程实现的,譬如,Internet 服务器inetd、Web 服务器 httpd 等。同时,守护进程完成许多系统任务,譬如作业规划进程 crond 等。
守护进程 Daemon,通常简称为 d,一般进程名后面带有 d 就表示它是一个守护进程。守护进程与终端无任何关联,用户的登录与注销与守护进程无关、不受其影响,守护进程自成进程组、自成会话,即pid=gid=sid。通过命令"ps -ajx"查看系统所有的进程,如下所示:
- TTY 一栏是问号?表示该进程没有控制终端,也就是守护进程;
- COMMAND 一栏使用中括号[]括起来的表示内核线程,这些线程是在内核里创建,没有用户空间代码,因此没有程序文件名和命令行,通常采用 k 开头的名字,表示 Kernel(内核)。
1.2 如何编写守护进程的程序
-
创建子进程、终止父进程
父进程调用 fork()创建子进程,然后父进程使用 exit()退出,这样做实现了下面几点。
第一,如果该守护进程是作为一条简单地 shell 命令启动,那么父进程终止会让 shell 认为这条命令已经执行完毕。
第二,虽然子进程继承了父进程的进程组ID,但它有自己独立的进程ID,这保证了子进程不是一个进程组的组长进程,这是下面将要调用 setsid 函数的先决条件! -
子进程调用 setsid 创建会话
调用 setsid()会使得子进程创建一个新的会话,子进程成为新会话的首领进程,同样也创建了新的进程组、子进程成为组长进程,此时创建的会话将没有控制终端。所以这里调用 setsid 有三个作用:
- 让子进程摆脱原会话的控制;
- 让子进程摆脱原进程组的控制;
- 让子进程摆脱原控制终端的控制;
tips:在调用 fork 函数时,子进程继承了父进程的会话、进程组、控制终端等,虽然父进程退出了,但原先的会话期、进程组、控制终端等并没有改变,因此,那还不是真正意义上使两者独立开来。setsid 函数能够使子进程完全独立出来,从而脱离所有其他进程的控制。
-
将工作目录更改为根目录
子进程是继承了父进程的当前工作目录,由于在进程运行中,当前目录所在的文件系统是不能卸载的,这对以后使用会造成很多的麻烦。因此通常的做法是让“/”作为守护进程的当前目录,当然也可以指定其它目录来作为守护进程的工作目录。 -
重设文件权限掩码 umask
文件权限掩码 umask 用于对新建文件的权限位进行屏蔽,由于使用 fork() 函数新建的子进程继承了父进程的文件权限掩码,这就给子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为 0,确保子进程有最大操作权限、这样可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是 umask,通常的使用方法为 umask(0)。 -
关闭不再需要的文件描述符
子进程继承了父进程的所有文件描述符,这些被打开的文件可能永远不会被守护进程(此时守护进程指的就是子进程,父进程退出、子进程成为守护进程)读或写,但它们一样消耗系统资源,可能导致所在的文件系统无法卸载,所以必须关闭这些文件,这使得守护进程不再持有从其父进程继承过来的任何文件描述符。 -
将文件描述符号为 0、1、2 定位到/dev/null
将守护进程的标准输入、标准输出以及标准错误重定向到/dev/null,这使得守护进程的输出无处显示、也无处从交互式用户那里接收输入。 -
其它:忽略 SIGCHLD 信号
处理 SIGCHLD 信号不是必须的,但对于某些进程,特别是并发服务器进程往往是特别重要的,服务器进程在接收到客户端请求时会创建子进程去处理该请求,如果子进程结束之后,父进程没有去 wait 回收子进程,则子进程将成为僵尸进程;如果父进程 wait 等待子进程退出,将又会增加父进程的负担、也就是增加服务器的负担,影响服务器进程的并发性能,在 Linux 下,可以将 SIGCHLD 信号的处理方式设置为
SIG_IGN,也就是忽略该信号,可让内核将僵尸进程转交给 init 进程去处理,这样既不会产生僵尸进程、又省去了服务器进程回收子进程所占用的时间。
下面举个守护进程的例子:(守护进程一般以单例模式运行)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
int main(void)
{
pid_t pid;
int i;
/* 创建子进程 */
pid = fork();
if (0 > pid) {
perror("fork error");
exit(-1);
}
else if (0 < pid)//父进程
exit(0); //直接退出
/*
*子进程
*/
/* 1.创建新的会话、脱离控制终端 */
if (0 > setsid()) {
perror("setsid error");
exit(-1);
}
/* 2.设置当前工作目录为根目录 */
if (0 > chdir("/")) {
perror("chdir error");
exit(-1);
}
/* 3.重设文件权限掩码 umask */
umask(0);
/* 4.关闭所有文件描述符 */
for (i = 0; i < sysconf(_SC_OPEN_MAX); i++)
close(i);
/* 5.将文件描述符号为 0、1、2 定位到/dev/null */
open("/dev/null", O_RDWR);
dup(0);
dup(0);
/* 6.忽略 SIGCHLD 信号 */
signal(SIGCHLD, SIG_IGN);
/* 正式进入到守护进程 */
for ( ; ; ) {
sleep(1);
puts("守护进程运行中......");
}
exit(0);
}
从上图可知,testApp 进程成为了一个守护进程,与控制台脱离,当关闭当前控制终端时,testApp 进程并不会受到影响,依然会正常继续运行;而对于普通进程来说,终端关闭,那么由该终端运行的所有进程都会被强制关闭,因为它们处于同一个会话。关于这个问题,大家可以自己去测试下,对比测试普通进程与守护进程,当终端关闭之后是否还在继续运行。
运行之后,没有任何打印信息输出,原因在于守护进程已经脱离了控制终端,它的打印信息并不会输出显示到终端,在代码中已经将标准输入、输出以及错误重定位到了/dev/null,/dev/null 是一个黑洞文件,自然是看不到输出信息。
使用"ps -ajx"命令查看进程;
本文参考正点原子的嵌入式LinuxC应用编程。