5、 守护进程
守护进程(daemon) 是生存期长的一种进程。它们常常在系统引导装入时启动,仅在系统关闭时才终止。因为它们没有控制终端,所以说它们是在后台运行的。UNIX系统有很多守护进程,它们执行日常事务活动。
5.1 编程规则
(1) 调用umask
首先要做的是调用umask将文件模式创建屏蔽字设置为一个已知值(通常是0)。因为继承来的umask 是linux系统的一般是0022,当设置为有 r 权限时,便会被屏蔽掉,那么这并不是我们想得到的结果
(2) 调用fork,然后父进程exit
这样做实现了下面几点:第一,如果该守护进程是作为一条简单的shell命令启动的,那么父进程终止会让shell认为这条命令已经执行完毕。第二,虽然子进程继承了父进程的进程组ID,但获得了一个新的进程ID,这就保证了子进程不是一个进程组的组长进程。 这是下面要进行setsid调用的先决条件。
(3) 调用setsid创建一个新会话
如果调用此函数的进程_不是一个进程组的组长_, 则函数创建一个新会话。具体会发生一下3件事
(1) 该进程变成新会话的会话首进程(session leader, 会话首进程是创建该会话的进程)。此时,该进程是新会话中的唯一进程。
(2) 该进程称为一个新进程组的组长进程。 新进程组ID是该调用进程的进程ID
(3) 该进程没有控制终端。如果在调用setsid之前该进程有一个控制终端,那么这种联系也被切断
如果该调用进程已经是一个进程组的组长,则次函数返回出错。 为了保证不处于这种情况,通常先调用fork然后服进行exit,而子进程继续。 因为子进程继承了父进程的进程组ID,而其进程ID是新分配的,两者不可能相等,这就保证了子进程不是一个进程组的组长
会话 是一个或多个进程组的集合。
因为父进程是进程组的组长,那么其已经有了一个会话,而子进程也在这个会话中;
而我们需要摆脱原来的独立出来
让进程摆脱原会话,原进程组,原终端的控制
(4) 将当前工作目录更改为根目录
执行chdir("/") 将当前工作目录更改为根目录
从父进程继承过来的当前工作目录可能在一个挂载的文件系统中。例如守护进程的工作目录是在u盘中,但是当u盘一被弹出程序也就不能正常运行了。
(5)关闭不再需要的文件描述符
关闭stdin/stdout/stderr, 因为这些默认都是输出到终端的。而守护进程并不需要控制终端。
关于为什么需要使用setsid
setsid,创建新的session,因为session leader调用setsid不会创建新session,所以我们才需要之前的那个fork。
那session到底是什么?当你通过控制台登录时,你就创建了一个session,一个session就是一个controlling terminal、一个controlling process group,再加上一堆后台process group,其中controlling process一般就是login shell,controlling terminal就是你在敲键盘看显示器的那个“终端”。 在shell中,你可以用"&"将一个命令在后台运行,或者你可以按Ctrl-Z,然后用bg命令将其放入后台,这时你可以用jobs命令看到后台运行的进程,这些后台进程就是background process group
看看当你logout的时候会发生什么,此时controlling terminal会被关闭,这个session中所有进程都会收到SIGHUP和SIGTERM/SIGQUIT,对于这些信号的缺省操作就是结束进程。
例如使用"&" 运行一个后台进程,当退出终端时,此时这个会话终止,这个会话中的所有进程接收到SIGHUP和SIGTERM/SIGQUIT信号,然后进程被结束
作为一个daemon,你当然不希望用户logout的时候就退出,解决方案有几种,一种就是忽略上述所有信号,例如nohup程序干的就是这个,但这样做有个小问题,很多程序使用信号作为一种简单的IPC机制,忽略这些信号会导致这种IPC失效;另外一种方案就是让进程从这个controlling terminal上脱离,setsid将会创建一个新的session,使当前进程成为新session的leader,并且不再关联之前session的controlling terminal。
3、第二次fork,这件事情有点晦涩,其实很多文档上并没说的太清楚,具体原因是这样的,即使一个进程创建了新的session,它依然有可能获得一个controlling terminal,比如你可以用ioctl(TIOCSCTTY),这样做的后果就是,新的session有了controlling terminal,然后前面我们提到的所有问题依然可能发生。为了彻底解决这个问题,我们需要第二次fork,因为ioctl(TIOCSCTTY)手册中在非常不起眼的地方提到了,只有session leader才能为session打开controlling terminal,第二次fork之后,子进程就是session中第二个进程,它这辈子再没机会成为session leader了(除非它调用setsid),也就再没能力打开controlling terminal了。这样我们就一劳永逸的解决了所有问题。
为了避免取得控制终端的另一种方法是,无论何时打开一个终端设备都一定要指定O_NOCTTY,如果成为了终端设备那么当终端退出的时候,程序也会退出
所以,如果你的程序行为很固定,你知道它不会无聊到去打开controlling terminal的话,第二次fork其实是不必要的,但是如果你是在写一个库,而且不知道使用它的程序到底会有什么样的行为,你最好还是再fork一次。
守护进程用例
#include<stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <stdlib.h>
void daemonize(const char *cmd)
{
pid_t pid;
int fd;
struct rlimit rl;
umask(0);
if ((pid = fork()) < 0)
{
fprintf(stderr,"%s can't fork", cmd);
exit(-1);
}
if (pid != 0)
exit(0);
setsid();
chdir("/");
if ((pid = fork()) < 0)
{
fprintf(stderr, "%s can't fork\n", cmd);
exit(-1);
}
if (pid != 0)
exit(0);
if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
{
fprintf(stderr, "%s can't get file limit\n", cmd);
exit(-1);
}
for (int i = 0; i < rl.rlim_max; i++)
close(i);
fd = open("dev/null", O_RDWR);
dup(fd);
dup(fd);
}
int main(int argc,char *argv[])
{
pid_t pid;
daemonize("hello");
while(1){
sleep(100);
}
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)