一:守护进程概述
Linux Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。
二:创建守护进程步骤:
首先我们要了解一些基本概念:
每个进程属于一个进程组
每个进程组有组号和组长,组长进程的pid就是该进程组的组号(pgid)
一个进程只能为它自己或子进程设置进程组ID号
会话期:
会话期(session)是一个或多个进程组的集合,当集合中只有一个进程组时,sid与该进程组 组长的pid相同,这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端,控制终端,登录会话和进程组通常是从父进程继承下来的。
setsid()函数可以建立一个对话期:
如果,调用setsid的进程不是一个进程组的组长,此函数创建一个新的会话期;如果是该进程组的组长,则此函数返回错误
(1)此进程变成该对话期的首进程
(2)此进程变成一个新进程组的组长进程
(3)此进程没有控制终端,如果在调用setsid前,该进程有控制终端,那么与该终端的联系被解除
(4)为了保证这一点,我们先调用fork()然后exit(),此时只有子进程在运行,fork后的子进程pid是重新分配的,即保证了此时运行的进程永远不会是进程组的组长
现在我们来给出创建守护进程所需步骤:
编写守护进程的一般步骤步骤:
(1)在父进程中执行fork并exit推出;
(2)在子进程中调用setsid函数创建新的会话;
(3)在子进程中调用chdir函数,让根目录 ”/” 成为子进程的工作目录;
(4)在子进程中调用umask函数,设置进程的umask为0;
(5)在子进程中关闭任何不需要的文件描述符
以图解两次fork的过程:
前提,会话中只有一个进程组,进程组中只有一个进程;
pid
ppid
pgid(组id)
sid(session id)
组长(y/n)
100
10
100
100
y
第一次fork时:
pid
ppid
pgid(组id)
sid(session id)
组长(y/n)
100
10
100
100
y
101
100
100(extends parent)
100
n
exit父进程:
pid
ppid
pgid(组id)
sid(session id)
组长(y/n)
101
100
100
100
n
init进程接管:
pid
ppid
pgid(组id)
sid(session id)
组长(y/n)
101
1
100
100
n
执行setsid后:
pid
ppid
pgid(组id)
sid(session id)
组长(y/n)
101
1
101
101
y
第二次fork:
pid
ppid
pgid(组id)
sid(session id)
组长(y/n)
101
1
101
101
y
102
101
101
101
n
exit父进程:
pid
ppid
pgid(组id)
sid(session id)
组长(y/n)
102
101
101
101
n
由init进程接管:
pid
ppid
pgid(组id)
sid(session id)
组长(y/n)
102
1
101
101
n
三:守护进程的编写步骤
fork子进程,而后父进程退出,此时子进程会被init进程接管
修改子进程的工作目录、创建新进程组和新会话、修改umask
子进程再次fork一个进程,这个进程可以称为孙子进程,而后子进程退出,此时子进程会被init进程接管
重定向孙子进程的标准输入流、标准输出流、标准错误流到/dev/null。
完成上面的4个步骤,那么最终的孙子进程就称为守护进程。先看下代码,后面再分析下每个步骤的原因。
#!/usr/bin/env python
#coding=utf8
import os, sys, time
#产生子进程,而后父进程退出
pid = os.fork()
if pid > 0:
sys.exit(0)
#修改子进程工作目录
os.chdir("/")
#创建新的会话,子进程成为会话的首进程
os.setsid()
#修改工作目录的umask
os.umask(0)
#创建孙子进程,而后子进程退出
pid = os.fork()
if pid > 0:
sys.exit(0)
#重定向标准输入流、标准输出流、标准错误
sys.stdout.flush()
sys.stderr.flush()
si = file("/dev/null", 'r')
so = file("/dev/null", 'a+')
se = file("/dev/null", 'a+', 0)
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
#孙子进程的程序内容
while True:
time.sleep(10)
f = open('/home/test.txt', 'a')
f.write('hello')
上面的程序没有任何错误处理,但是不影响原理分析。如果要应用到项目里,还需完善。下面笔者谈下自己对每个步骤的理解。
1、fork子进程,父进程退出
通常,我们执行服务端程序的时候都会通过终端连接到服务器,成功连接后会加载shell环境,终端和shell都是进程,shell进程是终端进程的子进程,通过ps命令可以很容易的查看到。在这个shell环境下一开始执行的程序都是shell进程的子进程,自然会受到shell进程的影响。在程序里fork子进程后,父进程退出,对了shell进程来说,这个父进程就算执行完了,而产生的子进程会被init进程接管,从而也就脱离了终端的控制。
2、关闭打开的文件描述符和修改子进程的工作目录
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误,子进程在创建的时候会继承父进程的工作目录,如果执行的程序是在u盘里的,就会导致u盘不能卸载。
3、创建新会话
使用setsid后,子进程就会成为新会话的首进程(session leader);子进程会成为新进程组的组长进程;子进程没有控制终端。
4、修改umask
由于umask会屏蔽权限,所以设定为0,这样可以避免读写文件时碰到权限问题。
5、fork孙子进程,子进程退出
经过上面几个步骤后,子进程会成为新的进程组老大,可以重新申请打开终端,为了避免这个问题,fork孙子进程出来。
6、重定向孙子进程的标准输入流、标准输出流、标准错误流到/dev/null
因为是守护进程,本身已经脱离了终端,那么标准输入流、标准输出流、标准错误流就没有什么意义了。所以都转向到/dev/null,就是都丢弃的意思。
7. 处理SIGCHLD信号
处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。
signal(SIGCHLD,SIG_IGN);
参考资料:
http://www.01happy.com/linux-python-daemon/
http://www.cnblogs.com/mickole/p/3188321.html
http://blog.youkuaiyun.com/jason314/article/details/5640969
针对守护这个概念延伸到python,python也有守护线程的概念
python daemon理解:
守护进程只与主进程有相互作用关系;主线程结束为前提,守护进程保证所有的子线程都执行完后就全部退出,即使守护进程没有执行完进程也会退出;
参考资料:
http://www.dongwm.com/archives/guanyuthreadingyanjiuer/