守护进程,也即通常所说的 Daemon 进程,是 Linux 下一种特殊的后台服务进程,它独立于控制终端并且周期性的执行某种任务或者等待处理某些发生的事件。守护进程通常在系统引导时启动,在系统关闭时终止。Linux 系统下大多数服务都是通过守护进程实现的。守护进程的名称通常以 d 结尾,如 httpd、crond、mysqld等。
控制终端是什么?
终端是用户与操作系统进行交流的界面。在 Linux 系统中,用户由终端登录系统登入系统后会得到一个 shell 进程,这个终端便成为这个 shell 进程的控制终端(Controlling Terminal)。shell 进程启动的其他进程,由于复制了父进程的信息,因此也都同依附于这个控制终端。
从终端启动的进程都依附于该终端,并受终端控制和影响。终端关闭,相应的进程都会自动关闭。守护进程脱离终端的目的,也即是不受终端变化的影响不被终端打断,当然也不想在终端显示执行过程中的信息。
如果不想进程受到用户、终端或其他变化的影响,就必须把它变成守护进程。守护进程可以在 Linux 启动时从脚本 /etc/rc.d 启动,也可以由作业规划进程 crond 启动,还可以通过用户终端(一般是 Shell)启动。
守护进程属于 Linux 进程管理的范畴。其首要的特性是后台运行,其次,要与从启动它的父进程的运行环境隔离开来,需要处理的内容大致包括会话、控制终端、进程组、文件描述符、文件权限掩码以及工作目录等。
实现一个守护进程,其实就是将普通进程按照上述特性改造为守护进程的过程。需要注意的一点是,不同版本的 Unix 系统其实现机制不同,BSD 和 Linux 下的实现细节就不同。根据上述的特性,我们便可以创建一个简单的守护进程,这里以 Linux 系统下从终端 Shell 来启动为例。
编写守护进程第一步,就是要使得进程独立于终端后台运行。为避免终端挂起,将父进程退出,造成程序已经退出的假象,而后面的工作都在子进程完成,这样控制终端也可以继续执行其他命令,从而在形式上脱离控制终端的控制。
由于父进程先于子进程退出,子进程就变为孤儿进程,并由 init 进程作为其父进程收养。
经过上一步,子进程已经后台运行,然而系统调用 fork 创建子进程,子进程便复制了原父进程的进程控制块(PCB),相应地继承了一些信息,包括会话、进程组、控制终端等信息。尽管父进程已经退出,但子进程的会话、进程组、控制终端的信息没有改变。为使子进程完全摆脱父进程的环境,需要调用 setsid
函数(注意:组长进程调用 setsid ,则出错返回,无法新建会话。)。
这里有必要说一下两个概念:会话和进程组。
进程组
:一个或多个进程的集合。拥有唯一的标识进程组 ID,每个进程组都有一个组长进程,该进程的进程号等于其进程组的 ID。进程组 ID 不会因组长进程退出而受到影响,fork 调用也不会改变进程组 ID。会话
:一个或多个进程组的集合。新建会话时,当前进程(会话中唯一的进程)成为会话首进程,也是当前进程组的组长进程,其进程号为会话 ID,同样也是该进程组的 ID。它通常是登录 shell,也可以是调用 setsid 新建会话的孤儿进程。
通常,会话开始于用户登录,终止于用户退出,期间的所有进程都属于这个会话。一个会话一般包含一个会话首进程、一个前台进程组和一个后台进程组,控制终端可有可无;此外,前台进程组只有一个,后台进程组可以有多个,这些进程组共享一个控制终端。
- 前台进程组:该进程组中的进程可以向终端设备进行读、写操作(属于该组的进程可以从终端获得输入)。该进程组的 ID 等于控制终端进程组 ID,通常据此来判断前台进程组。
- 后台进程组:会话中除了会话首进程和前台进程组以外的所有进程,都属于后台进程组。该进程组中的进程只能向终端设备进行写操作。
下图为会话、进程组、进程和控制终端之间的关系(登录 shell 进程本身属于一个单独的进程组)。
到此为止,我们熟悉了会话与进程间的关系,那么如何新建一个会话呢?通过调用 setsid 函数可以创建一个新会话,调用进程担任新会话的首进程,其作用有:
- 使当前进程脱离原会话的控制
- 使当前进程脱离原进程组的控制
- 使当前进程脱离原控制终端的控制
这样,当前进程才能实现真正意义上完全独立出来,摆脱其他进程的控制。注意非组长进程调用 setsid 才能创建一个新会话,对应的变化有:
- 该进程变成新会话的首进程
- 该进程成为一个新进程组的组长进程
- 该进程没有控制终端,如果之前有,则会被中断(会话过程对控制终端的独占性)
也就是说:组长进程不能成为新会话首进程,新会话首进程必定成为组长进程。
使用fork创建的子进程继承了父进程的当前工作目录。由于在进程运行中,当前目录所在的文件系统(如“/mnt/usb”)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此通常的做法是让根目录("/")作为守护进程的当前工作目录,这样就可以避免上述的问题。
当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp。改变工作目录的常见函数是chdir
。
fork 函数创建的子进程,继承了父进程的文件操作权限,为防止对以后使用文件带来问题,需要重设文件权限掩码。
文件权限掩码,设定了文件权限中要屏蔽掉的对应位。这个跟文件权限的八进制数字模式表示差不多,将现有存取权限减去权限掩码(或做异或运算),就可产生新建文件时的预设权限。
调用 umask
设置文件权限掩码,通常是重设为 0,清除掩码,这样可以大大增强守护进程的灵活性。
同文件权限掩码一样,子进程可能继承了父进程打开的文件,而这些文件可能永远不会被用到,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下,因此需要一一关闭它们。由于守护进程脱离了终端运行,因此标准输入、标准输出、标准错误输出这3个文件描述符也要关闭。通常按如下方式来关闭:
首先一个简单的守护进程的例子:
#include <iostream>
#include <sys/stat.h>
#include <fcntl.h>
// 守护进程初始化函数
void init_daemon()
{
pid_t pid;
int i = 0;
if ((pid = fork()) == -1) {
printf("Fork error !\n");
exit(1);
}
if (pid != 0) {
exit(0); // 父进程退出
}
setsid(); // 子进程开启新会话,并成为会话首进程和组长进程
chdir("/tmp"); // 改变工作目录
umask(0); // 重设文件掩码
for (; i < getdtablesize(); ++i) {
close(i); // 关闭打开的文件描述符
}
return;
}
int main(int argc, char *argv[])
{
int fp;
time_t t;
char buf[] = {"This is a daemon: "};
char *datetime;
int len = 0;
// 初始化 Daemon 进程
init_daemon();
// 每隔一分钟记录运行状态
while (1) {
if (-1 == (fp = open("/tmp/daemon.log", O_CREAT|O_WRONLY|O_APPEND, 0600))) {
printf("Open file error !\n");
exit(1);
}
len = strlen(buf);
write(fp, buf, len);
t = time(0);
datetime = asctime(localtime(&t));
len = strlen(datetime);
write(fp, datetime, len);
close(fp);
sleep(60);
}
return 0;
}
首先运行守护进程,然后查看输出文件内容,最后用ps命令找到守护进程,将其杀死,如下图:
另外,有些 Unix 提供一个 daemon 的 C 库函数,实现守护进程。(BSD 和 Linux 均提供这个函数)