继僵尸与孤儿进程之后,我们终于迎来了一个充满正能量的进程,但是很可惜,它仍旧是一个孤儿进程。但守护进程用途很广泛,大多数的Linux服务器都是用守护进程来实现的,比如Internet服务器inetd,Web服务器httpd等。
守护进程的基本特性
守护进程也称·精灵进程
(Daemon),是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
Linux系统启动时会启动很多系统服务进程,这些系统服务进程没有控制终端,不能直接和用户交互。这些系统服务进程不受用户登录注销的影响,它们一直运行着,这些系统服务进程就是守护进程。
下面我们通过一个终端命令来查看一下系统(CentOS)的守护进程:
ps axj | grep -E "d$"
对上述命令的解释为:
①ps:表示对进程监测和控制。
②参数a:表示不仅列出当前用户的进程,也列出所有其他用户的进程。
③参数x:表示不仅列出控制终端的进程,也列出所有无控制终端的进程。
④参数j:表示列出与作业控制相关的信息。
⑤grep -E “d$”:表示递归匹配以字符d结尾的信息。
从上图的监测筛选结果可以看出,上述进程均属于守护进程,它的特点如下:
1)PPID为1(上图红色框):守护进程的父进程为1,即init进程,守护进程为孤儿进程。
2)PID|PGID|SIG相同(上图黄色框):守护进程自成进程组,自成会话,不受用户登录或注销影响。
3)TPGID为-l(上图粉色框):守护进程没有控制终端的进程,并且它与终端无任何关系。
4)一般以-d结尾(上图橘色框):表示Daemon,可有可无。
5)生命周期7*24:守护进程永不关闭,因为大多数服务器都是借助守护进程来实现的,服务器只能重启,不能关机。
除此之外,在command一列用[]括起来的名字表示 内核线程
,这些线程在内核里创建,没有用户空间代码,因此没有程序文件名和命令行
,通常采用以k
开头的名字,表示Kernal。udevd负责维护/dev目录下的设备文件
;acpid负责电源管理
;syslogd负责维护/var/log下的日志文件。
守护进程的创建规则
在编写守护进程程序时需要遵循一些基本的规则,以便防止产生并不需要的交互作用,它的·创建规则
如下:
⑴ 首先调用umask将文件模式创建屏蔽字(掩码)设置为0。
原因在于:由继承得来的文件模式创建屏蔽字可能会拒绝设置某些权限。比如守护进程要创建一个可读可写的文件,而继承的文件模式创建屏蔽字可能屏蔽了写权限,导致功能缺失。
⑵ 调用fork(),然后使父进程退出(exit)。
原因在于:第一,如果该守护进程是作为一条简单的shell命令启动的,那么父进程退出使得shell认为这条命令已经执行完毕。第二,子进程继承了父进程的进程组id,但具有一个新的进程id,保证了子进程不是一个进程组的组长(下面会提到原因) 。
**⑶ 调用setsid创建一个新会话,并成为Session Leader。**它的原型如下:
#include<unistd.h>
pid_t setsid(void);
返回值:调用成功返回新建会话的id(当前进程的id),出错返回-1。
注意:调用setsid函数之前,当前进程不允许是进程组的Leader(组长),否则返回-1。
成功调用该函数的结果如下:
① 创建一个新的Session。当前进程成为Session Leader,当前进程的id就是Session的id。
② 创建一个新的进程组。当前进程成为进程组的Leader,当前进程的id就是进程组的id。
③ 如果当前进程原本有一个控制终端,则它失去了这个控制终端,成为一个没有控制终端的进程。(所谓失去控制终端指原来的控制终端仍然是打开的,仍然可以读写,但只是一个普通的打开文件而不是控制终端)。
⑷ 将当前工作目录更改为根目录。使用chdir修改当前的工作目录。
原因在于:从当前父进程继承来的当前工作目录可能在一个装配文件系统中,因为守护进程通常在系统再引导之前是一直存在的,如果当前守护进程目录在一个装配文件系统中,那么该文件系统就不能被卸载。
⑸ 关闭不再需要的文件描述符。
原因在于:可以使得守护进程不再持有从其父进程继承来的某些文件描述符。
⑹ 其他:忽略SIGCHLD信号。子进程退出时不再向父进程发送SIGCHLD信号。
下面我们遵循以上规则来创建一个守护进程,测试代码如下:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void mydaemon()
{
umask(0);//缺省值清0
pid_t id = fork();
if(id > 0){//father
exit(1);
}
setsid();//设置新会话
chdir("/");//更改工作目录为根目录
close(0);//关闭0、1、2文件描述符
close(1);
close(2);
signal(SIGCHLD,SIG_IGN);//忽略SIGCHLD信号
}
int main()
{
mydaemon();
while(1){
sleep(1);
}
return 0;
}
结果与我们上面的预期一致,守护进程的父进程的pid为1,自成进程组,自成会话,且与控制终端无关。
我们也可以通过守护进程的进程号,cd /proc目录下查看相关信息:
在/proc目录下有很多关于文件系统的信息,比如CPU信息(cpuinfo),内存信息(meminfo),磁盘信息(diskstats)等。
我们的Linux也提供了一个标准的创建守护进程的函数daemon,它的原型如下:
#include<unistd.h>
int daemon(int nochdir,int noclose);
参数nochdir:为0时表示当前工作目录变为根目录,否则不变。
参数noclose:为0时表示将标准输入、输出、错误输出重导向为/dev/null,它是Linux下的黑洞,写入的信息会被kernal丢弃,永远写不满。
返回值:成功返回0,失败返回-1并设置errno。
下面我们将两个参数都设置为0,观察一下效果:
守护进程的两次fork
有些open source采用的依旧是一次fork,当然,两次fork是鉴于安全性的考虑,在开始正题之前,我们先模拟一下两次fork,通过观察结果来阐述为什么尽量要fork两次?
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void mydaemon()
{
umask(0);//缺省值清零
if(fork() > 0){//第一次fork
exit(1);
}
setsid();//设置新会话
signal(SIGCHLD,SIG_IGN);//忽略SIGCHLD信号
if(fork() > 0){//第二次fork
exit(2);
}
chdir("/");//更改工作目录为/目录
close(0);
close(1);
close(2);
}
int main()
{
mydaemon();
while(1){
sleep(1);
}
return 0;
}
从结果可以最主要的区别是:进程的pid与gid、sid不同,这表明守护进程是属于某个会话,属于某个进程组。而脱离了一次fork自成会话,自成进程组的理论。那么这样做的目的何在呢?
第一次fork
:我们使得子进程称为一个独立的会话,称为了一个孤儿进程,被init收养,也就脱离了控制终端。
第二次fork
:当前子进程已经称为了会话组长,则可以去打开控制终端,当我们再次fork时,子进程的id与sid不再相同,即不再是会话组的组长,因此也就无法打开新的控制终端(打开一个控制终端的前提是该进程必须是会话组长)。