daemon编程规则(为什么要fork两次)

在linux或者unix操作系统中在系统的引导的时候会开启很多服务,这些服务就叫做守护进程。为了增加灵活性,root可以选择系统开启的模式,这些模式叫做运行级别,每一种运行级别以一定的方式配置系统。

守护进程是脱离于终端并且在后台运行的进程。守护进程脱离于终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的终端信息所打断。
守护进程,也就是通常说的Daemon进程,是Linux中的后台服务进程。它是一个生存期较 长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程常常在系统引导装入时启动,在系统关闭时终止。Linux系统 有很多守护进程,大多数服务都是通过守护进程实现的,同时,守护进程还能完成许多系统任务,例如,作业规划进程crond、打印进程lqd等(这里的结尾 字母d就是Daemon的意思)。
由于在Linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程 都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。但是守护进程却能够突破这种限制,它从被执行开始 运转,直到整个系统关闭时才退出。如果想让某个进程不因为用户或终端或其他地变化而受到影响,那么就必须把这个进程变成一个守护进程。
创建一个简单的守护进程:
1、创建子进程,父进程退出
这是编写守护进程的第一步。由于守护进程是脱离控制终端的,因此,完成第一步后就会在Shell终端里造成一程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在Shell终端里则可以执行其他命令,从而在形式上做到了与控制终端的脱离。
在Linux中父进程先于子进程退出会造成子进程成为孤儿进程,而每当系统发现一个孤儿进程就会自动由1号进程(init)收养它,这样,原先的子进程就会变成init进程的子进程。
 

 

孤儿进程和僵尸进程的介绍

https://www.cnblogs.com/Anker/p/3271773.html


2、在子进程中创建新会话
这个步骤是创建守护进程中最重要的一步,虽然它的实现非常简单,但它的意义却非常重大。在这里使用的是系统函数setsid,在具体介绍setsid之前,首先要了解两个概念:进程组和会话期

 

 
 会话、进程组、进程与控制终端的关系
进程组:是一个或多个进程的集合。进程组有进程组ID来唯一标识。除了进程号(PID)之外,进程组ID也是一个进程的必备属性。且该进程组ID不会因组长进程的退出而受到影响。
  • 每个进程也属于一个进程组
  • 每个进程主都有一个进程组号,该号等于该进程组组长的PID号 .
  • 一个进程只能为它自己或子进程设置进程组ID号

会话期:

会话期是一个或多个进程组的集合。通常,一个会话开始于用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期

setsid()函数可以建立一个对话期:

 如果,调用setsid的进程不是一个进程组的组长,此函数创建一个新的会话期

调用setsid有下面的3个作用:

让进程摆脱原会话的控制
让进程摆脱原进程组的控制
让进程摆脱原控制终端的控制

(1)此进程变成该对话期的首进程

(2)此进程变成一个新进程组的组长进程。

(3)此进程没有控制终端,如果在调用setsid前,该进程有控制终端,那么与该终端的联系被解除。 如果该进程是一个进程组的组长,此函数返回错误。

(4)为了保证这一点,我们先调用fork()然后exit(),此时只有子进程在运行

由于创建守护进程的第一步调用了 fork函数来创建子进程,再将父进程退出。由于在调用了fork函数时,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会 话期、进程组、控制终端等并没有改变,因此,还还不是真正意义上的独立开来,而setsid函数能够使进程完全独立出来,从而摆脱其他进程的控制。

现在我们来给出创建守护进程所需步骤:

编写守护进程的一般步骤步骤:

(1)在父进程中执行fork并exit推出;

(2)在子进程中调用setsid函数创建新的会话;

(3)在子进程中调用chdir函数,让根目录 ”/” 成为子进程的工作目录;

(4)在子进程中调用umask函数,设置进程的umask为0;

(5)在子进程中关闭任何不需要的文件描述符

说明:

2. 脱离控制终端,登录会话和进程组
有必要先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。
控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用setsid()使进程成为会话组长:
setsid();
说明:当进程是会话组长时setsid()调用失败。但第一点已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。


3、改变当前目录为根目录
这一步也是必要的步骤。使用fork创建的子进程继承了父进程的当前工作目录。由于在进程运行 中,当前目录所在的文件系统(如“/mnt/usb”)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此, 通常的做法是让"/"作为守护进程 的当前工作目录,这样就可以避免上述的问题,当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp。改变工作目录的常见函数式 chdir。
4、重设文件权限掩码
文件权限掩码是指屏蔽掉文件权限中的对应位。比如,有个文件权限掩码是050,它就屏蔽了文件 组拥有者的可读与可执行权限。由于使用fork函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件权限 掩码设置为0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask。在这里,通常的使用方法为umask(0)。
5、关闭文件描述符
同文件权限码一样,用fork函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。
在上面的第二步之后,守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能到达守护进程,守护进程中用常规方法(如printf)输出的字符也不可能在终端上显示出来。所以,文件描述符为0、1和2 的3个文件(常说的输入、输出和报错)已经失去了存在的价值,也应被关闭。
通常按如下方式关闭文件描述符:
===============================
for(i=0;i<MAXFILE;i++)
close(i);
===============================
这样,一个简单的守护进程就建立起来了。
6. 处理SIGCHLD信号
处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。
signal(SIGCHLD,SIG_IGN);
这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。
实现守护进程的完整实例(每隔10s在/tmp/dameon.log中写入一句话):
=====================================================================
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#define MAXFILE 65535
int main()
{
pid_t pc;
int i,fd,len;
char *buf="this is a Dameon\n";
len = strlen(buf);
pc = fork(); //第一步
if(pc<0){
printf("error fork\n");
exit(1);
}
else if(PC>0)
exit(0);
setsid(); //第二步
chdir("/"); //第三步
umask(0); //第四步
for(i=0;i<MAXFILE;i++) //第五步
close(i);
while(1){
if((fd=open("/tmp/dameon.log",O_CREAT|O_WRONLY|O_APPEND,0600))<0){
perror("open");
exit(1);
}
write(fd,buf,len+1);
close(fd);
sleep(10);
}
}
 

服 务器开发需要考虑的内容很多,比如服务器的架构、稳定性、性能以及负载能力等等。事实上,在开发服务器的过程中,需要综合考虑各种因素,比如就客户端连接 时间较短却又比较频繁的服务器(例如HTTP服务器)而言,在可选的服务器结构中,预先派生进/线程的结构就要比并发式结构高效,这一点将在后续的文章中 对其进行详细的介绍。然后就是服务器实现方面的细节,比如是否需要支持跨平台的能力、采用什么样的开发语言和开发工具、如何提高服务器系统的性能。所有的 这些问题都需要在服务器的定义与设计的过程中作出充分的考虑。

其 实,无论是Windows服务器,还是Linux服务器,它们之间都有共同的特点。首先就是后台运行,目前,绝大多数服务器都是后台运行的,这是因为服务 器的主要任务是给客户端提供所请求的服务,通常情况下是不需要与用户进行界面交互的,用户只需要能够启动服务、暂停服务或者停止服务就可以了,因此,服务 器没有必要去占有一个终端会话(或者说是拥有一个可视化的用户界面);其次,由于服务器是后台运行的,它并没有一个可视化的用户界面,所以服务器运行时所 需的参数就只能通过文件[1]读入,然后根据从文件中读入的数据作不同的处理;再次,由于服务器的后台运行,它无法通过界面将运行状态以及一些必要的处理结果显示给用户,因此,它需要将这些信息写入一个文件[2], 以便在服务器出现问题的时候,用户能够根据该文件中的内容对服务器的故障进行诊断;最后,还是与服务器的后台运行有关,对于计算机的用户来说,服务器并不 是一个需要经常交互的程序,与一般的应用程序相比,在服务器设计的过程中,应该更多地考虑服务器占用系统资源的问题,这里所说的资源包括CPU、IO以及 存储器资源。对于Windows服务来说,这点尤为重要,因为Windows服务很有可能就是安装在某一个用户的机器上,而不是特定的Windows服务 器上。试想,如果某个Windows服务占用了过多的系统资源,那么该系统的用户就很有可能无法正常地完成其他的工作。上面总结了各种服务器所共有的特点,下面将对这些共有特点的设计与实现进行详细的描述,并对Windows服务器与Linux服务器之间的差别进行必要的说明。

 
一、 后台运行
在Linux 环境下,要实现服务器的后台运行非常简单,只需要在服务器程序文件名后面加上“&”符号就可以了。例如某服务器程序的文件名是 test_server,那么启动该服务器并使其后台运行的方法就是在提示符下输入“test_server&”。假设现在有一个 test_server服务器,其代码如下:
#include <stdio.h>
int main (int argc, char *argv[])
{
    while (1)
    {
        // code to serve the client
    }
    return 0;
}
在使用“test_server&”运行该程序以后,系统会自动回到提示符状态而不占用任何终端(如果直接使用程序名来启动程序,那么系统会因为while死循环而占用终端,直到将程序进程杀死)。我们可以使用ps命令来查看程序是否已经在后台运行,我们可能会获得类似下面的提示信息:
root      1417 1358 99 14:47 tty1     00:12:10 ./test_server
为什么在这个简单的程序中需要使用while死循环呢?原因有两个,首先,如果不是死循环,那么程序刚运行就会退出,我们根本就看不到它是否已经运行了,更不用说去检查它是否已在后台运行;其次,这个程序结构是服务器结构的一种抽象,大多数服务器的数据接收、处理和发送等操作都是在这个while死循环中进行的,直到获得一个信号或指令后,服务器程序才会退出。
遗憾的是,没有一种让人感觉比较专业的方法能够让服务器程序退出,我们可以用kill命令将服务器进程杀死,这种做法似乎有点野蛮,但也不失为一种不错的方法。在稍后的介绍中,我们会用另外一种看上去感觉更加专业的办法来解决这个问题。
另一种使服务器程序后台运行的方法是使用守护进程(daemon process)。要将服务器程序作为守护进程运行,通常的做法是先定义一个守护进程初始化函数,然后在main函数中调用这个初始化函数。一般情况下,该守护进程初始化函数可以定义如下:
在定义了守护进程初始化函数以后,只需要在必要的时刻(一般是main函数中)调用该初始化函数就可以了,例如:
int main (int argc, char *argv[])
{
    InitDaemon();
    while (1)
    {
    }
    return 0;
}
前 面已经提到,要使处于后台运行的服务器进程退出,可以用kill命令将服务器进程杀死,这对于使用守护进程初始化函数实现后台运行的服务器程序同样有效。 这种做法看起来似乎有点野蛮,但不失为一种使服务器进程退出的好方法。但如果采用这种方法,那么在设计服务器程序的时候,我们应该使其具有捕获某种信号 (这个信号通常是由kill命令指定的)的能力,并在捕获了该信号以后能够完成一些服务器进程退出前的扫尾工作,比如释放内存、关闭文件描述字等,否则很 有可能造成资源泄漏。
举例 来说,我们可以使用kill命令向某个进程发出指定的信号,这个信号可以通过参数来指定,当然我们可以不指定这个参数,这样的话kill命令会默认地向进 程发出SIGTERM信号。现在我们要做的是,在服务器程序中加入对SIGTERM信号的处理函数,以便在获得该信号后执行一些必要的处理。我们可以使用 类似下面的代码来实现这样的功能:
#include <signal.h>
void handle_term_signal (int sig)
{
    // 此处填写必要的处理逻辑
}
 
int main (int argc, char *argv[])
{
    // . . .
    signal (SIGTERM, handle_term_signal);
    // . . .
    return 0;
}
然 后使用“kill –SIGTERM 进程号”或“kill 进程号”命令向指定的服务器进程发送SIGTERM信号,服务器进程接收到这个SIGTERM信号后,就会执行handle_term_signal函 数。由于SIGTERM信号是可以被阻塞和拦截的,这一点与SIGKILL信号是不同的,因此在服务器程序中,我们需要捕获的是SIGTERM信号,而不 是SIGKILL信号。
需要说明的是,在Linux系统中,进程接收到SIGTERM信号后并不会退出,要让运行中的进程退出,还需要向其发送SIGKILL信号。也就是说,首先向服务器进程发送SIGTERM信号,使其完成退出前的收尾工作,然后向其发送SIGKILL信号,将进程杀死。
为了操作的方便,可以编写一个很简单的shell程序来完成上面所说的操作。打开vi,输入以下的shell命令,然后以killit文件名保存:
kill –SIGTERM $1
sleep 2
kill –SIGKILL $1
这样的话,只需要使用“killit 进程号”命令就可以让后台运行的服务器进程退出了。在上面的shell程序中,两个kill命令间使用了sleep命令,这是为了确保服务器进程在接收到SIGKILL信号前,已经完成了必要的收尾工作。
值得注意的是,如果在函数handle_term_signal()中加入exit()函数调用,那么当进程接收到SIGTERM信号后,会自动退出,而不需要再次使用kill –SIGKILL命令。
当 然,我们可以不使用kill命令来使服务器进程退出,而使用命令行参数这样一种看上去相对专业的方法来实现。假如服务器程序文件名为 “test_server”,那么我们可以使用“test_server start”来启动服务器;用“test_server stop”来停止服务器;而用“test_server restart”来重新启动服务器。实现这种做法的基本思路是:对命令行参数进行判断,如果是start,那就看是否已经有一个服务器进程在运行,如果是 的话,则提示说服务器已经运行,否则就启动服务器;如果命令行参数是stop,那么就看服务器进程是否处于运行状态,如果是的话,则服务器进程退出,否则 就提示说服务器还没有运行;如果命令行参数是restart,那么首先将原服务器进程退出,然后再启动。这样做既可以使我们的服务器程序看上去更加专业, 又可以避免服务器的多次重复启动所造成的某些资源冲突以及资源浪费。那 么服务器程序在启动的时候,应该如何判断是否已经有一个进程正在运行呢(也就是图一中两个菱形框的判断条件,通常将这样的问题称为程序的二重启动问题)? 解决这个问题最直接的办法是使用进程通讯,通过进程通讯,待启动的服务器进程会首先查询某些标志位或试图与已经运行的服务器进程通讯。如果在全局域中设置 了标志位,或者能够成功地与已经运行的服务器进程通讯,则表示服务器进程已经启动,无需再次启动;否则就启动服务器进程。Linux下实现进程通讯的方法 很多,socket、管道、互斥锁以及共享内存等,都可以实现进程间通讯。我们可以使用共享内存来实现,这是因为共享内存除了可以体现标志位以外,还能够 保存一些有用的数据,比如进程的已运行服务器进程的pid。这样的话,在启动服务器进程的时候,首先判断共享内存中的标志位,如果标志位存在,则表示已经 有一个服务器进程在运行,进而再判断当前的服务器程序参数是什么,如果参数为“start”,那么显然是要让当前待运行服务器进程直接退出的;如果参数为 “stop”,那么就从共享内存中获取已运行服务器进程的pid,然后使用kill函数向其发送SIGTERM信号,已运行服务器进程在捕获了这个 SIGTERM信号后,立即转入handle_term_signal()函数进行进程退出前的扫尾工作,然后进程退出。假设我们使用共享内存来解决二重 启动与进程退出问题,由此可见,Linux下服务器程序的开发比Windows下要复杂许多,它需要开发人员更多地考虑服务器的启动、停止和重启的细节问题,不仅如此,开发人员还需要对Linux环境下C语言的高级应用有一定的了解。
 
二、 配置文件
前面已经提到,由于服务器程序没有用户界面,所以用户也就无法通过界面来设置服务器运行所需要的参数。这一问题可以使用配置文件来解决,服务器程序只要读取配置文件中的信息,就可以获得所需要的参数。
通 常情况下,服务器程序只有在启动的时候才读取配置文件,因此,如果用户修改了配置文件的内容,要想使得所做的修改生效,就必须重新启动服务器程序。修改了 配置文件以后需要重新启动服务器的另一个原因是,在服务器程序的运行过程中,某些参数是需要被多次使用的,如果在使用的过程中修改了这些参数的值,就有可 能影响服务器程序的运行逻辑。
常 用的配置文件格式有两种,一种是INI形式的文件,另一种是XML格式的。配置文件使用什么样的格式其实并不重要,只要服务器能够从中正确地读取数据就可 以了。当然,配置文件一定是文本文件,这是为了方便服务器的管理员对配置文件进行修改。如果设计的系统能够提供配置文件的编辑程序,那么,配置文件也可以 是二进制文件。
在Linux 系统中,使用C语言读写INI格式文件或者是XML文件都不是件简单的事情,开发者可以使用第三方提供的开发库,但就我目前的情况,我使用的是INI格式 文件,并且为INI格式文件中数据的读取编写了一套函数库。除非第三方的开发库做得非常优秀、可信度非常高,否则尽量不要使用,这是因为你无法控制他人所 编写的程序中的错误率,一旦程序的运行出现问题,调试他人的程序将会是件令人头痛的事情。在Windows系统中,读写INI文件非常简单,开发者不需要 自己去编写文件解析程序,使用Windows API中的GetPrivateProfileString、WritePrivateProfileString等函数就可以方便地读写INI文件;读 写XML文件也不会太难,.NET Framework为XML文件的操作提供了很好的支持。在32位的Windows系统中,应该尽量将配置信息写在系统注册表中以供服务器程序读取,而不 要使用INI文件,这是Windows系统中应用程序读写配置信息的最佳操作。如果所设计的服务器系统需要与16位Windows系统中的某些程序兼容, 那么就可以根据情况来决定是否使用INI文件。
 
三、日志文件
日 志文件是服务器系统的重要组成部分。目前出现的绝大多数服务器都有自己的日志文件。由于服务器程序的后台运行特性及其特殊的工作方式,它无法将一些过程、 状态以及结果信息显示在屏幕上,日志文件就成为了服务器程序记录数据的主要方式。由于日志文件中记录了服务器程序处理过程、工作状态和日期等关键数据,因 此,日志文件是服务器系统错误跟踪的主要依据,如果服务器程序在运行的过程中出现问题,那么系统管理员就可以根据日志文件中的数据描述确定问题的来源,进 而解决问题,使服务器正常工作。
在Linux环境下开发服务器的过程中,程序员可以根据实际情况在服务器程序的适当位置添加记录日志信息的代码,这样,当服务器程序运行到该位置时,会自动地向指定的日志文件输出信息。下面的代码段试图在程序出现异常后将异常信息输出到日志文件:
void MyServerApp::Main (int argc, char *argv[])
{
    // . . .
    try
    {
        // . . .
}
catch (MyServerException &ex)
{
    gAppLog->WriteLog (LOGLEV_ERR, “Exception raised: %s\n”, ex.Message);
}
    // . . .
}
现在,我们谈谈如何实现日志文件的写入处理,也就是如何实现上例中的gAppLog->WriteLog函数[4]。 不难发现,WriteLog函数是一个可变参的函数,参数的设置可以根据不同情况进行设定。例如,上面的WriteLog函数仅向日志文件写入了日志条目 级别和必要的信息字符串,服务器系统的设计者同样可以修改这个WriteLog函数,使得该函数还能够向日志文件输出服务器名称、日志条目写入时间等信 息。在Linux系统中,设计可变参的函数其实很简单,只要在函数声明的时候使用省略符格式,在函数定义的时候使用va_list相关的宏来实现具体的操 作即可。下面的例子演示了WriteLog函数的具体实现,真正的日志输出函数应该根据服务器系统的具体需求情况进行定义。
int WriteLog (int level, char *fmt, . . .)
{
    char loglev_str[512];
    FILE *fp;
    va_list args;
    memset(loglev_str, 0x00, sizeof(loglev_str));
    fp = fopen (“serversystem.log”, “a+”); // 此处第一个参数指定日志文件名
// 第二个参数使用a+模式打开文件,保证日志的正确写入
    if (NULL == fp) return -1;
    switch(level)
    {
        case LOGLEV_OK: strcpy(loglev_str, “[OK ]”); break;
        case LOGLEV_ERR: strcpy(loglev_str, “[ERR ]”); break;
        case LOGLEV_WAR: strcpy(loglev_str, “[WARN]”); break;
        defaultbreak;
    }
    va_start(args, fmt);
    fprintf (fp, “%s”, loglev_str);
    vfprintf (fp, fmt, args);
    va_end(args);
    fclose(fp);
    return 0;
}
在 上面的代码中,WriteLog函数一味地向serversystem.log文件写入信息,时间一长,势必会导致日志文件容量的无限期增加,这是一个非 常严峻的问题,过大的日志文件可能占用磁盘的大部分有效空间,从而造成服务器系统因为没有足够的磁盘空间而出现异常甚至崩溃。由于服务器程序启动以后很少 需要人为的干预,日志文件容量无限期增加这一问题很容易被服务器系统管理员忽视,而作为管理员来说,要每隔一定的时期去为服务器清理日志文件也是一件麻烦 的事情,并且稍不小心就有可能影响服务器系统的正常运行,这些问题对于使命关键的服务器(例如,大型商务系统的核心服务器等)来说是无法容忍的。由此可 见,服务器系统需要一个日志管理机制,为服务器系统日志的管理提供一个中心位置,该日至管理机制至少需要两个功能:①对过大的日志文件进行备份,并重写(overwrite)日志文件;②删除过期的备份日志文件。
服 务器的日志管理机制可以是当前服务器系统的一个子系统,也可以是一个独立的服务器系统,这可以根据所设计的服务器系统的规模来决定。日志管理机制没有必要 时时刻刻处于对日志的清理状态,因为日志管理机制的运行也需要占用系统资源,势必会影响主服务器系统的运行效率。通常的做法是,每天或每隔几天,选择一个 服务器访问率相对较小的时间(比如午夜或凌晨)进行系统日志的清理工作。如果服务器系统在此期间内无法停止运行,那么日志处理模块还需要提供日志写入缓冲 和日志文件锁定机制,确保日志信息在日志管理机制对日志文件进行清理时也能正确写入。

    至此,对服务器程序三大主要特点的基本介绍就告一段落。本文首先对服务器程序的特点作了简要的介绍,引出了服务器程序的最主要的特点:后台运行,这一特点也就决定了服务器程序配置文件和日志文件存在的必要性;然后,本文对Linux环境下服务器程序的后台运行等特点作了详细的描述,并提出了一些设计和实现的方案。限于篇幅,本文无法将实现的每个细节都阐述清楚,比如上面所说的日志写入缓冲和日志锁定机制等,但在后续的Linux服务器开发文章中,笔者会尽可能地阐明其中的具体细节问题,使得读者对Linux服务器程序的实现过程有更深一步的了解。

在linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程,都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。 

 

    在编写精灵进程程序时需要遵循一些基本规则,以便防止产生并不希望的交互作用。下面先说明这些规则,然后是一个按照规则编写的函数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

第一次fork的作用是让shell认为这条命令已经终止,不用挂在终端输入上,还有就是为了后面上的setsid服务,因为调用setsid函数的进程不能是组长进程,如果不fork出子进程,则此时的父进程是进程组长,就无法调用setsid。当子进程调用完setsid函数之后,子进程是会话组长也是进程组组长,并且脱离了控制终端,此时,不管控制终端如何操作,新的进程都不会收到一些信号使得进程退出。

第二次fork的作用虽然当前关闭了和终端的联系,但是后期可能会误操作打开了终端。只有会话首进程能打开终端设备,也就是在fork一次,再把父进程退出,再次fork的子进程作为守护进程继续运行,保证了该精灵进程不是对话期的首进程,第二次不是必须的,是可选的,市面上有些开源项目也是fork一次。

经过前面2个步骤,基本想要做的都做了。第2次fork不是必须的,也看到很多开源服务没有fork第2次。fork第2次主要目的是:防止进程再次打开一个控制终端。因为打开一个控制终端的前提条件是该进程必须是会话首进程,再fork一次,子进程ID != sid(sid是进程父进程的sid),所以就无法打开新的控制终端。

void InitDaemon()
{
    pid_t pid;
    if ((pid = fork()) != 0)
        exit(0);
    setsid();
    if ((pid = fork()) != 0)
        exit(0);
    chdir(“/”);
    umask(0);
}

程序首先调用fork,并让父进程退出,子进程继续运行。由于子进程是由父进程通过fork系统调用派生的,因此子进程继承父进程的进程组号,但它拥有自己的进程号;然后调用setsid创建一个新的session[3], 子进程是该session中唯一的进程,并且是该session的leader(或者说是creator),同时也是新创建的进程组的leader。在调 用setsid以后,调用进程将不再拥有控制终端。

第二次fork的目的在于,确保当进程打开终端设备时,也无法获得控制终端,这是因为由session leader派生的子进程一定不是session leader,也就无法获得控制终端;接下来的chdir系统调用是更改守护进程的工作目录,假设守护进程是在vfat文件系统中启动的,如果没有使用 chdir来更改守护进程的工作目录,那么在守护进程进入后台运行后,此vfat文件系统就有可能无法卸载(unmount)。事实上,在Linux系统 中,即使使用了chdir来更改守护进程的工作目录,启动该守护进程的文件系统仍然无法卸载。在实际使用中,chdir更多地用于满足服务器本身的处理需 求;最后的umask(0)是为了在守护进程创建自己的文件时,文件权限位不受原有文件创建掩码的权限位的影响。

  daemon目的就是防止终端产生的一些信号让进程退出。上面函数并没有直接调用signal函数去处理它,而是间接通过fork和setsid函数使用更少代码优雅处理,而被有些人误以为是由于僵死进程的原因,才需要这样处理。
 
    当然,也有很多程序不是像上面函数那样去实现,而是直接通过忽略信号方式处理。这样其实也不错,因为这些信号很少会有用到的价值,直接忽略基本上不存在误杀的情况。反正达到最终目的就可以,条条大路通罗马。
   下面罗列一下控制终端会产生哪些信号。程序中只要处理好这些信号,同样能达到上面函数实现的目的。
   //后台进程读取/写入终端输入产生下面两个信号,或者控制终端不存在时读取和写入会产生
   signal(SIGTTOU, SIG_IGN);
   signal(SIGTTIN, SIG_IGN);
 
   //按CTRL-C ,CTRL-\ CTRL-Z会向前台进程组发送下面这些信号
   signal(SIGINT,  SIG_IGN );
   signal(SIGQUIT, SIG_IGN );
   signal(SIGTSTP, SIG_IGN );
   
   //终端断开,会给会话首进程所在组或孤儿进程组所有成员发送下面信号
   signal(SIGHUP,  SIG_IGN );
 
   还有有些信号也可以由终端shell产生,需要关注
   signal(SIGCONT, SIG_IGN );
   signal(SIGSTOP, SIG_IGN );
 
  上面这些信号,应该有些程序缺省处理(SIG_DFL)本身动作就是忽略(SIG_IGN),不是退出进程。不过按照上面写也不会造成什么问题。

下面是一个用 Python 实现一个Daemon 进程
import sys, os, time, atexit
from signal import SIGTERMclass Daemon:
"""
A generic daemon class.
Usage: subclass the Daemon class and override the run() method
"""
def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
self.stdin = stdin
self.stdout = stdout
self.stderr = stderr
self.pidfile = pidfile
def daemonize(self):
"""
do the UNIX double-fork magic, see Stevens' "Advanced
Programming in the UNIX Environment" for details (ISBN 0201563177)
http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
"""
try:
pid = os.fork()
if pid > 0:
# exit first parent
sys.exit(0)
except OSError, e:
sys.stderr.write("fork #1 failed: %d (%s)/n" % (e.errno, e.strerror))
sys.exit(1)
# decouple from parent environment
os.chdir("/")
os.setsid()
os.umask(0)
# do second fork
try:
pid = os.fork()
if pid > 0:
# exit from second parent
sys.exit(0)
except OSError, e:
sys.stderr.write("fork #2 failed: %d (%s)/n" % (e.errno, e.strerror))
sys.exit(1)
# redirect standard file descriptors
sys.stdout.flush()
sys.stderr.flush()
si = file(self.stdin, 'r')
so = file(self.stdout, 'a+')
se = file(self.stderr, 'a+', 0)
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
# write pidfile
atexit.register(self.delpid)
pid = str(os.getpid())
file(self.pidfile, 'w+').write("%s/n" % pid)
def delpid(self):
os.remove(self.pidfile)
def start(self):
"""
Start the daemon
"""
# Check for a pidfile to see if the daemon already runs
try:
pf = file(self.pidfile, 'r')
pid = int(pf.read().strip())
pf.close()
except IOError:
pid = None
if pid:
message = "pidfile %s already exist. Daemon already running?/n"
sys.stderr.write(message % self.pidfile)
sys.exit(1)
# Start the daemon
self.daemonize()
self.run()
def stop(self):
"""
Stop the daemon
"""
# Get the pid from the pidfile
try:
pf = file(self.pidfile, 'r')
pid = int(pf.read().strip())
pf.close()
except IOError:
pid = None
if not pid:
message = "pidfile %s does not exist. Daemon not running?/n"
sys.stderr.write(message % self.pidfile)
return# not an error in a restart
# Try killing the daemon process
try:
while 1:
os.kill(pid, SIGTERM)
time.sleep(0.1)
except OSError, err:
err = str(err)
if err.find("No such process") > 0:
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
else:
print str(err)
sys.exit(1)
def restart(self):
"""
Restart the daemon
"""
self.stop()
self.start()
def run(self):
"""
You should override this method when you subclass Daemon. It will be called after the process has been
daemonized by start() or restart().
"""

 

 

 

 

 

 

 

posted @ 2018-04-26 21:19  屌丝的IT  阅读(1508)  评论(0编辑  收藏  举报