守护进程原理及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后,产生的孙子进程不再是会话首领,也就没有再次获得终端的能力。
来看看Redis是如何实现守护进程的:redis/server.c at cb51bb4320d2240001e8fc4a522d59fb28259703 · antirez/redis · GitHub
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,即可操作相关进程。示例代码中的“进程注册信号处理方式”就是用来响应信号量的,守护进程可以针对不同的信号,做出不同的反应。