共计 3473 个字符,预计需要花费 9 分钟才能阅读完成。
以前把守护过程与后台任务搞混了,前面看了文章才晓得这两者的区别,写此文表白本人对守护过程的了解.
1: 什么是守护过程?
所谓守护过程是一种是 Linux 的一种长期运行的后盾服务过程,httpd、named、sshd 等服务都是以守护过程 Daemon 形式运行的,通常服务名称以字母d结尾,也就是 Daemon 第一个字母.
- 无需管制终端(不须要与用户交互)
- 在后盾运行
- 生命周期比拟长,个别是随系统启动和敞开
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
- bash:过程和过程组 ID 都是 10179,父过程其实是 sshd(10177)
- ps:过程和过程组 ID 都是 10263,父过程是 bash(10179),因为是在 Shell 上执行的命令
- 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 过程在其子过程完结时会主动收尸,这样也就不会产生僵尸过程了。