守护进程也称为精灵进程,是生存期较长的一种进程,常常在系统自举时启动,仅在系统关闭时终止。没有控制终端,仅仅在后台运行,Linux有很多守护进程执行日常事务活动。是不受终端控制的进程。想要脱离所有终端的原因是守护进程可能是从终端启动,在这之后这个终端要能用来执行其他任务,如果在某终端上启动了一个守护进程后从终端注销,那其他又从该终端登陆,任何守护进程的错误信息不应在后面用户的终端会话过程中出现。同样由终端上的一些键产生的信号也不应对以前从该终端上启动的任何守护进程造成影响。
守护进程编码规则
编写守护进程时遵循的基本规则,以便防止产生并不需要的交互作用:
1、 调用umask将文件模式创建屏蔽字设置为0。由继承的来的文件模式创建屏蔽字可能会拒绝设置某些权限。更高进程的文件模式创建屏蔽字并不影响父进程的屏蔽字。所有shell都有内置umask命令,用户可以设置umask值以控制所创建文件的默认权限,该值表示成八进制数,一位代表要屏蔽的权限。设置了相应位后,所对应的权限就会被拒绝。但是需要注意:对于文件来说,这一数字的最大值分别是6。系统不允许你在创建一个文本文件时就赋予它执行权限,必须在创建后用chmod命令增加这一权限。目录则允许设置执行权限,这样针对目录来 说,umask中各个数字最大可以到7。
-rw-rw-rw- 1 root root 0 Jul 22 13:59 mytest.txt
total 126404
drwxrwxrwx 2 root root 6 Jul 22 13:59 mytest.dir <<---目录的权限为777
-rw-rw-rw- 1 root root 0 Jul 22 13:59 mytest.txt <<---文件的权限为666
2、 调用fork函数,然后使父进程退出,目的是: 如果该守护进程是作为一条简单shell命令启动的,那么父进程终止是的shell认为这条命令已经执行完成。子进程继承父进程的进程组ID,但具有一个新的进程ID,保证了子进程不是一个进程组的组长进程,这是setsid调用的必要前提条件。
3、调用setsid以创建一个新的会话,使调用进程称为新会话的首进程,成为一个新进程组的组长进程,没有控制终端,这部分见下文的分析。
有关会话的概念是:一个或多个进程组的集合,这里涉及到进程组等多个概念,进程组是一个或多个进程的集合,通常与同一作业相关联,可以接收来自同一终端的各种信号。每个信号组有一个唯一的进程组ID,该ID也是一个正整数,并可以存放在pid_t中。getpgrp()可以返回调用进程的进程组ID。每个进程组都可以有一个组长进程,组长进程的标识是其进程组ID等于其进程ID。组长进程可以创建一个进程组,创建该组中的进程,然后终止,只要在某个进程组中有一个进程存在,该进程组就存在,与组长进程是否终止无关,从进程组创建开始到其中最后一个进程离开为止的时间区间称为进程组的生存期。进程组的最后一个进程可以终止或转移到另一个进程组。进程可以通过setpgid()来加入一个现有的组或创建一个新的进程组,其实用setsid()也可以创建一个新的进程组。setpgid(pid_t pid, pid_t pgid)。
一个进程只能为它自己活子进程设置进程组ID。在子进程调用exec函数之后,就不能改变该子进程的进程ID啦。通常在fork()之后调用此函数,使父进程设置子进程的进程组ID,并使子进程设置其自己的进程组ID。两个操作是冗余的,但让父子进程都这么做可以保证父子进程认为子进程已进入该进程组,由于父子进程运行先后次序的不确定,会造成一段时间内子进程的组成员身份不确定,产生竞争条件。
进程调用setsid()函数建立一个新会会话,如果调用该函数的进程不是一个进程组的组长,那该函数就创建了一个新会话,将发生如下的事情:
(1) 该进程变成新会话首进程,会话首进程通常是创建该会话的进程,该进程是新会话中的唯一的进程。
(2) 该进程成为一个新进程组的组长进程,新进程组ID就是调用进程的ID。
(3) 该进程没有控制终端,如果在调用setsid之前该进程有一个控制终端,那么这种联系将被中断。
以上就是指向setsid()之后的现象。
如果调用setsid的进程是一个进程组的组长,则该函数会返回出错,为了保证不发生这种错误,通常先调用fork,然后使其父进程终止,而子进程继续,因为子进程继承了父进程的进组ID。而其进程ID是新分配的,两个ID不同,保证了子进程不是进程组的组长。
通常将会话首进程的进程ID作为会话的ID,可以通过getsid获取会话首进程的进程组ID。
控制终端的概念:
(1) 一个会话可以有一个控制终端,通常是登陆在其上的终端设备或伪终端设备。
(2) 建立与控制终端连接的会话首进程称为控制进程。
(3) 一个会话中的介个进程组可被分成一个前台进程组以及一个或多个后台进程组。
(4) 如果一个会话有一个控制终端,则它有一个前台进程组,会话中的其他进程组则为后台进程组。
(5) 无论何时键入终端的中断键、退出键,就会将终端信号、退出信号发送给前台进程组的所有进程,因此需要知道那个进程组是前台进程组id,才能知道终端输入和终端产生的信号发送到何处,可通过tcgetpgrp(int fieldes)获取前台进程组,而tcsetpgrp(int fieldes, pid_t pgrpid)设置前台进程组。
(6) 如果终端接口检测到已经断开连接,则将挂断信号发送给控制进程(会话首进程),因此终端接口需要知道会话首进程的id才能知道发送的进程,可通过tcgetsid(int fieldes)获取。
通常登陆时将自动建立控制终端,不管标准输入、标准输出是否被重定向,程序都要与控制终端交互,保证程序能读写控制终端的方法是打开文件/dev/tty,内核中次文件是控制终端的同义词,若没有控制终端,打开该设备将失败。
通常在setsid()之后,会再次调用fork()。目的是确保守护进程将来即使打开一个终端设备,也不会自动获得终端,原因是没有控制终端的会话首进程打开终端设备时该终端会自动成为这个会话的控制终端,通过第二次的fork可以确保这次生成的子进程不再是一个会话的首进程,因此它不会获得控制终端。而在fork之前通常会忽略信号,这是因为会话首进程退出时会给该会话中的前台进程组(当打开控制终端后,就有一个前台进程组)的所有进程发送信号,而信号的默认处理函数通常是进程终止,这并不是希望的,因此需要对信号进程屏蔽处理。
4、将当前工作目录更改为根目录。
5、 关闭不再需要的文件描述符,这使得守护进程不再持有从其父进程继承来的某些文件描述符,可以通过getrlimit函数来判定最大文件描述符值,并关闭知道该值的所有描述符。
6、 某些守护进程打开/dev/null使其具有文件描述符0、1和2,因此任何一个试图读标准输入、写标准输出或标准错误的库都没有效果。因此守护进程并不与终端设备关联,并不能在终端设备上显示其输出,也无处从交互式用户接收输入。
守护进程没有控制终端,在发送问题时要用一些其他方式以输出消息,这些消息既有一般的通告消息,也有需管理员处理的紧急事件消息。syslog函数是输出这些消息的标准方式,它将消息发往syslogd守护进程。
syslogd守护进程
Unix系统通常会从一个初始化脚本中启动名为syslogd的守护进程,只要系统不停止,该服务一直运行,在启动时执行如下操作:
1、读入配置文件,通常是/etc/syslog.conf。设定守护进程对接收到每次键入的各种等级消息如何处理,消息可能写入一个文件,或发送给指定的用户,或转发给另一台主机上的syslogd进程。
2、创建Unix域套接口,给它绑定路径名/var/run/log。
3、创建UDP套接字,给它 捆绑端口514(syslog使用的端口号)
4、打开路径名/dev/klog,内核中的所有出错信息作为这个设备的输入出现。
然后syslogd进程运行一个无限循环,调用select等待三个描述字(2、3、4生成的描述字)变为可读,读入登记信息,并按照配置文件对消息进行处理。若接收到SIGHUP信号,会重新读入配置文件。
syslog函数
因为守护进程没有控制终端,不能fprintf到stderr上,守护进程为登记消息通常调用syslog函数。
void syslog(int priority, const char *message, ...);
priority参数是级别和设施的组合。message与printf所用的格式化字符串类型。增加了%m,打印出当前error对应的出错消息。
有关设施和级别的目的是允许在/etc/syslog.conf文件中进行配置,使得对相同设施的消息得到同样的处理,或使得相同级别的消息得到同样的处理。
当应用程序第一次调用syslog时,创建一个Unix域数据报套接口,然后调用connect连往syslogd守护进程建立的套接口/var/run/log。该套接口在进程终止前一直打开。
根据以上的规则编写了如下的daemond过程,如下所示:
void daemonize(const char *cmd)
{
int i, fd0, fd1, fd2;
pid_t pid;
struct rlimit r1;
struct sigaction sa;
umask(0); //清理文件创建掩码
if (getrlimit(RLIMIT_NOFILE, &r1) < 0) {
printf("error.\n");
exit(1);
}
if ((pid = fork()) < 0)
printf("fork failed.\n");
exit(1);
else if (pid != 0) { //让父进程退出
exit(0);
}
//子进程继续运行
setsid(); //构建新的会话进程,但该会话目前无控制终端,该进程成为会话首进程。
//信号屏蔽函数是解决会话首进程退出后,前台进程组收到会话首进程的SIGHUP信号,若不屏蔽会出现进程退出的问题。
sa.sa_handler = SIG_IGN;
sigemptyset(&sa_sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGHUP, &sa, NULL)) < 0) {
printf("sigaction error.\n");
exit(1);
}
/*
该fork的作用是防止守护进程打开控制终端,使得会话首进程获得控制终端。
子进程会在打开控制终端之后变为前台进程组的进程,确保不会是会话的会话首进程。
*/
if ((pid = fork() < 0) {
printf("fork failed.\n");
exit(1);
} else if (pid != 0) {
exit(0);
}
//子进程继续运行
if (chdir("/") < 0) {
printf("chdir error\n");
exit(1);
}
//关闭所有打开的文件描述符
if (r1.rlim_max == RLIM_INFINITY) {
r1.rlim_max = 1024;
}
for (i = 0; i < r1.rlim_max; i++) {
close(i);
}
//重定向0, 1, 2到/dev/null, fd从最小的开始分配,因此是0, 1, 2
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);
openlog(cmd, LOG_CONS, LOG_DAEMON);
if (fd0 !=0 || fd1 != 1 || fd2 != 2) {
syslog(LOG_ERR, "uexpected file descriptors %d %d %d", fd0, fd1, fd2);
exit(1);
}
}
阅读(9075) | 评论(0) | 转发(0) |