1. 引言
上一篇文章中,我们看到了如何通过 multiprocessing 来创建子进程。 通过 multiprocessing 实现 python 多进程
接下来我们来详细了解一下多个进程之间如何进行通信和同步。
2. 进程间通信
我们曾经介绍过 UNIX 环境中多个进程如何相互通信。
主要包含:
- 信号
- 管道
- FIFO
- 消息队列
- 信号量
- 共享内存
- 域套接字
- socketpair
Python 作为跨平台的脚本语言,并没有实现上述所有进程间通信的方式,下面我们来一一介绍一下,本文我们主要来介绍信号机制。
3. 信号
UNIX 环境下,信号是一种非常有用且常用的机制,他实现了系统中断功能。 对于大部分信号,系统已经做了相应的处理,但除了几个信号(SIGKILL 和 SIGSTOP 等信号)以外,系统允许我们通过信号响应函数对相应信号发生后的行为进行重新定义,这是通知响应模式中最常见的处理方式。 在多进程环境中,通过向另一个进程发送预定的某个信号从而触发对于事件的响应,这是最为简单的一种进程间通信方式。 Python 也提供了信号处理的模块 — signal,虽然 Python 中的信号处理远没有 UNIX 环境中的强大。
4. UNIX 环境中的信号
我们曾经介绍过 UNIX 环境中的信号与处理方法。
4.0.1. POSIX.1-1990标准信号
POSIX.1-1990标准信号
信号 | 取值 | 默认动作 | 含义 |
---|---|---|---|
SIGHUP | 1 | Term | 终端的挂断或进程死亡 |
SIGINT | 2 | Term | 来自键盘的中断信号 |
SIGQUIT | 3 | Core | 来自键盘的离开信号 |
SIGILL | 4 | Core | 非法指令 |
SIGABRT | 6 | Core | 来自abort的异常信号 |
SIGFPE | 8 | Core | 浮点例外 |
SIGKILL | 9 | Term | 杀死 |
SIGSEGV | 11 | Core | 段非法错误(内存引用无效) |
SIGPIPE | 13 | Term | 管道损坏:向一个没有读进程的管道写数据 |
SIGALRM | 14 | Term | 来自alarm的计时器到时信号 |
SIGTERM | 15 | Term | 终止 |
SIGUSR1 | 30,10,16 | Term | 用户自定义信号1 |
SIGUSR2 | 31,12,17 | Term | 用户自定义信号2 |
SIGCHLD | 20,17,18 | Ign | 子进程停止或终止 |
SIGCONT | 19,18,25 | Cont | 如果停止,继续执行 |
SIGSTOP | 17,19,23 | Stop | 非来自终端的停止信号 |
SIGTSTP | 18,20,24 | Stop | 来自终端的停止信号 |
SIGTTIN | 21,21,26 | Stop | 后台进程读终端 |
SIGTTOU | 22,22,27 | Stop | 后台进程写终端 |
4.0.2. SUSv2和POSIX.1-2001定义的信号
SUSv2和POSIX.1-2001定义的信号
信号 | 取值 | 默认动作 | 含义 |
---|---|---|---|
SIGBUS | 10,7,10 | Core | 总线错误(内存访问错误) |
SIGPOLL | | Term | Pollable事件发生(Sys V),与SIGIO同义 |
SIGPROF | 27,27,29 | Term | 统计分布图用计时器到时 |
SIGSYS | 12,-,12 | Core | 非法系统调用(SVr4) |
SIGTRAP | 5 | Core | 跟踪/断点自陷 |
SIGURG | 16,23,21 | Ign | socket紧急信号(4.2BSD) |
SIGVTALRM | 26,26,28 | Term | 虚拟计时器到时(4.2BSD) |
SIGXCPU | 24,24,30 | Core | 超过CPU时限(4.2BSD) |
SIGXFSZ | 25,25,31 | Core | 超过文件长度限制(4.2BSD) |
4.0.3. 其他常见的信号
其他常见的信号
信号 | 取值 | 默认动作 | 含义 |
---|---|---|---|
SIGIOT | 6 | Core | IOT自陷,与SIGABRT同义 |
SIGEMT | 7,-,7 | Term | 表示一个实现定义的硬件错误信号 |
SIGSTKFLT | -,16,- | Term | 协处理器堆栈错误(不使用) |
SIGIO | 23,29,22 | Term | 描述符上可以进行I/O操作 |
SIGCLD | -,-,18 | Ign | 与SIGCHLD同义 |
SIGPWR | 29,30,19 | Term | 电力故障(System V) |
SIGINFO | 29,-,- | Term | 与SIGPWR同义 |
SIGLOST | -,-,- | Term | 文件锁丢失 |
SIGWINCH | 28,28,20 | Ign | 窗口大小改变(4.3BSD, Sun) |
SIGUNUSED | -,31,- | Term | 未使用信号(will be SIGSYS) |
Python 中的信号处理与 UNIX 原生的信号处理基本上是一致的,所有的常量、枚举、方法均被包含在标准库 signal 包中。
5. 信号枚举
signal 包定义了各个信号名及其对应的整数,比如:
代码语言:javascript复制import signal
print(signal.SIGABRT)
print(signal.SIGINT)
Python 中所用的信号名与值都和上面列表中 Linux 系统的值一致。
6. 预设信号处理函数 — signal
与 linux 原生信号机制一样,signal 方法是最核心的方法,他可以定义某个信号的响应方法,从而实现对信号中断的响应。
singnal.signal(signalnum, handler)
signalnum 是上述信号枚举中的一个,handler 则是我们需要定义的方法。
6.1. 默认 handler
与原生 linux 系统中一样,signal 包中同样提供了以下两个默认操作,可以作为 handler 参数传入 signal 方法:
- signal.SIG_DFL — 将该信号的响应恢复为系统默认处理方法
- signal.SIG_IGN — 忽略该信号
6.2. 示例
代码语言:javascript复制import logging
import signal
import time
def sighandler(signum, frame):
logging.info('signo: %s handled' % signum)
exit(0)
if __name__ == '__main__':
signal.signal(signal.SIGTERM, sighandler)
signal.signal(signal.SIGINT, sighandler)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
while True:
time.sleep(10)
执行程序,陷入了死循环,此时我们按下 CTRL C,打印出了:
^C2019-05-28 17:30:00,152 - INFO: signo: 2 handled
捕获并处理了 SIGINT 信号。
7. 阻塞等待信号
上面我们通过死循环 time.sleep 实现了进程的无限等待。 熟悉 linux 编程的同学都知道,系统早已实现了这一功能,python 也同样提供了相应的封装:
- pause() — 无限等待,直到信号到来
- sigwait(sigset) — 暂停执行调用现成,直到信号集中指定的信号到来,返回信号编号
- sigwaitinfo(sigset) — 暂停执行调用现成,直到信号集中指定的信号到来,返回信号信息对象
- sigtimedwait(sigset, timeout) — 具有超时的 sigwaitinfo
sigwait 与 pause 最大的不同在于 sigwait 在被信号中断后,并不会运行 signal 方法预设的响应函数,而是会自动继续运行。
7.1. 示例
7.1.1. pause
我们通过 pause 来改进上面的例子。
代码语言:javascript复制import logging
import signal
def sighandler(signum, frame):
logging.info('signo: %s handled' % signum)
exit(0)
if __name__ == '__main__':
signal.signal(signal.SIGTERM, sighandler)
signal.signal(signal.SIGINT, sighandler)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
sigset = {signal.SIGTERM, signal.SIGINT}
res = signal.pause()
logging.info('sigwait returned by %s' % res)
执行程序,陷入了等待,此时我们按下 CTRL C,打印出了:
^C2019-05-28 17:30:00,152 - INFO: signo: 2 handled
捕获并处理了 SIGINT 信号。
7.1.2. sigwait
我们再通过 sigwait 来实现。
代码语言:javascript复制import logging
import signal
def sighandler(signum, frame):
logging.info('signo: %s handled' % signum)
exit(0)
if __name__ == '__main__':
signal.signal(signal.SIGTERM, sighandler)
signal.signal(signal.SIGINT, sighandler)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
sigset = {signal.SIGTERM, signal.SIGINT}
res = signal.sigwait(sigset)
logging.info('sigwait returned by %s' % res)
执行程序,陷入了等待,此时我们按下 CTRL C,打印出了:
^C2019-05-29 10:50:33,828 - INFO: sigwait returned by Signals.SIGINT
可以看到,程序并没有去执行我们预设的响应函数,而是直接返回了信号枚举,并继续执行。
7.2. 使用哪一个
那么,问题来了,到底我们应该使用 pause 还是 sigwait 呢? 经典场景下,我们的守护进程完成初始化任务之后,设定好信号响应函数与信号屏蔽字,然后陷入死循环中的等待,一旦信号到来,就去执行默认响应函数,之后继续等待,这样的场景下,signal、pthread_sigmask、pause 的组合用起来是非常方便而得心应手的。 但是,另一个场景下,如果进程需要等待某个信号的发生,一旦信号发生,进程才能继续向下运行,此时使用上述方法则有着一个明显的问题,那就是如果在 signal 调用后 pause 调用前,信号就已经发生,则程序去自动运行预设响应函数,此后,执行 pause 进入无限的等待中,显然不是我们想要的,python 没有 unix 环境用来解决这个问题的 sigsuspend 方法,sigwait 就成了唯一的选择。
8. 获取或更改信号屏蔽字 — pthread_sigmask
上面的例子我们看到,使用 signal、pause 的方法组合可以配合信号响应函数实现中断处理。 但大部分信号都会中断 pause 的阻塞状态,而不仅仅是那些我们所关心的拥有响应函数的信号,有没有办法让我们的进程屏蔽掉那些我们不关心的信号,只让我们关心的那些信号来打破进程的阻塞呢? 答案当然是肯定的,我们可以通过 pthread_sigmask 方法预设信号屏蔽,从而实现仅对我们关心的信号的等待和响应。
8.1. 方法与参数
pthread_sigmask(how, mask)
how 参数有以下三种选择:
- SIG_BLOCK — 新增屏蔽信号集
- SIG_UNBLOCK — 从屏蔽信号集中删除集合
- SIG_SETMASK — 设置屏蔽字
mask 参数是信号枚举或数值的 set。 返回修改前的阻塞信号集,因此,如果传入 how 参数 为 SIG_BLOCK 或 SIG_UNBLOCK 同时 mask 参数为空,则该接口就变成了查询接口。
8.2. 示例
代码语言:javascript复制import logging
import os
import signal
def sighandler(signum, frame):
logging.info('signo: %s handled' % signum)
if __name__ == '__main__':
signal.signal(signal.SIGUSR1, sighandler)
signal.signal(signal.SIGUSR2, sighandler)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
logging.info('main proc started %s' % os.getpid())
signal.pthread_sigmask(signal.SIG_UNBLOCK, {signal.SIGUSR1})
signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGUSR2})
signal.pause()
执行打印出了:
2019-05-29 11:13:10,063 - INFO: main proc started 24742
此时,我们执行:
kill -SIGUSR2 24742
会发现进程没有受到任何影响。 而当我们执行:
kill -SIGUSR1 24742
打印出了。
2019-05-29 11:14:18,427 - INFO: signo: 10 handled
9. 发出信号
上面我们详细介绍了信号的响应,既然是进程间通信方法,那除了响应,同样重要的当然还有发出信号的过程了。
9.1. 向进程发出信号 — os.kill
kill(process_id, signalnum)
kill 方法并不是 signal 包中的方法,由于其通用性而被放到了 os 包中,用来向某个进程发出某个信号。
9.2. 向线程发出信号 — pthread_kill
pthread_kill(thread_id, signalnum)
pthread_kill 用来向同一个进程的其他线程发出信号,如果向某个线程发出信号,那么只有进程中的主线程会收到并处理信号,这是 Linux 本身的规范,此前我们已有过详细的介绍。