11.3. signal — 同步系统事件
目的:同步系统事件
信号是一种操作系统的功能,它提供了一种通知事件程序并使其异步处理的方法。他们可以由系统本身生成或者从一个进程发送到另一个进程。由于信号会中断程序的常规流程,因此如果在操作中间接收到信号,某些操作(尤其是I/O)可能会产生错误。
信号由整数标识,并在操作系统中 C 的头文件中定义。Python 将适配于各个平台的信号定义在了 signal
模块的常量中。这个模块中使用了 SIGINT
以及 SIGUSR1
。两者通常都是针对所有 Unix 和 类 Unix 系统定义。
提醒
Unix 信号编程是一项不小的工作。本篇只是一个介绍,并不包括在每个平台上成功使用信号所需的详细信息。不同的 Unix 版本虽然有一定程度的标准化,但是也有一些变化,所以如果遇到麻烦,请查阅操作系统文档。
接收信号
与其他形式的基于事件编程一样,通过创建一个称之为 信号处理器 的回调函数接收信号,这个函数在信号发生时调用。信号处理器的参数是信号编号以及被信号中断的那个时间点的程序堆栈帧。
signal_signal.py
import signal
import os
import time
def receive_signal(signum, stack):
print('Received:', signum)
# 注册信号处理器
signal.signal(signal.SIGUSR1, receive_signal)
signal.signal(signal.SIGUSR2, receive_signal)
# 打印进程id,它可以备用 'kill' 用来发送信号
print('My PID is:', os.getpid())
while True:
print('Waiting...')
time.sleep(3)
这个例子无限循环,每次中断几秒。当信号到来时,sleep()
调用被中断,同时信号处理函数 receive_signal
打印出来了信号编号。在信号处理器返回之后,循环继续。
可以使用 os.kill()
或者 Unix 命令行程序 kill
给运行中的进程发送信号。
$ python3 signal_signal.py
My PID is: 71387
Waiting...
Waiting...
Waiting...
Received: 30
Waiting...
Waiting...
Received: 31
Waiting...
Waiting...
Traceback (most recent call last):
File "signal_signal.py", line 28, in <module>
time.sleep(3)
KeyboardInterrupt
前面的输出是由运行 signal_signal.py
在一个窗口中产生的,然后另一个窗口运行:
$ kill -USR1 $pid
$ kill -USR2 $pid
$ kill -INT $pid
检索注册的信号处理器
为了去查看某个信号注册了哪个信号处理器,可以使用 getsignal()
函数。传入信号编号作为参数。返回值是注册的信号处理器,或者特殊值 SIG_IGN
(如果信号被忽略),SIG_DFL
(默认信号处理行为),或者 None
(如果存在的信号处理器是从 C 注册的,而不是 Python)。
signal_getsignal.py
import signal
def alarm_received(n, stack):
return
signal.signal(signal.SIGALRM, alarm_received)
signals_to_names = {
getattr(signal, n): n
for n in dir(signal)
if n.startswith('SIG') and '_' not in n
}
for s, name in sorted(signals_to_names.items()):
handler = signal.getsignal(s)
if handler is signal.SIG_DFL:
handler = 'SIG_DFL'
elif handler is signal.SIG_IGN:
handler = 'SIG_IGN'
print('{:<10} ({:2d}):'.format(name, s), handler)
再次声明,因为每个系统可能有不同的信号,下面的输出因系统而异。这个例子来自于 OS X:
$ python3 signal_getsignal.py
SIGHUP ( 1): SIG_DFL
SIGINT ( 2): <built-in function default_int_handler>
SIGQUIT ( 3): SIG_DFL
SIGILL ( 4): SIG_DFL
SIGTRAP ( 5): SIG_DFL
SIGIOT ( 6): SIG_DFL
SIGEMT ( 7): SIG_DFL
SIGFPE ( 8): SIG_DFL
SIGKILL ( 9): None
SIGBUS (10): SIG_DFL
SIGSEGV (11): SIG_DFL
SIGSYS (12): SIG_DFL
SIGPIPE (13): SIG_IGN
SIGALRM (14): <function alarm_received at 0x1019a6a60>
SIGTERM (15): SIG_DFL
SIGURG (16): SIG_DFL
SIGSTOP (17): None
SIGTSTP (18): SIG_DFL
SIGCONT (19): SIG_DFL
SIGCHLD (20): SIG_DFL
SIGTTIN (21): SIG_DFL
SIGTTOU (22): SIG_DFL
SIGIO (23): SIG_DFL
SIGXCPU (24): SIG_DFL
SIGXFSZ (25): SIG_IGN
SIGVTALRM (26): SIG_DFL
SIGPROF (27): SIG_DFL
SIGWINCH (28): SIG_DFL
SIGINFO (29): SIG_DFL
SIGUSR1 (30): SIG_DFL
SIGUSR2 (31): SIG_DFL
发送信号
来自 Python 的信号发送函数是 os.kill()
。它的使用在模块 os
介绍,使用 os.fork()
创建进程.
警报
警报是一种特殊的信号,程序要求操作系统在一段时间之后再去通知它。由于标准模块 os
的文档支出,这对于在系统 I / O 操作或者其他系统调用中无限阻塞。
signal_alarm.py
import signal
import time
def receive_alarm(signum, stack):
print('Alarm :', time.ctime())
# Call receive_alarm in 2 seconds
signal.signal(signal.SIGALRM, receive_alarm)
signal.alarm(2)
print('Before:', time.ctime())
time.sleep(4)
print('After :', time.ctime())
这个例子中,sleep()
的调用被中断了。但在信号处理之后继续执行,sleep()
之后打印的消息显示程序暂停了至少 sleep
的时间。
$ python3 signal_alarm.py
Before: Sat Apr 22 14:48:57 2017
Alarm : Sat Apr 22 14:48:59 2017
After : Sat Apr 22 14:49:01 2017
忽略信号
为了忽略信号,注册 SIG_IGN
为信号处理方法。这个脚本中将信号 SIGINT
的处理方法替换为 SIG_IGN
,并且为信号 SIGUSR1
注册了一个处理方法。然后使用 signal.pause()
去等待接收一个信号。
signal_ignore.py
import signal
import os
import time
def do_exit(sig, stack):
raise SystemExit('Exiting')
signal.signal(signal.SIGINT, signal.SIG_IGN)
signal.signal(signal.SIGUSR1, do_exit)
print('My PID:', os.getpid())
signal.pause()
正常的 SIGINT
(通过 Ctrl-C
由shell 发送给 程序的信号)会引发 KeyboardInterrupt
。 这个例子中忽略了 SIGINT
然而当接收到信号 SIGUSR1
时会引起 SystemExit
。输出中的每个 ^C
表示尝试在终端使用 Ctrl-C
杀死程序。从另一个终端使用 kill -USR1 72598
最终造成程序退出。
$ python3 signal_ignore.py
My PID: 72598
^C^C^C^CExiting
信号和线程
信号和线程通常不会很好结合在一起,因为只有进程的主线程才会接受信号。下列的列子设置了一个信号处理方法,在一个线程中等待信号到达,然后从另一个线程中发送信号。
signal_threads.py
import signal
import threading
import os
import time
def signal_handler(num, stack):
print('Received signal {} in {}'.format(
num, threading.currentThread().name))
signal.signal(signal.SIGUSR1, signal_handler)
def wait_for_signal():
print('Waiting for signal in',
threading.currentThread().name)
signal.pause()
print('Done waiting')
# 启动一个不会接收信号的线程
receiver = threading.Thread(
target=wait_for_signal,
name='receiver',
)
receiver.start()
time.sleep(0.1)
def send_signal():
print('Sending signal in', threading.currentThread().name)
os.kill(os.getpid(), signal.SIGUSR1)
sender = threading.Thread(target=send_signal, name='sender')
sender.start()
sender.join()
# 等待线程看到信号(不会发生的)
print('Waiting for', receiver.name)
signal.alarm(2)
receiver.join()
信号处理程序都在主线程中注册,因为这是 Python 的 signal
模块实现的要求,无论底层平台是否支持线程和信号混合开发。尽管接收线程调用了 signal.pause()
,但是它不会接收到信号。脚本结束位置的 signal.alarm(2)
阻止了无限循环,否则接收者线程永远不会退出。
$ python3 signal_threads.py
Waiting for signal in receiver
Sending signal in sender
Received signal 30 in MainThread
Waiting for receiver
Alarm clock
虽然可以在任何线程中设置警报,但是他们总是在主线程中接收。
signal_threads_alarm.py
import signal
import time
import threading
def signal_handler(num, stack):
print(time.ctime(), 'Alarm in',
threading.currentThread().name)
signal.signal(signal.SIGALRM, signal_handler)
def use_alarm():
t_name = threading.currentThread().name
print(time.ctime(), 'Setting alarm in', t_name)
signal.alarm(1)
print(time.ctime(), 'Sleeping in', t_name)
time.sleep(3)
print(time.ctime(), 'Done with sleep in', t_name)
# 开启一个不会接收到信号的线程
alarm_thread = threading.Thread(
target=use_alarm,
name='alarm_thread',
)
alarm_thread.start()
time.sleep(0.1)
# 等待线程看到信号(不会发生)
print(time.ctime(), 'Waiting for', alarm_thread.name)
alarm_thread.join()
print(time.ctime(), 'Exiting normally')
警报不会终止 use_alarm()
中的 sleep()
调用。
$ python3 signal_threads_alarm.py
Sat Apr 22 14:49:01 2017 Setting alarm in alarm_thread
Sat Apr 22 14:49:01 2017 Sleeping in alarm_thread
Sat Apr 22 14:49:01 2017 Waiting for alarm_thread
Sat Apr 22 14:49:02 2017 Alarm in MainThread
Sat Apr 22 14:49:04 2017 Done with sleep in alarm_thread
Sat Apr 22 14:49:04 2017 Exiting normally
推荐阅读
- signal 标准库文档
- PEP 475 -- 当使用
EINTR
系统调用是还时重试subprocess
-- 更多关于发送信号到进程的例子。- 使用 os.fork() 创建进程 --
kill()
函数可以用于在进程间发送信号。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。