守护进程原理及Python实现

守护进程,不依赖于终端,在后台运行的程序,通常称为daemon(ˈdiːmən或ˈdeɪmən)。

一些常见的Linux软件通常都是已守护进程的方式运行,比如:

nginx

redis

memcached

守护进程的原理:

通过fork() 复刻出子进程,并通过setsid()创建新会话,成为会话首领;同时结束原来的父进程,使得复刻出来的子进程脱离终端而运行。

守护进程Python代码实现:

def daemon_start(self):
try:
# 第1次fork,并结束父进程
pid = os.fork()
if pid > 0:
sys.exit(0)
except Exception as e:
sys.exit(1)
# 创建新会话,并成为会话首领
os.setsid()
os.chdir(self.workdir)
os.umask(self.umask)
try:
# 第2次fork,结束当前这个子进程,fork出来的孙子进程由于不是进程首领,无法再次获取终端(这里的子进程,孙子进程都是相对于最开始的那个初始进程而言)
pid = os.fork()
if pid > 0:
sys.exit(0)
except Exception as e:
sys.exit(1)
def handle_exit(signum, _):
sys.exit(0)
# 孙子进程注册信号处理方式
signal.signal(signal.SIGINT, handle_exit)
signal.signal(signal.SIGTERM, handle_exit)
signal.signal(signal.SIGHUP, signal.SIG_IGN)
# 孙子进程是守护进程,不存在标准输入输出,所以关闭。
sys.stdin.close()

核心函数说明:

os.fork(): 对进程进行复刻;值得特别注意的是fork之后,原来的进程并没有终止,而是继续存在,被成为父进程;也就是说,在fork成功后,一共会存在2个进程,1个是原来的进程,称为父进程,1个是新创建的进程,称为子进程。父进程和子进程都会从fork的位置开始继续向下执行,不同的是父进程中,得到的fork返回值为子进程的进程号,而子进程中得到的是0。通过这个返回值,就能判断哪个是父进程,哪个是子进程。以上这点值得特别注意,这与我们以往理解的程序执行逻辑完全不同。

os.setsid():创建新的会话,并成为会话首领。

os.chdir():修改当前工作目录路径,防止目录被移除导致守护进程异常。

os.umask():设置文件创建模式屏蔽字,使得创建文件不受系统默认权限的影响。

常见问题:

1.第1次fork子进程已经脱离终端,为什么还要第2次fork,第2次fork是否必须?

第2次fork并不是必须的,实际上,很多流行的开源软件的守护进程并没有进行第2次fork。第2次fork的目的在于防止第1次fork出来的进程再次获得终端,第2次fork后,产生的孙子进程不再是会话首领,也就没有再次获得终端的能力。

void daemonize(void) {
int fd;
if (fork() != 0) exit(0); /* parent exits */
setsid(); /* create a new session */
/* Every output goes to /dev/null. If Redis is daemonized but
* the 'logfile' is set to 'stdout' in the configuration file
* it will not log at all. */
if ((fd = open("/dev/null", O_RDWR, 0)) != -1) {
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
if (fd > STDERR_FILENO) close(fd);
}
}

2.进程已经脱离终端,如何让它停止或者重启?

每一个进程都有一个进程id,即pid,通常程序启动后,会把pid写入到/var/run/目录下的某个文件里,通过发送信号量给pid,即可操作相关进程。示例代码中的“进程注册信号处理方式”就是用来响应信号量的,守护进程可以针对不同的信号,做出不同的反应。