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之前,首先要了解两个概念:进程组和会话期
- 每个进程也属于一个进程组
- 每个进程主都有一个进程组号,该号等于该进程组组长的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);
===============================
这样,一个简单的守护进程就建立起来了。
处理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环境下服务器程序的后台运行等特点作了详细的描述,并提出了一些设计和实现的方案。限于篇幅,本文无法将实现的每个细节都阐述清楚,比如上面所说的日志写入缓冲和日志锁定机制等,但在后续的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),所以就无法打开新的控制终端。
程序首先调用fork,并让父进程退出,子进程继续运行。由于子进程是由父进程通过fork系统调用派生的,因此子进程继承父进程的进程组号,但它拥有自己的进程号;然后调用setsid创建一个新的session[3], 子进程是该session中唯一的进程,并且是该session的leader(或者说是creator),同时也是新创建的进程组的leader。在调 用setsid以后,调用进程将不再拥有控制终端。
下面是一个用 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().
"""