乐趣区

关于python3.x:编写python的daemon程序

以前把守护过程与后台任务搞混了,前面看了文章才晓得这两者的区别,写此文表白本人对守护过程的了解.

1: 什么是守护过程?

所谓守护过程是一种是 Linux 的一种长期运行的后盾服务过程,httpd、named、sshd 等服务都是以守护过程 Daemon 形式运行的,通常服务名称以字母d结尾,也就是 Daemon 第一个字母.

  1. 无需管制终端(不须要与用户交互)
  2. 在后盾运行
  3. 生命周期比拟长,个别是随系统启动和敞开
2: 守护过程必要性

通常咱们执行工作时是在前台执行,霸占了以后终端,此时无奈进行操作,就算咱们增加了 & 符号,将程序放到后盾,但也就因为终端断网等问题,导致程序中断。

所要晓得的是:在目前的 linux 上,有了 systemd 这个服务,这个服务管理工具能够不便咱们写在后盾运行的程序,甚至能够代替这种守护过程。通过把写服务的配置文件,让 systemd 监控咱们的程序,能够随系统启动而运行,能够设定启动条件,及其的不便。

3: 过程组
$ ps -o pid,pgid,ppid,comm | cat
  PID  PGID  PPID  COMMAND
10179  10179 10177 bash
10263  10263 10179 ps
10264  10263 10179 cat
  1. bash:过程和过程组 ID 都是 10179,父过程其实是 sshd(10177)
  2. ps:过程和过程组 ID 都是 10263,父过程是 bash(10179),因为是在 Shell 上执行的命令
  3. cat:过程组 ID 与 ps 的过程组 ID 雷同,父过程同样是 bash(10179)
4: 会话组

​ 多个过程形成一个过程组,而会话组是由多个过程组构建而。而过程组又被称为 job,会话有前台作业,也会有后台作业;一个会话能够有一个管制终端,当管制终端有输出和输入时都会传递给前台过程组,比方Ctrl + Z。会话的意义在于能将多个作业通过一个终端管制,一个前台操作,其它后盾运行。

那么如何编写守护过程呢?

其实编写守护过程很简略,只须要遵循一下几点即可

1: 创立子过程,父过程退出

PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
    0    49    49    49 pts/2       70 Ss       0   0:00 /bin/bash
   49    70    70    49 pts/2       70 R+       0   0:00  \_ ps axjf
    0    17    17    17 pts/1       68 Ss       0   0:00 /bin/bash
   17    68    68    17 pts/1       68 S+       0   0:00  \_ python hello.py
   68    69    68    17 pts/1       68 S+       0   0:00      \_ python hello.py
    0     1     1     1 pts/0        1 Ss+      0   0:00 /bin/bash

过程 fork 后,父过程退出。这么做的起因有 2 点:

  • 如果守护过程是通过 Shell 启动,父过程退出,Shell 就会认为工作执行结束,这时子过程由 init 收养
  • 子过程继承父过程的过程组 ID,保障了子过程不是过程组组长,因为后边调用 setsid() 要求必须不是过程组长
  • PGID 就是过程所属的 Group 的 Leader 的 PID,如果 PGID=PID,那么该过程是 Group Leader

2、子过程创立新会话

调用 setsid() 创立一个新的会话,并成为新会话组长。这个步骤次要是要与继承父过程的会话、过程组、终端脱离关系。

那么问题来了,为什么过程组组长无奈调用 setsid()呢?

对于过程组长来说,过程组 ID 曾经和 PID 雷同了,如果它被容许调用 setsid() 的话,它的过程组 ID 会放弃不变,会呈现:

1: 过程组长属于新的会话;

2: 老的过程组成员属于旧的会话。

这样状况变成了一个过程组的成员属于不同的会话,Linux 想要禁止这种状况的产生。

3、禁止子过程从新关上终端

此刻子过程是会话组长,为了避免子过程从新关上终端,再次 fork 后退出父过程,也就是此子过程。这时子过程 2 不再是会话组长,无奈再关上终端。其实这一步骤不是必须的,不过加上这一步骤会显得更加谨严。

4、设置当前目录为根目录

如果守护过程的当前工作目录是 /usr/home 目录,那么管理员在卸载 /usr 分区时会报错的。为了防止这个问题,能够调用 chdir() 函数将工作目录设置为根目录/

5、设置文件权限掩码

文件权限掩码是指屏蔽掉文件权限中的对应位。因为应用 fork()函数新建的子过程继承了父过程的文件权限掩码,这就给该子过程应用文件带来了诸多的麻烦。因而,把文件权限掩码设置为 0,能够大大加强该守护过程的灵活性。通常应用办法是umask(0)

6、敞开文件描述符

子过程会继承曾经关上的文件,它们占用系统资源,且可能导致所在文件系统无奈卸载。此时守护过程与终端脱离,常说的输出、输入、谬误描述符也应该敞开,毕竟这个时候也不会应用终端了。

守护过程的出错解决

因为守护过程脱离了终端,不能将错误信息输入到管制终端,即便 gdb 也无奈失常调试。罕用的办法是应用 syslog 服务,将错误信息输出到 /var/log/messages 中。

syslog 是 Linux 中的系统日志治理服务,通过守护过程 syslogd 来保护。该守护过程在启动时会读一个配置文件/etc/syslog.conf。该文件决定了不同品种的音讯会发送向何处。

代码展现

import os
import sys


def daemonize(pid_file=None):
    pid = os.fork()
    if pid:
        sys.exit(0)
    os.setsid()

    _pid = os.fork()
    if _pid:
        sys.exit(0)

    os.umask(0)
    os.chdir('/')
    sys.stdout.flush()
    sys.stderr.flush()

    with open('/dev/null') as read_null, open('/dev/null','w') as write_null:
        os.dup2(read_null.fileno(), sys.stdin.fileno())
        os.dup2(write_null.fileno(), sys.stdout.fileno())
        os.dup2(write_null.fileno(), sys.stderr.fileno())

    if pid_file:
        with open(pid_file,'w+') as f:
            f.write(str(os.getpid()))

if __name__ == "__main__":
    daemonize('test.txt')

对于 os.dup2 这个函数

os.dup2() 办法用于将一个文件描述符 fd 复制到另一个 fd2。

Unix, Windows 上可用。

>>> import os
>>> f = open("hello.txt","a")
>>> os.dup2(f.fileno(),1)
>>> f.close()
>>> print("hello world")
>>> print("changed")
cat hello.txt
1
hello world
changed

附加话题

为什么服务器端经常 fork 两次呢?

因为这是为了防止产生僵尸过程。

当咱们只 fork()一次后,存在父过程和子过程。这时有两种办法来防止产生僵尸过程:

  • 父过程调用 waitpid()等函数来接管子过程退出状态。
  • 父过程先完结,子过程则主动托管到 Init 过程(pid = 1)。

    目前先思考 子过程先于父过程完结 的状况:

  • 若父过程未解决子过程退出状态,在父过程退出前,子过程始终处于僵尸过程状态。
  • 若父过程调用 waitpid()(这里应用阻塞调用确保子过程先于父过程完结)来期待子过程完结,将会使父过程在调用 waitpid()后进入睡眠状态,只有子过程完结父过程的 waitpid()才会返回。如果存在子过程完结,但父过程还未执行到 waitpid()的状况,那么这段期间子过程也将处于僵尸过程状态。

    由此,能够看出父过程与子过程有父子关系,除非 保障父过程先于子过程完结 或者 保障父过程在子过程完结前执行 waitpid(),子过程均有机会成为僵尸过程。那么如何使父过程更不便地创立不会成为僵尸过程的子过程呢?这就要用两次 fork()了。

    父过程一次 fork()后产生一个子过程随后立刻执行 waitpid(子过程 pid, NULL, 0)来期待子过程完结,而后子过程 fork()后产生孙子过程随后立刻 exit(0)。这样子过程顺利终止(父过程仅仅给子过程收尸,并不需要子过程的返回值),而后父过程继续执行。这时的孙子过程因为失去了 它的父过程(即是父过程的子过程),将被转交给 Init 过程托管。于是父过程与孙子过程无继承关系了,它们的父过程均为 Init,Init 过程在其子过程完结时会主动收尸,这样也就不会产生僵尸过程了。

退出移动版