站内链接:

1. 进程组和作业

进程组

注意区分进程组和用户组的区别, 关于用户以及用户组的命令可以见用户组信息的介绍, 这里主要讲述进程组的一些知识点, 以及后续进程组和会话, 作业的关系.

  • 进程组: 进程组是一个或者多个进程的集合, 一般来说一个进程组就表示一个作业, 接收同一个终端的各类信号信息.
  • 进程组长: 每个进程组都有一个进程组 ID, 每一个进程组都有一个组长, 在大部分系统中, 进程组 ID 一般就是组长进程 ID.
  • 生存期: 进程组可以包含 1 个到 N 个进程, 只要还存在进程, 进程组就会一直存在. 从进程组创建到最后一个进程离开为止, 这个时间区间称为进程组的生存期.
1
2
3
4
5
6
7
8
# 使用管道测试一个进程组, 可以看到tail和sleep属于同一个进程组
tail -f tmp/unexec.txt | sleep 100 &
ps -ej|head -1; ps -ej |grep <pgid>

# 2. 发送sighup信息, 发现两个进程全部退出
kill -1 -<pgid>
# 例如杀死进程组:6666 的所有进程信息
kill -1 -6666

作业

作业(job)是一个进程组. 现代 shell 一般都支持作业控制, 作业分为前台作业(前台进程组), 后台作业(后台进程组), 当发生终端或者 IO 等操作时, 终端需要控制作业的转换.

  1. 启动多个作业并查看
1
2
3
4
# 测试
tail -f tmp/exec.sh &
sleep 100 &
jobs

其输出如下:

1
2
[1]-  Running                 tail -f tmp/exec.sh &
[2]+ Running sleep 100 &

其中[1], [2]表示作业号.

  1. 终端作业控制
1
2
3
4
5
6
7
8
# 1. 将stdin重定向到指定文件中
cat > temp.txt &
# 2. 查看进程状态
ps aux|head -1; ps aux|grep cat
# 3. 恢复停止的作业, 并输入信息, 并以ctrl-d退出编辑
fg 1
# 4. 查看temp.txt, 判断是不是第三步的输入信息
cat temp.txt

此时查看进程状态, 可以看到该作业被挂入后台, 而且进程状态变为T-停止状态等待重新运行

1
2
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root 10067 0.0 0.0 8904 760 pts/5 T 11:28 0:00 cat

终端控制前后台进程组的数据流动和函数调用, 参考<APUE>书籍: 终端会话控制

2. 会话和控制终端

信号

Singal, 软件终端, 程序可以通过处理信号来实现异步事件机制. 利用kill -l方法可以列出当前系统支持的信号信息.

1
2
3
4
5
6
7
8
9
10
11
12
13
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

后续就可以通过命令来像指定的进程发送信号:

1
2
3
4
# 1. 发送kill信号
kill -9 <pid>
# 2. 发送hup
kill -1 <pid>

会话

终于, 这里的会话是基于系统或者控制终端的会话, 而不是网络应用层或者网络连接的中会话.

  • 一个 session 是一个或者多个进程组的集合.
  • 一个 session 可以拥有一个终端, 也可以没有
  • 一个 session 都有一个会话首进程(通常为创建会话进程, 但可以变化), 首进程 ID 就是会话 ID
  • 一个 session 有一个前台进程组和多个后台进程组

下面来看下一个终端下的会话例子, 并使用ps来查看各个进程所属的会话.

1
2
3
4
5
6
# 1. 两个后台命令
tail -f exec.sh | cat | sleep 20000 &
sleep 1000 | cat > temp.txt

# 2. 查看session, pgid
ps -j

其输出如下:

1
2
3
4
5
6
7
  PID  PGID   SID TTY          TIME CMD
9863 9863 9863 pts/5 00:00:00 bash
10152 10152 9863 pts/5 00:00:00 tail
10153 10152 9863 pts/5 00:00:00 cat
10154 10152 9863 pts/5 00:00:00 sleep
10155 10155 9863 pts/5 00:00:00 sleep
10156 10155 9863 pts/5 00:00:00 cat

可以看到 4 个进程都属于同一个会话, 并且会话 ID 就是会话首进程 ID(bash), 其中进程组10155是前台进程组, 10152为后台进程组, 这两进程组都属于同一个会话9863. 下面看下上面 5 个进程的关系图:

会话进程组

其中:

  • 登录shell对应 9863 bash 进程
  • proc1,proc2 对应 10156, 10155, 他们是前台进程组
  • proc3, proc4, proc5 对应 10152, 10153, 10154

控制终端

在章节终端历史我们已经了解到终端的发展历史以及终端的基本概念. 这里主要从与会话和进程组的关系来讨论控制终端概念.

  1. 一个会话可以有一个控制终端(tty, pts), 使用 item2 本地启动 shell 或者远程 ssh 登录都可以
  2. 连接控制终端的会话首进程亦被称为控制进程, 会话首进程见 2.2 节
  3. 在终端上输入的任何信号都会自动发送给前台进程组
  4. 一旦终端被关闭或者检测到网络断开, 则会自动发送 sighup 信号, 该信号一般会关闭该回话所有进程

关闭终端关闭之后进程消失的问题, 即使使用command &后台挂起也不可以, 一般正常的的应用都会处理sighup, 所以一旦接收到该信号都会关闭自身应用. 如果想要阻止这种现象, 那就可以使用命令nohup commmand &, 其会忽略 sighup 信号, 其大概原理是:

  • nohup 创建父进程, 然后设置 sighup 信号屏蔽
  • 父进程随后 fork 子进程, 此时信号屏蔽字会被继续, 从而达到目的.

当然, 如果是开发者自己编写的程序, 则可以设置为守护进程方式启动运行. 下面是一些在终端上的信号测试工作:

  1. macos 系统: 本地使用 item2/terminal 启动终端, 并启动几个后台进程, 关闭终端的时候, 所有进程死
  2. windows: 使用 xshell 通过 ssh 连接 mac, 启动后台进程, 关闭终端的时候, 所有进程死
  3. 云服务器: 使用 ssh 连接远程(pts), 启动后台进程, 退出本次连接的时候, 后台进程变为孤儿进程
  4. 云服务器: 操作同 3, 但是向会话首进程发送 sighup, 所有进程死

前台进程组和控制终端关系如图: 控制终端

3. 守护进程

实现原理

OS 本身就拥有很多脱离控制终端的守护进程, 比如初始自举进程 init, inetd 守护进程, cron 守护进程等, 这些都是守护进程, 在正常情况下, 他们在系统自举时启动, 一直在系统后台运行, 直到系统关闭时才会终止, 可以通过命令ps -axj来查看守护进程信息.
那么守护进程是如何创建的呢? 创建守护进程有哪些基本步骤吗?

  1. 父进程 p1, 设置 umask 为 0, 从而确保后续子进程创建的文件权限为 777.
  2. p1 父进程 fork 子进程 pc1, 同时父进程 p1 退出, 这样做的目的见下文.
  3. 子进程 pc1 调用 setsid 创建新会话.
  4. 将当前工作目录设置为根目录
  5. 关闭不需要的文件描述符(清除无关资源)
  6. 设置/dev/null, 确保后续试图 stdin, stdout, stderr 的库例程都不会产生作用.

请注意守护进程和nohup命令的实现机制不同, 虽然他们都可能确保进程脱离终端在后台独立运行, 但是明显, 守护进程更加彻底的完成后台运行.

问题 1: 为什么父进程 p1 需要 fork 子进程之后自动关闭呢? 会话的创建和这个有什么关系?

  1. 父进程关闭确保祖父进程(例如 shell)以为该进程已经执行完毕
  2. 子进程继续pgid,实际就是父进程 ID, 确保了子进程不是进程组的组长, 为后续会话创立提供条件
  3. 创建新会话: 子进程成为会话首进程;子进程成为新的进程组组长;子进程不再有控制终端,脱离终端.

通过上面的操作, 使子进程在后台稳定的进行, 不再受终端等因素的干扰.

python 应用

自定义 daemon

下面是用代码实现的守护进程, 其中各个步骤都类似 3.1 节的基本步骤.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# coding:utf-8
import os
import sys
import atexit
import time
import signal
from signal import SIGTERM


class Daemon(object):
def __init__(self, pidfile, globalLog, stdin='/dev/null',
stdout='/dev/null', stderr='/dev/null'):
pass

def daemonize(self):
# 0. do first fork
try:
pid = os.fork()
if pid > 0:
sys.exit(0)
except OSError as e:
sys.exit(1)

# 1. decouple from parent environment
os.chdir("/")
os.setsid()
os.umask(0)

# 2. fork children and kill self.
try:
pid = os.fork()
if pid > 0:
sys.exit(0)
except OSError as e:
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())

# registrem destructor function
atexit.register(self.delpid)
# write pidfile
try:
fd = open(self.pidfile, 'w+')
fd.write('%s\n' % (str(os.getpid())))
fd.close()
os.chmod(self.pidfile, 0o777)
except IOError as msg:
self._globalLog.getError().log(
self._globalLog.ERROR,
"%s:open pidfile failed." %
msg)
sys.exit(1)

def delpid(self):
'''destructor function'''
os.remove(self.pidfile)

def sigInt(self, signum, frame):
pass

def start(self):
# 1.各种检查, 这里省略
pass
# 2.Start the daemon
self.daemonize();self.run()

def stop(self):
pass

def restart(self):
self.stop();self.start()

def run(self):
pass

Daemon 库

如果使用守护进程库实现守护进程, 那就很方便.

1
2
3
4
5
6
7
8
from daemon import Daemon

class pantalaimon(Daemon):
def run(self):
# Do stuff

pineMarten = pantalaimon('/path/to/pid.pid')
pineMarten.start()

multiprocessing

在多进程中, 可以使用multiprocessing来创建守护子进程.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import multiprocessing
import time
import sys

def deamon():
p = multiprocessing.current_process()
print('Daemon process:{} {}'.format(p.name, p.pid))
time.sleep(3)
sys.stdout.flush()


if __name__ == '__main__':
d = multiprocessing.Process(name='daemon', target=daemon)
d.daemon=True # 设置守护进程
d.start()
print('Main Process stop')

参考

文章参考:

本博客其他相关文章: