摘要:针对Linux环境下的守护进程daemon,分析了一般性守护进程的编写方法,并提出若干见解,通过总结归纳进而为设计和开发守护进程提供了有意的参考,给出了基于Linux守护进程实现的主要思想。
关键词: 守护进程;信号量;控制终端
1 引言
Linux在启动时需要启动很多系统服务,它们向本地和网络用户提供了Linux的系统功能接口,直接面向应用程序和用户。提供这些服务的程序是由运行在后台的守护进程(Daemons)来执行的。
编写守护进程实际上是把一个普通进程按照守护进程的特性进行改造。比如,网络通信服务中的守护进程需要能同时接受多个请求,它不断地在侦听端等待远程的 连接请求,收到请求后,创建一个子进程,让其负责与远端的通信,而自己则继续返回侦听。子进程和父进程间的通信采用消息机制,因此守护进程的开发涉及到子 进程、进程组、会晤期、信号量、文件权限、目录和控制终端等多个概念。本文主要分析守护进程的概念、实现原理以及编写守护进程的方法。
2 Linux进程结构
2.1 进程的创建
Linux使用fork()函数来创建一个子进程,当fork()调用失败时系统返回-1。一旦子进程被创建,父子进程一起从fork()处继续执行, 相互竞争系统的资源。如果希望子进程继续执行,而父进程阻塞直到子进程完成任务,这时候可以调用wait()或者waitpid()系统调用。
pid_t wait(int *stat_loc);
pid_t waitpid(pid_t pid,int *stat_loc,int options);
wait()系统调用会使父进程阻塞直到一个子进程结束或者是父进程接受到了一个信号。如果没有父进程或者没有子进程或者它的子进程已经结束了,wai()会立即返回。成功时(因一个子进程结束)wait()将返回子进程的ID,否则返回-1。
2.2 守护进程的创建
守护进程最重要的特性是后台运行,因此守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符、控制终端、会话和进程组、工作目录以及 文件创建掩码等。这些环境是守护进程从执行它的父进程(特别是Shell)中继承下来的。以下程序使用一个INIT_DAEMON宏来实现守护进程的初始 化工作。
#define INIT_DAEMON
{
if (fork()>0)
exit(0);// 是父进程,结束父进程
else if(fork()<0)exit(1);// fork失败,退出
setsid(); // 第一子进程成为新的进程组长,并与控制终端分离
if(fork()>0) exit(0);//是第一子进程,结束第一子进程
else if(fork<0) exit(1);// fork失败,退出。
} //是第二子进程,继续,第二子进程不再是会话组长
第一次调用fork函数,为避免挂起,控制终端将守护进程放入后台执行,然后调用setsid()函数脱离控制终端和进程组,使该进程成为会话组长,并 与原来的登录会话和进程组脱离。此时进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端。为了避免这种情况,可以通过使进程不再成为会话组 长来禁止进程重新打开控制终端,这就需要第二次调用fork()函数。父进程(会话组长)退出,子进程继续执行,并不再拥有打开控制终端的能力。在正在执 行的进程中调用INIT_DAEMON后,进程将成为守护进程,脱离控制终端进入后台执行。
2.3 信号量机制
为了防止在守护进程没有正常运转起来时,控制终端受到干扰退出或挂起,需要将以下有关控制终端操作的信号屏蔽。
signal(SIGTTOU, SIG_IGN);// 后台进程写控制终端
signal(SIGTTIN, SIG_IGN);// 后台进程读控制终端
signal(SIGTSTP, SIG_IGN);// 终端挂起
signal(SIGHUP, SIG_IGN);// 进程组长退出时向所有会议成员发出
当信号出现时,开发人员可以要求系统进行以下三种操作:
(1)忽略信号。大多数信号都是采取这种方式进行处理,但是对SIGKILL和SIGSTOP信号不能做忽略处理。
(2)捕捉信号。最常见的情况是,如果捕捉到SIGCHID信号,则表示子进程已经终止。可在此信号的捕捉函数中调用waitpid()函数取得该子进程的进程ID和它的终止状态。
(3)执行系统的默认动作。对绝大多数信号而言,系统的默认动作都是终止该进程。
3 守护进程开发准则
3.1 控制终端
首先使用ps命令打印系统中各个进程的状态。
所有守护进程都以超级用户(用户ID为0)的优先权运行。没有一个守护进程具有控制终端,终端名称设置为问号(?)、终端前台进程组ID设置为-1。缺少控制终端是守护进程调用了setsid()的结果。
3.2 进程组
进程组是一个或多个进程的集合。进程组ID类似于进程ID,可存放在 pid_t 数据类型中。每个进程组有一个组长进程,组长进程的标识是其进程组ID等于其进程ID。进程组组长可以创建一个进程组,创建该组中的进程,然后终止。只要 在某个进程组中有一个进程存在,则该进程就存在,这与其组长进程是否终止无关。
3.3 会话期
会话期(Session)是一个或 多个进程组的集合。在一个会话期中有3个进程组,通常是有Shell的管道线将几个进程编成一组。一个会话期可以有一个单独的控制终端,即在其上登录的终 端设备(终端登录)或伪终端设备(网络登录),但这个控制终端并不是必需的。如果一个会话期有一个控制终端,则它有一个前台进程组,其他进程组为后台进程 组。
3.4 脱离控制终端,登录会话和进程组
登录会话可以包含多个进程组,这些进程组共享一个控制终端,这个控制终端通常是创建 进程的控制终端。登录会话和进程组通常是从父进程继承下来的。需要说明的是,当进程是会话组长时,setsid()调用将失败,2.2已经保证进程不是会 话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离,由于会话过程对控制终端的独占性,进程同时 与控制终端脱离。具体是操作就是:
(1)成为新对话期的首进程;
(2)成为一个新进程组的首进程;
(3)没有控制终端。
守护进程要摆脱从父进程继承下来的控制终端它们影响,可以使用setsid()函数设置新会话的领头进程,并与原来的登录会话和进程组脱离。这只是其中的一种方法,还可以使用如下处理方法:
if ((fd = open("/dev/tty",O_RDWR)) >= 0)
{ioctl(fd,TIOCNOTTY,NULL);
// 其中/dev/tty是一个流设备,也是终端映射
close(fd); } // 调用close()函数将终端关闭
3.5 关闭文件描述符
进程从创建它的父进程那里继承了打开的文件描述符,如果守护进程留下一个处于打开状态的普通文件,将阻止该文件被任何其他进程从文件系统中删除。一般来 说,必要的是关闭0、1、2三个文件描述符,即标准输入(STDINT)、标准输出(STDOU)、标准错误(STDERR)。关闭不必要的连接甚至更为 重要,因为在该终端上的用户退出系统后,将执行vhangup()系统调用,守护进程访问该终端的权利于是被撤消。这表示守护进程虽有它以为处于打开状态 的文件描述符,事实上已不再能通过这些文件描述符访问该终端。关闭三者的代码如下:
for (fd = 0, fdtablesize = getdtablesize(); fd < fdtablesize; fd++)
// fdtablesize是一个进程一次可以打开进程的最大数
close(fd); // 关闭打开的文件描述符,包括标准输入、标准输出和标准错误输出
如果程序想保留0、1、2三个文件描述符,那么循环应绕过这三者。实现代码如下:
for (fd =3, fdtablesize = getdtablesize();fd < fdtablesize; fd++)
close(fd);
4 结束语
守护进程广泛应用于Linux/Unix环境下的系统管理、网络通信以及嵌入式应用等领域。本文分析了Linux守护进程的结构与实现原理,所欠缺的是 构建程序的通用化,原因是存在不同环境之间切换并执行不同的任务,同时还必须考虑其他系统之间的所有差异,今后的工作主要集中在标准化以及简化异构环境中 的管理任务的方法上。
beyes | 2010-05-16 02:09 |
Linux 守护进程的编程方法 3.Linux下守护进程的创建有很多的方法,比如我们可以使用cron,inetd等程序来创建。这里我们介绍在控制终端上有用户来启动的守护程序。这种守护程序不依赖于任何一个终端,不会随着用户的退出而结束。这种程序经常用于网络程序之中。 -----------------------------------------------------------
原型 pid_t fork(void); 创建子进程
头文件
#include <sys/types.h>
#include <unistd.h>
描述
利用fork创建的子进程仅与父进程在PID和PPID上有不同,并且实事上资源的使用也是各自独立的,都是通过相对地址访问的(逻辑首地址都是0)。 文件locks和pending signals不被继承。
返回值
如果成功, 在父进程中执行的则返回子进程的PID,在子进程中执行的则返回0 ;如果失败,在父进程下将返回-1,并且不会有子进程被创建。
-----------------------------------------------------------
将一个程序变为守护程序一般按照下面的步骤:
调用函数fork,然后父进程退出,这样子进程就变成了后台进程了。同时子进程变成为进程组的组长(组长可能是父进程或者是创建父进程的进程)为第二步系统调用setsid做准备。
调用setsid创建一个新的会议组,进程成为一个新的会议组的组长。这样这个会议组就不再控制终端了。
-----------------------------------------------------------
setsid();
描述
在新的session中运行一个程序。
-----------------------------------------------------------
添加信号SIGHUP的处理。为后面的会议组长退出作处理。同时再一次调用fork。然后父进程退出。由于进程组长退出时向所有会议成员发出SIGHUP,所以我忽略这个信号。这样我们的这个进程就没有控制终端了。
调用函数chdir将进程的工作目录改到根目录(/)。这样我们的程序可以不占用某一个可卸载的设备。防止root卸载设备时系统报告设备忙。
关闭所有的文件描述符,同时重定向3个标准文件描述符。
下面的程序将创建一个守护程序,这个程序重定向了标准输出,标准输入,和标准错误输出。每分钟向标准错误输出可用的系统内存页数。
////////////////////////// fock_test1.c /////////////////////////////
#include <unistd.h>
#include <signal.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <time.h>
#include <fcntl.h>
int main(int argc, char **argv)
{
struct sigaction act;
int error, in, out;
time_t now;
int memory;
/* 父进程退出 */
if(fork() != 0) exit(1);
/* 创建一个新的会议组 */
if(setsid() < 0) exit(1);
/* 忽略信号SIGHUP */
act.sa_handler = SIG_IGN;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if(sigaction(SIGHUP, &act, NULL) == -1) exit(1);
/* 子进程退出,孙进程没有控制终端了 */
if(fork() != 0) exit(1);
if(chdir("/") == -1) exit(1);
/* 标准错误重定向 */
error = open("/tmp/error", O_WRONLY|O_CREAT, 0600);
dup2(error, STDERR_FILENO);
close(error);
/* 标准输入重定向 */
in = open("/tmp/in", O_RDONLY|O_CREAT, 0600);
if(dup2(in, STDIN_FILENO) == -1) perror("in");
close(in);
/* 标准输出重定向 */
out = open("/tmp/out", O_WRONLY|O_CREAT, 0600);
if(dup2(out, STDOUT_FILENO) == -1) perror("out");
close(out);
while(1)
{
time(&now);
memory = sysconf(_SC_AVPHYS_PAGES);
fprintf(stderr, "时间/t%s/t/t可用内存[%d]/n", ctime(&now), memory);
sleep(10);
}
exit(0);
}
运行这个程序后,使用ps -x命令可以看到这个程序的tty是?表示这个程序没有控制终端。
程序运行过程中如果你修改了程序的参数,则必然希望程序重新导入参数,那么你可以用ps命令查看程序的进程号:
ps -ef|grep fock_test1
如果想要终止程序(2296这个值视本地系统而异):
kill 2296
|