操作系统:守护进程, 进程会话介绍
站内链接:
1. 进程组和作业
进程组
注意区分进程组和用户组的区别, 关于用户以及用户组的命令可以见用户组信息的介绍, 这里主要讲述进程组的一些知识点, 以及后续进程组和会话, 作业的关系.
进程组
: 进程组是一个或者多个进程的集合, 一般来说一个进程组就表示一个作业
, 接收同一个终端的各类信号信息.进程组长
: 每个进程组都有一个进程组 ID, 每一个进程组都有一个组长, 在大部分系统中, 进程组 ID 一般就是组长进程 ID
.生存期
: 进程组可以包含 1 个到 N 个进程, 只要还存在进程, 进程组就会一直存在. 从进程组创建到最后一个进程离开为止, 这个时间区间称为进程组的生存期.
1 | # 使用管道测试一个进程组, 可以看到tail和sleep属于同一个进程组 |
作业
作业(job)是一个进程组. 现代 shell 一般都支持作业控制, 作业分为前台作业(前台进程组), 后台作业(后台进程组), 当发生终端或者 IO 等操作时, 终端需要控制作业的转换.
- 启动多个作业并查看
1 | # 测试 |
其输出如下:
1 | [1]- Running tail -f tmp/exec.sh & |
其中[1], [2]
表示作业号.
- 终端作业控制
1 | # 1. 将stdin重定向到指定文件中 |
此时查看进程状态, 可以看到该作业被挂入后台, 而且进程状态变为T-停止状态
等待重新运行
1 | USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND |
终端控制前后台进程组的数据流动和函数调用, 参考<APUE>
书籍:
2. 会话和控制终端
信号
Singal, 软件终端, 程序可以通过处理信号来实现异步事件机制. 利用kill -l
方法可以列出当前系统支持的信号信息.
1 | 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP |
后续就可以通过命令来像指定的进程发送信号:
1 | # 1. 发送kill信号 |
会话
终于, 这里的会话是基于系统或者控制终端的会话, 而不是网络应用层或者网络连接的中会话.
- 一个 session 是一个或者多个进程组的集合.
- 一个 session 可以拥有一个终端, 也可以没有
- 一个 session 都有一个会话首进程(通常为创建会话进程, 但可以变化), 首进程 ID 就是会话 ID
- 一个 session 有一个前台进程组和多个后台进程组
下面来看下一个终端下的会话例子, 并使用ps
来查看各个进程所属的会话.
1 | # 1. 两个后台命令 |
其输出如下:
1 | PID PGID SID TTY TIME CMD |
可以看到 4 个进程都属于同一个会话, 并且会话 ID 就是会话首进程 ID(bash), 其中进程组10155
是前台进程组, 10152
为后台进程组, 这两进程组都属于同一个会话9863
. 下面看下上面 5 个进程的关系图:
其中:
登录shell
对应 9863 bash 进程- proc1,proc2 对应 10156, 10155, 他们是前台进程组
- proc3, proc4, proc5 对应 10152, 10153, 10154
控制终端
在章节终端历史我们已经了解到终端的发展历史以及终端的基本概念. 这里主要从与会话和进程组的关系来讨论控制终端
概念.
- 一个会话可以有一个控制终端(tty, pts), 使用 item2 本地启动 shell 或者远程 ssh 登录都可以
- 连接控制终端的
会话首进程
亦被称为控制进程
, 会话首进程见 2.2 节 - 在终端上输入的任何信号都会自动发送给
前台进程组
- 一旦终端被关闭或者检测到网络断开, 则会自动发送 sighup 信号, 该信号一般会关闭该回话所有进程
关闭终端关闭之后进程消失的问题, 即使使用command &
后台挂起也不可以, 一般正常的的应用都会处理sighup
, 所以一旦接收到该信号都会关闭自身应用. 如果想要阻止这种现象, 那就可以使用命令nohup commmand &
, 其会忽略 sighup 信号, 其大概原理是:
- nohup 创建父进程, 然后设置 sighup 信号屏蔽
- 父进程随后 fork 子进程, 此时信号屏蔽字会被继续, 从而达到目的.
当然, 如果是开发者自己编写的程序, 则可以设置为守护进程方式启动运行. 下面是一些在终端上的信号测试工作:
- macos 系统: 本地使用 item2/terminal 启动终端, 并启动几个后台进程, 关闭终端的时候, 所有进程死
- windows: 使用 xshell 通过 ssh 连接 mac, 启动后台进程, 关闭终端的时候, 所有进程死
- 云服务器: 使用 ssh 连接远程(pts), 启动后台进程, 退出本次连接的时候, 后台进程变为孤儿进程
- 云服务器: 操作同 3, 但是向会话首进程发送 sighup, 所有进程死
前台进程组和控制终端关系如图:
3. 守护进程
实现原理
OS 本身就拥有很多脱离控制终端的守护进程, 比如初始自举进程 init, inetd 守护进程, cron 守护进程等, 这些都是守护进程, 在正常情况下, 他们在系统自举
时启动, 一直在系统后台运行, 直到系统关闭时才会终止, 可以通过命令ps -axj
来查看守护进程信息.
那么守护进程是如何创建的呢? 创建守护进程有哪些基本步骤吗?
- 父进程 p1, 设置 umask 为 0, 从而确保后续子进程创建的文件权限为 777.
- p1 父进程 fork 子进程 pc1, 同时父进程 p1 退出, 这样做的目的见下文.
- 子进程 pc1 调用 setsid 创建新会话.
- 将当前工作目录设置为根目录
- 关闭不需要的文件描述符(清除无关资源)
- 设置/dev/null, 确保后续试图 stdin, stdout, stderr 的库例程都不会产生作用.
请注意守护进程和nohup
命令的实现机制不同, 虽然他们都可能确保进程脱离终端在后台独立运行, 但是明显, 守护进程更加彻底的完成后台运行
.
问题 1: 为什么父进程 p1 需要 fork 子进程之后自动关闭呢? 会话的创建和这个有什么关系?
- 父进程关闭确保祖父进程(例如 shell)以为该进程已经执行完毕
- 子进程继续
pgid,实际就是父进程 ID
, 确保了子进程不是进程组的组长, 为后续会话创立提供条件 - 创建新会话: 子进程成为会话首进程;子进程成为新的进程组组长;子进程不再有控制终端,脱离终端.
通过上面的操作, 使子进程在后台稳定的进行, 不再受终端等因素的干扰.
python 应用
自定义 daemon
下面是用代码实现的守护进程, 其中各个步骤都类似 3.1 节的基本步骤.
1 | # coding:utf-8 |
Daemon 库
如果使用守护进程库实现守护进程, 那就很方便.
1 | from daemon import Daemon |
multiprocessing
在多进程中, 可以使用multiprocessing
来创建守护子进程.
1 | import multiprocessing |
参考
文章参考:
本博客其他相关文章: