python 进程间通信(一) -- 信号的基本使用

2019-05-29 14:36:29   最后更新: 2019-05-29 14:36:29   访问数量:102




上一篇文章中,我们看到了如何通过 multiprocessing 来创建子进程

通过 multiprocessing 实现 python 多进程

 

接下来我们来详细了解一下多个进程之间如何进行通信和同步

 

我们曾经介绍过 UNIX 环境中多个进程如何相互通信

UNIX 环境高级编程

 

主要包含:

  1. 信号
  2. 管道
  3. FIFO
  4. 消息队列
  5. 信号量
  6. 共享内存
  7. 域套接字
  8. socketpair

 

Python 作为跨平台的脚本语言,并没有实现上述所有进程间通信的方式,下面我们来一一介绍一下,本文我们主要来介绍信号机制

 

 

UNIX 环境下,信号是一种非常有用且常用的机制,他实现了系统中断功能

对于大部分信号,系统已经做了相应的处理,但除了几个信号(SIGKILL 和 SIGSTOP 等信号)以外,系统允许我们通过信号响应函数对相应信号发生后的行为进行重新定义,这是通知响应模式中最常见的处理方式

在多进程环境中,通过向另一个进程发送预定的某个信号从而触发对于事件的响应,这是最为简单的一种进程间通信方式

Python 也提供了信号处理的模块 -- signal,虽然 Python 中的信号处理远没有 UNIX 环境中的强大

 

我们曾经介绍过 UNIX 环境中的信号与处理方法

UNIX信号的基本概念及列表

 

POSIX.1-1990标准信号

POSIX.1-1990标准信号
信号取值默认动作含义
SIGHUP1Term终端的挂断或进程死亡
SIGINT2Term来自键盘的中断信号
SIGQUIT3Core来自键盘的离开信号
SIGILL4Core非法指令
SIGABRT6Core来自abort的异常信号
SIGFPE8Core浮点例外
SIGKILL9Term杀死
SIGSEGV11Core段非法错误(内存引用无效)
SIGPIPE13Term管道损坏:向一个没有读进程的管道写数据
SIGALRM14Term来自alarm的计时器到时信号
SIGTERM15Term终止
SIGUSR130,10,16Term用户自定义信号1
SIGUSR231,12,17Term用户自定义信号2
SIGCHLD20,17,18Ign子进程停止或终止
SIGCONT19,18,25Cont如果停止,继续执行
SIGSTOP17,19,23Stop非来自终端的停止信号
SIGTSTP18,20,24Stop来自终端的停止信号
SIGTTIN21,21,26Stop后台进程读终端
SIGTTOU22,22,27Stop后台进程写终端

 

SUSv2和POSIX.1-2001定义的信号

SUSv2和POSIX.1-2001定义的信号
信号取值默认动作含义
SIGBUS10,7,10Core总线错误(内存访问错误)
SIGPOLL TermPollable事件发生(Sys V),与SIGIO同义
SIGPROF27,27,29Term统计分布图用计时器到时
SIGSYS12,-,12Core非法系统调用(SVr4)
SIGTRAP5Core跟踪/断点自陷
SIGURG16,23,21Ignsocket紧急信号(4.2BSD)
SIGVTALRM26,26,28Term虚拟计时器到时(4.2BSD)
SIGXCPU24,24,30Core超过CPU时限(4.2BSD)
SIGXFSZ25,25,31Core超过文件长度限制(4.2BSD)

 

其他常见的信号

其他常见的信号
信号取值默认动作含义
SIGIOT6CoreIOT自陷,与SIGABRT同义
SIGEMT7,-,7Term表示一个实现定义的硬件错误信号
SIGSTKFLT-,16,-Term协处理器堆栈错误(不使用)
SIGIO23,29,22Term描述符上可以进行I/O操作
SIGCLD-,-,18Ign与SIGCHLD同义
SIGPWR29,30,19Term电力故障(System V)
SIGINFO29,-,-Term与SIGPWR同义
SIGLOST-,-,-Term文件锁丢失
SIGWINCH28,28,20Ign窗口大小改变(4.3BSD, Sun)
SIGUNUSED-,31,-Term未使用信号(will be SIGSYS)

 

Python 中的信号处理与 UNIX 原生的信号处理基本上是一致的,所有的常量、枚举、方法均被包含在标准库 signal 包中

 

signal 包定义了各个信号名及其对应的整数,比如:

import signal print(signal.SIGABRT) print(signal.SIGINT)

 

 

Python 中所用的信号名与值都和上面列表中 Linux 系统的值一致

 

与 linux 原生信号机制一样,signal 方法是最核心的方法,他可以定义某个信号的响应方法,从而实现对信号中断的响应

singnal.signal(signalnum, handler)

 

signalnum 是上述信号枚举中的一个,handler 则是我们需要定义的方法

 

默认 handler

与原生 linux 系统中一样,signal 包中同样提供了以下两个默认操作,可以作为 handler 参数传入 signal 方法:

  • signal.SIG_DFL -- 将该信号的响应恢复为系统默认处理方法
  • signal.SIG_IGN -- 忽略该信号

 

示例

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 信号

 

上面我们通过死循环 + time.sleep 实现了进程的无限等待

熟悉 linux 编程的同学都知道,系统早已实现了这一功能,python 也同样提供了相应的封装:

  1. pause() -- 无限等待,直到信号到来
  2. sigwait(sigset) -- 暂停执行调用现成,直到信号集中指定的信号到来,返回信号编号
  3. sigwaitinfo(sigset) -- 暂停执行调用现成,直到信号集中指定的信号到来,返回信号信息对象
  4. sigtimedwait(sigset, timeout) -- 具有超时的 sigwaitinfo

 

sigwait 与 pause 最大的不同在于 sigwait 在被信号中断后,并不会运行 signal 方法预设的响应函数,而是会自动继续运行

 

示例

pause

我们通过 pause 来改进上面的例子

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 信号

 

sigwait

我们再通过 sigwait 来实现

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

 

可以看到,程序并没有去执行我们预设的响应函数,而是直接返回了信号枚举,并继续执行

 

使用哪一个

那么,问题来了,到底我们应该使用 pause 还是 sigwait 呢?

经典场景下,我们的守护进程完成初始化任务之后,设定好信号响应函数与信号屏蔽字,然后陷入死循环中的等待,一旦信号到来,就去执行默认响应函数,之后继续等待,这样的场景下,signal、pthread_sigmask、pause 的组合用起来是非常方便而得心应手的

但是,另一个场景下,如果进程需要等待某个信号的发生,一旦信号发生,进程才能继续向下运行,此时使用上述方法则有着一个明显的问题,那就是如果在 signal 调用后 pause 调用前,信号就已经发生,则程序去自动运行预设响应函数,此后,执行 pause 进入无限的等待中,显然不是我们想要的,python 没有 unix 环境用来解决这个问题的 sigsuspend 方法,sigwait 就成了唯一的选择

 

上面的例子我们看到,使用 signal、pause 的方法组合可以配合信号响应函数实现中断处理

但大部分信号都会中断 pause 的阻塞状态,而不仅仅是那些我们所关心的拥有响应函数的信号,有没有办法让我们的进程屏蔽掉那些我们不关心的信号,只让我们关心的那些信号来打破进程的阻塞呢?

答案当然是肯定的,我们可以通过 pthread_sigmask 方法预设信号屏蔽,从而实现仅对我们关心的信号的等待和响应

 

方法与参数

pthread_sigmask(how, mask)

 

how 参数有以下三种选择:

  1. SIG_BLOCK -- 新增屏蔽信号集
  2. SIG_UNBLOCK -- 从屏蔽信号集中删除集合
  3. SIG_SETMASK -- 设置屏蔽字

 

mask 参数是信号枚举或数值的 set

返回修改前的阻塞信号集,因此,如果传入 how 参数 为 SIG_BLOCK  或 SIG_UNBLOCK  同时 mask 参数为空,则该接口就变成了查询接口

 

示例

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

 

上面我们详细介绍了信号的响应,既然是进程间通信方法,那除了响应,同样重要的当然还有发出信号的过程了

 

向进程发出信号 -- os.kill

kill(process_id, signalnum)

 

kill 方法并不是 signal 包中的方法,由于其通用性而被放到了 os 包中,用来向某个进程发出某个信号

 

向线程发出信号 -- pthread_kill

pthread_kill(thread_id, signalnum)

 

pthread_kill 用来向同一个进程的其他线程发出信号,如果向某个线程发出信号,那么只有进程中的主线程会收到并处理信号,这是 Linux 本身的规范,此前我们已有过详细的介绍

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,全部原创,只有干货没有鸡汤

 

 






技术帖      python      进程间通信      进程      ipc      signal      信号     


京ICP备15018585号