Python中的信号机制初窥
Start with an example
A piece of code
先看一段代码:
from multiprocessing import Process, Manager
from time import sleep
def f(process_number):
try:
print "starting thread: ", process_number
while True:
print process_number
sleep(3)
except KeyboardInterrupt:
print "Keyboard interrupt in process: ", process_number
finally:
print "cleaning up thread", process_number
if __name__ == '__main__':
processes = []
manager = Manager()
for i in xrange(4):
p = Process(target=f, args=(i,))
p.start()
processes.append(p)
try:
for process in processes:
process.join()
except KeyboardInterrupt:
print "Keyboard interrupt in main"
finally:
print "Cleaning up Main"
运行后按ctrl-c
,可以看到:
^C
Keyboard interrupt in process: 3
Keyboard interrupt in process: 0
Keyboard interrupt in process: 2
cleaning up thread 3
cleaning up thread 0
cleaning up thread 2
Keyboard interrupt in process: 1
cleaning up thread 1
Keyboard interrupt in main
Cleaning up Main
可以看到这里利用KeyboardInterrupt
使所有进程都得以比较优雅地退出。这种退出进程的方法确实值得学习,但它仅限于进程是通过ctrl-c
的方式结束的情况,假如我调用的是Process.terminate()
方法来结束这些进程的话,该如何优雅地退出呢?
What just happened
我们先要弄明白上面的这段代码到底发生了什么:
- 当我们按下
ctrl-c
后会在主进程触发一个signal.SIGINT
信号,然后主进程会把这个信号逐一发布给其所有的子进程(Signals are propagated down the process tree)。 signal.SIGINT
信号default就绑定了一个handler方法,这个handler方法会raiseKeyboardInterrupt
。所以所有的进程都会产生一个KeyboardInterrupt
异常。(换个角度理解,default绑定的handler方法就像是操作系统的预安装的软件,在你起任何一个python进程的时候就执行了绑定的代码。)- 我们抓住了
KeyboardInterrupt
异常,然后就可以做进程退出前的一些工作了。
The signal machinism in Python
Signal handler
信号的handler方法其实是一个回调函数,触发了某个信号后才会去调用对应handler方法。(可以理解为有一个线程会专门接收和处理信号,一旦这个线程接收到信号就会调用其handler方法来处理)
Python标准库中提供了两个现成的handler方法:signal.SIG_DFL
和signal.SIG_IGN
。前者是使用每个信号default的handler方法(各个信号不一样),后者是ignore这个信号,即绑定了这个handler后,接收到此信号就和没接收到时一样。
Write a signal handler
一个signal的handler必须要有两个输入参数,第一个是信号对应的数值,第二个是接收到信号时stack的信息。写好handler后要通过signal.signal()
来绑定signal和handler方法。
例子如下:
import signal
def signal_handler(signum, stack):
print 'Received:', signum
signal.signal(signal.SIGINT, signal_handler)
if __name__ == '__main__':
while True:
print 'Waiting...try kill using signal 2(SIGINT)'
time.sleep(3)
需要注意的是一旦绑定好handler之后,从主进程创建的子进程中此信号和此handler也是绑定的(因为是从主进程fork出来的进程嘛)。
另外,SIGKILL
和SIGSTOP
是不能绑定handler的(cannot be caught, blocked, or ignored)。
Understand from another angle
用Pycharm来run上述的代码,然后强制Stop(⌘F2)程序。你会发现按一次Stop按钮后,屏幕上输出了:
Waiting...try kill using signal 2(SIGINT)
Received: 2
Waiting...try kill using signal 2(SIGINT)
Waiting...try kill using signal 2(SIGINT)
Waiting...try kill using signal 2(SIGINT)
程序并没有结束,而绑定的你的signal handler被调用了一次。说明第一次Stop,Pycharm是发送了SIGINT
到主进程(就和你执行程序时按ctrl-c
一样)。而后再按一次Stop按钮,程序直接就结束了。这一次Stop,Pycharm是发送SIGKILL
到主进程,进程直接被强制kill掉了。正是由于SIGKILL
无法绑定handler,从而保证了进程可以被杀死。
Exit the process gracefully with other signals
参考前文的ctrl-c
的例子,利用异常也可以让Process.terminate()
来优雅地结束子进程:
from multiprocessing import Process
import signal
import time
class TerminateInterrupt(BaseException): pass
def signal_handler(signum, stack):
print 'Capture terminate signal.'
raise TerminateInterrupt
class MyProcess(Process):
def __init__(self):
super(MyProcess, self).__init__()
self.a = 0
def run(self):
signal.signal(signal.SIGTERM, signal_handler)
try:
while True:
self.a += 1
time.sleep(1)
except TerminateInterrupt:
print 'Exit the process.'
if __name__ == '__main__':
p = MyProcess()
p.start()
time.sleep(5)
p.terminate()
p.join()
执行上述代码结果为:
Capture terminate signal.
Exit the process.
注意这里是在run
方法里面对signal.SIGTERM
信号进行的绑定,因为run
方法里面已经是子进程的空间了,如果是在__init__
方法做这个绑定的话,主进程其实也被一并绑定了(这样子进程是通过fork自然拥有这个绑定的)。
由于信号的处理是异步回调的操作,所以使用异常来通知进程结束是最为方便的做法。不使用异常也是可以的,比如设一个变量表示是否结束,while语句中需要对这个变量进行判断,而signal handler用来修改这个变量的值即可(这种在子进程中每隔一段时间轮询是否退出的方式要更加优雅一些,这样保证了一个操作轮回的结束,而使用信号产生异常的方式则很可能在操作当中就直接结束了,导致操作只进行了一半(想象下如果这个操作是发送一条数据,发送的数据不完整是很致命的))。
Comments