戏说守护、僵尸、孤儿进程

首先说简单的结论:

  • 没有父进程的进程就是孤儿进程,孤儿进程会被init领养,成为一个准守护进程。

  • 如果进程他爹活着,但是不给子进程收尸(wait、waitpid),子进程就会变成僵尸。

守护进程(Daemon)是在一类脱离终端在后台执行的程序, 通常以 d 结尾, 随系统启动, 其父进程 (ppid) 通常是 init 进程

以下是Wikipedia中关于Daemon的定义:

In multitasking computer operating systems, a daemon (/ˈdiːmən/ or /ˈdeɪmən/) is a computer program that runs as a background process, rather than being under the direct control of an interactive user. Traditionally daemon names end with the letter d: for example, syslogd is the daemon that implements the system logging facility and sshd is a daemon that services incoming SSH connections.

一般要让当前程序以守护进程形式运行, 在命令后加 & 并重定向输出即可:
$ nohup some_program > /dev/null 2>&1 &

这是直接运行程序的方式, 如果是用具体语言代码的形式来实现呢, 总的来说守护进程应该有以下几个特征:

  • 后台运行
  • 也就是不占用console的前面,也就是bash里运行程序后面加个&
  • 成为process group leader
  • Process Group Leader就是父进程是init的那个进程。
  • 成为session leader
  • 一个ssh登录会启动一个bash,bash会fork出很多子进程,这些进程轮流接手tty输出。这都是属于一个session。 session leader就是这一堆进程的父进程。
  • fork一次或者两次
  • fork 两次是出于被当成库调用的考虑。
  • chdir到/
  • 防止占用别的路径的working dir的fd,导致一些block不能unmount
  • umask
  • 需要重置umask,防止后续子进程继承非默认umask造成奇怪的行为。
  • 处理标准输入输出,错误输出(0,1,2)
  • 重定向stdout、stderr、stdin,防止tty中断后的broken pipe信号。
  • 日志
  • 输出重定向后,需要有办法反映内部情况。
  • 信号处理
  • 最后最好对将一些终端相关的信号处理忽略一下,防止受到相关信号导致的进程退出。例如:SIGHUP、SIGTTIN、SIGTTOU。这是很多没有经验的菜鸟容易忽略的点。一般nohup命令会帮我们处理。

用下面的代码就可以实现一个非常规范的守护进程(代码注释很详细但有点长):

#!/usr/bin/env python

import sys, os, time, atexit
from signal import SIGTERM

class Daemon:
  """
  A generic daemon class.
   
  Usage: subclass the Daemon class and override the run() method
  """
  def __init__(self, pidfile='nbMon.pid', stdin='/dev/null', stdout='nbMon.log', stderr='nbMon.log'):
    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().
    """

可以看一个示例:

#!/usr/bin/env python
# coding=utf-8

from daemon import Daemon
import socket
import time

html = """HTTP/1.1 200 OK\r\nContent-Type: image/jpeg\r\nConnection: close\r\nContent-Length: """
html404 = """HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\nContent-Length: 13\r\n\r\n<h1>404 </h1>"""

class agentD(Daemon):
  def run(self):
    listen_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
    listen_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    listen_fd.bind(("0.0.0.0", 9000))
    listen_fd.listen(10)
    while True:
      conn, addr = listen_fd.accept()
      print "coming", conn, addr
      read_data = conn.recv(10000)
      #print read_data
      try:
        pic_name = read_data.split(" ")[1][1:]
        print pic_name
        with file(pic_name) as f:
          pic_content = f.read()
          length = len(pic_content)
          html_resp = html
          html_resp += "%d\r\n\r\n" % (length)
          print html_resp
          html_resp += pic_content
      except:
        print "404 occur"
        html_resp = html404
      
      while len(html_resp) > 0: 
        sent_cnt = conn.send(html_resp)
        print "sent:", sent_cnt
        html_resp = html_resp[sent_cnt:]
      conn.close()

if __name__ == "__main__":
  agentd = agentD(pidfile="agentd.pid", stdout="agentd.log", stderr="agentd.log")
  agentd.run()

实现了一个非常蹩脚的HTTP Server :-P

上面守护进程的生成步骤中涉及到了孤儿进程:任何孤儿进程产生时都会立即为系统进程init自动接收为子进程,这一过程也被称为“收养”。但由于创建该进程的进程已不存在,所以仍应称之为“孤儿进程(Orphan Process)”。

与之相关的一个概念就是 僵尸进程(Zombie Process)了。当子进程退出时, 父进程需要wait/waitpid系统调用来读取子进程的exit status, 然后子进程被系统回收。如果父进程没有wait的话, 子进程将变成一个"僵尸进程", 内核会释放这个子进程所有的资源,包括打开的文件占用的内存等。但在进程表中仍然有一个PCB, 记录进程号和退出状态等信息, 并导致进程号一直被占用, 而系统能使用的进程号数量是有限的(可以用ulimit查看相关限制), 如果产生大量僵尸进程的话, 将因为没有可用的进程号而导致系统不能产生新的进程。

因此很多自带重启功能的服务实现就是用wait/waitpid实现的。waitpid()会暂时停止目前进程的执行,直到有信号来到或子进程结束。比如tornado中fork多进程就是这样, 监控子进程的运行状态, 当其意外退出时自动重启子进程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容

  • 一,守护进程概述Linux Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某...
    柏树_Jeff阅读 2,933评论 0 5
  • Linux 进程管理与程序开发 进程是Linux事务管理的基本单元,所有的进程均拥有自己独立的处理环境和系统资源,...
    JamesPeng阅读 2,449评论 1 14
  • 基本概念 Linux Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务...
    lifesmily阅读 927评论 0 0
  • 端午假期,我来到了向往的草原,到达的那刻发了一个朋友圈:一路北上,我会在这里拍我的花儿、赶我的牛、烤我的全羊、住我...
    晓多阅读 1,208评论 9 14
  • 请写出下面几个alert的执行结果 请写出以下程序的输出 一个数组par中存放有多个人员的信息,每个人员的心心由年...
    ps_fba7阅读 636评论 0 0