如果我们远程登录了远程的 Linux 服务器,运行了一些耗时较长的任务,如何让命令提交后不受本地关闭终端窗口/网络断开连接的干扰呢?
守护进程
守护进程,也即通常所说的 Daemon 进程,是 Linux 下一种特殊的后台服务进程,它独立于控制终端并且周期性的执行某种任务或者等待处理某些发生的事件。守护进程的名称通常以 “d” 结尾,如 “httpd”、“crond”、“mysqld”等. Redis 可以通过修改配置文件以 Daemon方式运行.
在 Linux 中,由终端登录系统登入系统后会得到一个 shell 进程,这个终端便成为这个 shell 进程的控制终端(Controlling Terminal)。shell 进程启动的其他进程,由于复制了父进程的信息,因此也都同依附于这个控制终端。终端关闭,相应的进程都会自动关闭。守护进程脱离终端的目的,也即是不受终端变化的影响不被终端打断,当然也不想在终端显示执行过程中的信息。
如果不想进程受到用户、终端或其他变化的影响,就必须把它变成守护进程。
实现守护进程
通过一些特殊命令实现 Daemon 进程
nohup
如果想让某一条长时间运行的命令不受终端退出的影响,可以使用nohup命令.
The nohup utility invokes utility with its arguments and at this time sets the signal SIGHUP to be ignored.If the standard output is a termi-nal, the standard output is appended to the filenohup.outin the current directory.If standard error is a terminal, it is directed to the same place as the standard output.
我们知道,当用户注销(logout)或者网络断开时,终端会收到 HUP(hangup)信号从而关闭其所有子进程.而 nohup 的功能就是让提交的命令忽略 hangup 信号
使用了 nohump 后,标准输出和标准错误缺省会被重定向到 nohup.out 文件中。一般我们可在结尾加上"&"来将命令同时放入后台运行,也可用">filename2>&1"来更改缺省的重定向文件名。
setsid()
setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
通过编程,让进程直接以 Daemon 方式运行
通常实现一个 Daemon 很简单,几步就可以实现,我们看一下Redis的实现.
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); }}
- 第一步,先fork 一个子进程,然后退出原有程序,这样子进程就变成一个孤儿进程.由 init做为他的父进程.从而在形式上脱离控制终端的控制。
- 调用setsid ,由新创建的子进程创建一个新的会话,并成为这个会话的 Lader.
- 将原有的stdin,stdout,stderr,都定向到 /dev/null.
以上三步是 Redis实现的 Daemon步骤.
此外,Redis 还将进程 ID 保存到 PID 文件里.这里介绍一下 PID 文件的作用.
在linux系统的目录/var/run下面一般我们都会看到很多的*.pid文件。而且往往新安装的程序在运行后也会在/var/run目录下面产生自己的pid文件。那么这些pid文件有什么作用呢?它的内容又是什么呢?
(1) pid文件的内容:pid文件为文本文件,内容只有一行, 记录了该进程的ID。 用cat命令可以看到。 (2) pid文件的作用:防止进程启动多个副本。只有获得pid文件(固定路径固定文件名)写入权限(F_WRLCK)的进程才能正常启动并把自身的PID写入该文件中。其它同一个程序的多余进程则自动退出。
调用chdir
改变当前目录为根目录.
由于进程运行过程中,当前目录所在的文件系统(如:“/mnt/usb”)是不能卸载的,为避免对以后的使用造成麻烦,改变工作目录为根目录是必要的。如有特殊需要,也可以改变到特定目录,如“/tmp”。
重设文件权限掩码
fork
函数创建的子进程,继承了父进程的文件操作权限,为防止对以后使用文件带来问题,需要。文件权限掩码,设定了文件权限中要屏蔽掉的对应位。这个跟文件权限的八进制数字模式表示差不多,将现有存取权限减去权限掩码(或做异或运算),就可产生新建文件时的预设权限。调用 umask
设置文件权限掩码,通常是重设为 0,清除掩码,这样可以大大增强守护进程的灵活性。
下面是一个用 Python 实现一个Daemon 进程
import sys, os, time, atexitfrom 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(). """
这段代码完整的实现了 Deamon 进程,相对于 Redis,修改了文件掩码,当前文件目录,还使用了 Double Fork技术防止僵尸进程.