本文主要是介绍daemon进程原理及实现 信号 终端 为何两次Fork,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
APUE 上的定义如下: 守护进程也称 daemon 进程,是生存期较长的一种进程,它们常常在系统自举时启动,仅在系统关闭时才终止。因为它们没有控制终端,所以说它们是再后台运行的。【Daemon 功能实现】
首先,讲解下 daemon 实现的基本原则。事实上,编写守护进程程序时是存在一些基本规则的,目的是防止产生不需要的交互作用(比如与终端的交互)。规则如下:
- 调用 umask 将文件模式创建屏蔽字设置为 0 。原因:防止继承得来的文件模式创建屏蔽字会拒绝设置某些权限的情况。
- 调用 fork ,然后使父进程退出(exit)。原因:第一,令启动 daemon 进程的 shell 认为命令已经执行完毕;第二,令产生的子进程不是其所在进程组的组长。
- 调用 setsid 以创建一个新会话。原因:使调用进程,第一,成为新会话的首进程;第二,成为新进程组的组长进程;第三,没有控制终端(在基于 System V 的系统中可以通过 fork 两次来达到防止取得控制终端的效果的,其不再需要下面的规则6)。
- 将当前工作目录更改为根目录。原因:防止出现不能 umount 的问题。
- 关闭不再需要的文件描述符。原因:令守护进程不再持有从父进程继承来的某些文件描述符。
- 某些守护进程打开 /dev/null 使其具有文件描述符0、1和2。原因:防止守护进程与终端设备相关联。
有了上面的原则,现在对照下 MySQL Proxy 中的代码:
/*** start the app in the background * * UNIX-version*/
void chassis_unix_daemonize(void) {
#ifdef _WIN32g_assert_not_reached(); /* shouldn't be tried to be called on win32 */
#else
#ifdef SIGTTOUsignal(SIGTTOU, SIG_IGN);
#endif
#ifdef SIGTTINsignal(SIGTTIN, SIG_IGN);
#endif
#ifdef SIGTSTPsignal(SIGTSTP, SIG_IGN);
#endifif (fork() != 0) exit(0);if (setsid() == -1) exit(0);signal(SIGHUP, SIG_IGN);if (fork() != 0) exit(0);chdir("/");umask(0);
#endif
}
从上面的实现代码中,可以看出以下几点:
- 代码执行的先后顺序有的是必须的(如setsid 之前的 fork),有的不是必须的(如 umask 放在最后执行)。
- 实现中使用了两次 fork ,为 System V 中理念。
- 在 setsid 和第二次 fork 之间插入了 signal 处理,用于对 SIGHUP 执行 SIG_IGN 处理。
在上述 6 条 daemon 编程规则中没有提到 signal 处理的问题,那么针对 SIGHUP 的处理代表的是什么意思呢?还是参阅 APUE :
如果终端接口检测到一个连接断开,则将此信号发送给与该终端相关的控制进程(会话首进程)。仅当终端的 CLOCAL 标志没有设置时,上述条件下才产生此信号。
有别于由终端正常产生的信号(如中断、退出和挂起)-- 这些信号总是传递给前台进程组 -- SIGHUP 信号可以发送到位于后台运行的会话首进程。SIGHUP 信号的默认处理动作是终止当前进程。通常会使用该信号来通知守护进程,以重新读取它们的配置文件,因为守护进程不会有控制终端,而且通常决不会收到这种信号。
从上面这段文字可以看出,这里增加了 signal 信号处理的原因是,在 setsid 和第二次 fork 之间,当前的子进程仍旧是会话首进程,有可能会在收到 SIGHUP 信号时终止,所以这里通过设置 SIG_IGN 进行忽略。
至此,一个 daemon-mode 的守护进程就启动了。
在linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程,都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。
daemon进程是后台守护进程,有时候也叫精灵进程(agent).linux 下server都是daemon进程。相信大部分开发人员都知道如何去写一个daemon进程。但是另一方面,大部分人不知道为什么要这么做,不少人是从某个地方copy一个函数,拿来主义。但是具体为什么这么实现,却不是很透彻。
- int daemon(void)
- {
- pid_t pid = fork();
- if( pid != 0 ) exit(0);//parent
- //first children
- if(setsid() == -1)
- {
- printf("setsid failed\n");
- assert(0);
- exit(-1);
- }
- umask(0);
- pid = fork();
- if( pid != 0) exit(0);
-
- //second children
- chdir ("/");
- for (int i = 0; i < 3; i++)
- {
- close (i);
- }
- int stdfd = open ("/dev/null", O_RDWR);
- dup2(stdfd, STDOUT_FILENO);
- dup2(stdfd, STDERR_FILENO);
- return 0;
- }
在编写精灵进程程序时需要遵循一些基本规则,以便防止产生并不希望的交互作用。下面先说明这些规则,然后是一个按照规则编写的函数daemon_init。
1. 首先做的是调用fork,然后使父进程exit。这样做实现了下面几点:
第一:如果该精灵进程是由一条简单shell命令启动的,那么使父进程终止使得shell认为这条命令已经执行完成;(之后的所有工作都在子进程中完成,而用户在Shell终端里则可以执行其他命令,从而在形式上做到了与控制终端的脱离。)
第二:在Linux中父进程先于子进程退出会造成子进程成为孤儿进程,而每当系统发现一个孤儿进程,就会自动由1号进程(init)收养它,这样,原先的子进程就会变成init的子进程;
第三:子进程继承了父进程的进程组ID,但具有一个新的进程ID,这就保证了子进程不是一个进程组的首进程。这对于下面就要做的setsid调用是必要的前提条件。
2. 调用setsid以创建一个新对话期。于是执行9.5节中列举的三个操作,使调用进程:
第一:成为新对话期的首进程;
第二:成为一个新进程组的首进程;
第三:没有控制终端。
在SVR之下,有些人建议在此时再调用fork,并使父进程终止。第二个子进程作为精灵进程继续运行。这样就保证了该精灵进程不是对话期的首进程,于是按照SVR规则(见9.6节)可以防止它取得控制终端。另一方面,为了避免取得控制终端,无论何时打开一个终端设备都要指定O_NOCTTY。
3. 将当前工作目录更改为根目录。父进程继承过来的当前工作目录可能在一个装配的文件系统中。因为精灵进程通常在系统再引导之前是一直存在的,所以如果精灵进程的当前工作目录在一个装配文件系统中,那么该文件系统就不能被拆卸。
另外,某些精灵进程可能会把当前工作目录更改到某个指定位置,在此位置做它们的工作。
例如,行式打印机假脱机精灵进程常常将其工作目录更改到它们的spool目录上。
4. 将文件方式创建屏蔽字设置为0。由继承得来的文件方式创建屏蔽字可能会拒绝设置某些许可权。例如,若精灵进程要创建一个组可读、写的文件,而继承的文件方式创建屏蔽字,屏蔽了这两种许可权,则要求的组可读、写就不能起作用。
5. 关闭不在需要的文件描述符。这样使精灵进程就不再持有从其父进程继承来的某些文件描述符(父进程可能是shell进程,或某个其他进程)。但是,究竟关闭那些描述符则与具体的精灵进程有关,所以在下面的例子中不包含此步骤。可以使用程序2-3中的open_max函数来决定最高文件描述符值,并关闭直到该值的所有描述符。
daemon进程原理及实现(来源于网络,后整理)
为何两次Fork
这是创建Daemon进程的大致步骤及作用:
- 第一次fork(产生父子进程,父进程退出,子进程与父进程控制终端脱离)
- 子进程setsid(子进程成为会话组长,与父进程会话脱离)
- 第二次fork(产生子孙进程,子进程退出,孙进程成为Daemon最终进程)
- 切换工作目录、设置文件创建掩模、关闭所有打开文件句柄等
原则上来说在第一次fork后,子进程即与父进程的控制终端脱离关系,父进程退出后,子进程被init接管,基本达到了Daemon进程要求。但还差了一点,子进程与父进程还在同一个会话组,因此子进程需要调用setsid以达到与父进程会话脱离。也就是这个setsid,使子进程成为了新的会话组组长,却导致了新的问题产生:在Linux中会话组长可以重新申请打开一个控制终端。为了彻底与控制终端断绝关系,我们需要一个非会话组长的进程,子进程的子进程正是我们要的。
守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进 程。Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd,Web服务器httpd等。同时,守护进程完成许多系统任务。 比如,作业规划进程crond,打印进程lpd等。
守护进程的编程本身并不复杂,复杂的是各种版本的Unix的实现机制不尽相同,造成不同 Unix环境下守护进程的编程规则并不一致。需要注意,照搬某些书上的规则(特别是BSD4.3和低版本的System V)到Linux会出现错误的。下面将给出Linux下守护进程的编程要点和详细实例。
一. 守护进程及其特性
守护进程最重要的特性是后台运行。在这一点上DOS下的常驻内存程序TSR与之相似。其次,守护进程必须与其运行前的环境隔离开来。这些环 境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下 来的。最后,守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还可以由用户终端(通常是 shell)执行。
总之,除开这些特殊性以外,守护进程与普通进程基本上没有什么区别。因此,编写守护进程实际上是把一个普通进程按照上述的守护进程的特性改造成为守护进程。如果对进程有比较深入的认识就更容易理解和编程了。
二. 守护进程的编程要点
不同Unix环境下守护进程的编程规则并不一致。所幸的是守护进程的编程原则其实都一样,区别在于具体的实现细节不同。这个原则 就是要满足守护进程的特性。同时,Linux是基于Syetem V的SVR4并遵循Posix标准,实现起来与BSD4相比更方便。编程要点如下;
1. 在后台运行。
为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。
2. 脱离控制终端,登录会话和进程组if(pid=fork())
exit(0); //是父进程,结束父进程,子进程继续
有必要先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。 控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用setsid()使进程成为会话组长:
setsid(); //设置为会话组长
说明:当进程是会话组长时setsid()调用失败。但第一点(fork())已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
3. 禁止进程重新打开控制终端
现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端(再fork一次):
if(pid=fork())
exit(0); //结束第一子进程,第二子进程继续(第二子进程不再是会话组长)
4. 关闭打开的文件描述符
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,可能会造成进程所占用的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:
for(i=0;i < 3; ++i)
close(i); //0, 1, 2 分别表示标准输入、标准输出和标准错误
当然,此处关闭哪个文件描述符与实际需要相关,也可以关闭以后再重新打开。
5. 改变当前工作目录
进程活动时,如果该目录是一个挂载的目录,将导致其文件系统不能卸载。一般需要将工作目录改变到根目录(chdir("/"))。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如 /tmp
6. 重设文件创建掩模
进程从创建它的父进程那里继承了文件创建掩模。由继承得来的文件方式创建屏蔽字可能会拒绝设置某些许可权。例如,若daemon进程要创建一个组可读、写的文件,而继承的文件方式创建屏蔽字,屏蔽了这两种许可权,则要求的组可读、写就不能起作用。为防止这一点,将文件创建掩模清除:
umask(0);
7. 处理SIGCHLD信号
处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结 束,子进程将成为僵尸进程(zombie)。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下 可以简单地将 SIGCHLD信号的操作设为SIG_IGN。
signal(SIGCHLD,SIG_IGN);
这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。
三. 守护进程实例
fork()两次,将孙子进程“过继”给1号进程init,如果孙子进程成功,其父进程将会是系统1号进程init,如果失败,将由init负责收拾残局。
void InitAsDaemon()
{
if (fork() > 0 ) exit(0);
setsid();
signal(SIGINT, SIG_IGN);
signal(SIGHUP,SIG_IGN);
signal(SIGQUIT, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
signal(SIGTOU, SIG_IGN);
signal(SIGTIN, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
if (fork() > 0) exit(0);
chdir("/");
umusk(0);
}
四. wait和waitpid
两者相同点都是用于在创建子进程之后,阻塞自己,然后检查自己的子进程中是否有僵尸进程存在,如果存在,该父进程就释放僵尸进程占用的资源,并返回,如果没有这样的僵尸进程,就一直阻塞。
不同点是后者还有两个pid和option参数。
pid:当pid > 0时,只等待进程号为pid的进程退出;pid == -1时,等待任意子进程,作用等同于wait;pid == 0时,等待同一进程组中任意子进程;pid < -1 时,等待某个进程组的任意进程,该进程组的组号为pid的绝对值。
option:有两个值,WNOHANG和WUNTRACED,前者使得waitpid立即返回而不管有没有子进程退出;后者是指其子进程集合中,如果有子进程是STOPED状态,就立即返回,如果该进程是被traced的,那么即使不提供WUNTRACED参数,也立即返回。
于是可以看出,wait是等待第一个退出的子进程,而waitpid是等待指定子进程退出。因此对于多进程服务器,使用waitpid可以避免因为第一个子进程退出调用了wait,而造成剩余子进程没有wait来处理,引起的僵尸进程的问题。
五. 僵尸进程的处理
僵尸进程是无法使用kill或者killall来杀死的,虽然僵尸进程并不占用很多系统资源(只是占用进程表process table中的一个项),但是过多的僵尸进程还是会对系统性能造成影响(达到系统进程数上限),因此应尽可能避免:
1、改写父进程:如上所述,fork两次,将init作为其父进程,由系统负责清理
2、kill掉僵尸进程的父进程,交由系统处理。
使用
ps auwx可以查看系统中僵尸进程,僵尸进程的状态会被标注为“Z”。或者
ps axf 以树形展示进程表
ps axm 列出线程,linux下进程线程一致
ps aux 列出进程的详细信息
这篇关于daemon进程原理及实现 信号 终端 为何两次Fork的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!