<!-- @page { margin: 0.79in } P { margin-bottom: 0.08in } -->
守护进程 (Daemon )是运行在后台的一种特殊进程。它独立于 控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进程。
Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器inetd ,Web 服务器httpd 等。同时,守护进程完成许多系统任务。比如,作业规划进程crond ,打印进程lpd 等。
守护进程的编程本身并不复杂,复杂的是各种版本的Unix 的实现机制不尽相同,
造成不同 Unix 环境下守护进程的编程规则并不一致。
需要注意,照搬某些书上的规则(特别是BSD4.3 和低版本的System V )到Linux 会出现错误的。
0. 基本概念
进程
. 每个进程都有一个父进程
. 当子进程终止时,父进程会得到通知并能取得子进程的退出状态。
进程组
. 每个进程也属于一个进程组
. 每个进程主都有一个进程组号,该号等于该进程组组长的PID 号
. 一个进程只能为它自己或子进程设置进程组ID 号
会话期
. 对话期(session) 是一个或多个进程组的集合。
.setsid() 函数可以建立一个对话期:
如果,调用setsid 的进程不是一个进程组的组长,此函数创建一个新的会话期。
(1) 此进程变成该对话期的首进程
(2) 此进程变成一个新进程组的组长进程。
(3) 此进程没有控制终端,如果在调用setsid 前,该进程有控制终端,那么与该终端的联系被解除。
如果该进程是一个进程组的组长,此函数返回错误。
(4) 为了保证这一点,我们先调用fork() 然后exit() ,此时只有子进程在运行,子进程继承了父进程的进程组ID ,但是进程PID 却是新分配的,所以不可能是新会话的进程组的PID 。从而保证了这一点。
1. 守护进程及其特性
(1) 守护进程最重要的特性是后台运行。在这一点上DOS 下的常驻内存程序TSR 与之相似。
(2) 其次,守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell )中继承下来的。
(3) 最后,守护进程的启动方式有其特殊之处。它可以在Linux 系统启动时从启动脚本/etc/rc.d 中启动,可以由作业规划进程crond 启动,还可以由用户终端(通常是 shell )执行。
总之,除开这些特殊性以外,守护进程与普通进程基本上没有什么区别。
因此,编写守护进程实际上是把一个普通进程按照上述的守护进程的特性改造成为守护进程 。
2. 守护进程的编程要点
前面讲过,不同Unix 环境下守护进程的编程规则并不一致。所幸的是守护进程的编程原则其实都一样,区别在于具体的实现细节不同。这个原则就是要满足守护进程的特性。
同时,Linux 是基于Syetem V 的SVR4 并遵循Posix 标准,实现起来与BSD4 相比更方便。编程要点如下;
(1). 在后台运行。
为避免挂起控制终端将Daemon 放入后台执行。方法是在进程中调用fork 使父进程终止,
让Daemon 在子进程中后台执行。
---------------------------------------------------------------------------------
if(pid=fork())
exit(0); // 是父进程,结束父进程,子进程继续
---------------------------------------------------------------------------------
(2). 脱离控制终端,登录会话和进程组
进程属于一个进程组,进程组号(GID )就是进程组长的进程号(PID )。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1 点的基础上,调用setsid() 使进程成为会话组长:
---------------------------------------------------------------------------------
setsid();
---------------------------------------------------------------------------------
说明:当进程是会话组长时setsid() 调用失败。但第一点已经保证进程不是会话组长。
setsid() 调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
(3). 禁止进程重新打开控制终端
现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。
可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:
---------------------------------------------------------------------------------
if(pid=fork())
exit(0); // 结束第一子进程,第二子进程继续(第二子进程不再是会话组长)
---------------------------------------------------------------------------------
(4). 关闭打开的文件描述符
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,
造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:
---------------------------------------------------------------------------------
for(i=0;i 关闭打开的文件描述符 close(i);
---------------------------------------------------------------------------------
(5). 改变当前工作目录
进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。
对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如: /tmp,使用:
---------------------------------------------------------------------------------
chdir("/tmp")
---------------------------------------------------------------------------------
但是一般是放在根目录下: "/"
(6). 重设文件创建掩模
进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:
---------------------------------------------------------------------------------
umask(0);
---------------------------------------------------------------------------------
(7). 处理SIGCHLD 信号
处理SIGCHLD 信号并不是必须的。
但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。
如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie )从而占用系统资源。
如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。
在Linux 下可以简单地将 SIGCHLD 信号的操作设为SIG_IGN 。
---------------------------------------------------------------------------------
signal(SIGCHLD,SIG_IGN);
---------------------------------------------------------------------------------
这样,内核在子进程结束时不会产生僵尸进程。
这一点与BSD4 不同,BSD4 下必须显式等待子进程结束才能释放僵尸进程。
3. 守护进程实例
守护进程实例包括两部分:主程序test.c 和初始化程序init.h 。主程序每隔一分钟向/tmp 目录中的日志test.log 报告运行状态。初始化程序 init.h 中的init_daemon 函数负责生成守护进程。
我们可以利用init_daemon 函数生成自己的守护进程。
(1). init.h文件
(2). test.c文件
=编译运行:
使用以下命令进行联合编译:
gcc init.h test.c
产生了一个程序比如叫a.out (默认的可执行文件名字,如果不指定编译输出文件名称就输出a.out)
可以运行此程序了./a.out了。
如果想要此程序在系统启动时自动运行,你可以在/etc/rc.d/rc.local里面用su命令加上一行,比如:
su - Jacky -c "/bin/a.out"
这个命令将以Jacky用户身份运行/bin/a.out程序
程序运行过程中如果你修改了程序的参数,则必然希望程序重新导入参数,那么你可以用ps命令查看程序的进程号:
ps -ef|grep a.out
然后用kill命令向程序发参数,比如:
kill -s SIGUSR1 11422
同理,如果想要终止程序,则向程序发SIGUSR2信号:
kill -s SIGUSR2 11422
当然也可以指定 编译输出文件 为test,使用 gcc -Wall -g init.h test.c -o test 或者 gcc -Wall -g -o test init.h test.c (-Wall 意思在编译和链接过程中显示所有警告信息 ,-g是方便GDB调试时使用的参数,一定注意-g在-o之前)