signal
— 为异步事件设置处理程序¶
源代码: Lib/signal.py
此模块提供了在 Python 中使用信号处理程序的机制。
通用规则¶
signal.signal()
函数允许定义自定义处理程序,以便在收到信号时执行。 安装了少量默认处理程序:SIGPIPE
被忽略(因此管道和套接字上的写入错误可以作为普通的 Python 异常报告),如果父进程没有更改 SIGINT
,则将其转换为 KeyboardInterrupt
异常。
一旦设置了特定信号的处理程序,它将保持安装状态,直到显式重置(无论底层实现如何,Python 都会模拟 BSD 样式接口),但 SIGCHLD
的处理程序除外,它遵循底层实现。
在 WebAssembly 平台上,信号是模拟的,因此行为有所不同。 并非所有函数和信号都可以在这些平台上使用。
Python 信号处理程序的执行¶
Python 信号处理程序不会在底层 (C) 信号处理程序内部执行。相反,底层信号处理程序会设置一个标志,该标志告诉 虚拟机 在稍后的某个时间点(例如,在下一个 字节码 指令)执行相应的 Python 信号处理程序。这会产生以下后果
捕获 C 代码中的无效操作引起的
SIGFPE
或SIGSEGV
等同步错误没有意义。 Python 将从信号处理程序返回到 C 代码,C 代码很可能会再次引发相同的信号,从而导致 Python 明显挂起。 从 Python 3.3 开始,您可以使用faulthandler
模块来报告同步错误。完全在 C 中实现的长时间运行的计算(例如对大量文本进行正则表达式匹配)可能会不受任何信号的影响而运行任意长的时间。 计算完成后,将调用 Python 信号处理程序。
如果处理程序引发异常,则会在主线程中“凭空”引发该异常。 有关讨论,请参阅下面的说明。
信号和线程¶
Python 信号处理程序始终在主解释器的 Python 主线程中执行,即使信号是在另一个线程中接收的。 这意味着信号不能用作线程间通信的手段。 您可以使用 threading
模块中的同步原语代替。
此外,只允许主解释器的主线程设置新的信号处理程序。
模块内容¶
在 3.5 版本中更改: 下面列出的信号 (SIG*)、处理程序 (SIG_DFL
、SIG_IGN
) 和信号掩码 (SIG_BLOCK
、SIG_UNBLOCK
、SIG_SETMASK
) 相关常量被转换为 枚举
(分别为 Signals
、Handlers
和 Sigmasks
)。 getsignal()
、pthread_sigmask()
、sigpending()
和 sigwait()
函数返回可读的 枚举
作为 Signals
对象。
signal 模块定义了三个枚举
- class signal.Signals¶
SIG* 常量和 CTRL_* 常量的
enum.IntEnum
集合。3.5 版本中新增。
- class signal.Handlers¶
常量
SIG_DFL
和SIG_IGN
的enum.IntEnum
集合。3.5 版本中新增。
- class signal.Sigmasks¶
常量
SIG_BLOCK
、SIG_UNBLOCK
和SIG_SETMASK
的enum.IntEnum
集合。可用性: Unix。
有关更多信息,请参阅手册页 sigprocmask(2) 和 pthread_sigmask(3)。
3.5 版本中新增。
在 signal
模块中定义的变量有:
- signal.SIG_DFL¶
这是两种标准信号处理选项之一;它将简单地执行该信号的默认功能。例如,在大多数系统上,
SIGQUIT
的默认操作是转储核心并退出,而SIGCHLD
的默认操作是简单地忽略它。
- signal.SIG_IGN¶
这是另一种标准信号处理程序,它将简单地忽略给定的信号。
- signal.SIGFPE¶
浮点异常。例如,除以零。
另请参阅
当除法或模运算的第二个参数为零时,会引发
ZeroDivisionError
。
- signal.SIGILL¶
非法指令。
- signal.SIGINT¶
来自键盘的中断(CTRL + C)。
默认操作是引发
KeyboardInterrupt
。
- signal.SIGSEGV¶
段错误:无效的内存引用。
- signal.SIGSTKFLT¶
协处理器上的堆栈故障。Linux 内核不会引发此信号:它只能在用户空间中引发。
在 3.11 版本中添加。
- signal.SIGTERM¶
终止信号。
- SIG*
所有信号编号都是以符号方式定义的。 例如,挂断信号定义为
signal.SIGHUP
;变量名称与 C 程序中使用的名称相同,如<signal.h>
中所示。 ‘signal()
’ 的 Unix man 手册列出了现有信号(在某些系统上是 signal(2),在其他系统上,列表在 signal(7) 中)。 请注意,并非所有系统都定义了相同的信号名称集;此模块仅定义系统定义的那些名称。
- signal.NSIG¶
比最高信号编号大 1 的数字。使用
valid_signals()
获取有效的信号编号。
- signal.ITIMER_VIRTUAL¶
仅当进程正在执行时才递减间隔计时器,并在到期时传递 SIGVTALRM。
- signal.ITIMER_PROF¶
当进程执行以及系统代表进程执行时,都递减间隔计时器。与 ITIMER_VIRTUAL 结合使用,此计时器通常用于分析应用程序在用户空间和内核空间中花费的时间。到期时传递 SIGPROF。
- signal.SIG_BLOCK¶
pthread_sigmask()
的 how 参数的可能值,指示要阻止信号。在 3.3 版本中添加。
- signal.SIG_UNBLOCK¶
pthread_sigmask()
的 how 参数的可能值,指示要取消阻止信号。在 3.3 版本中添加。
- signal.SIG_SETMASK¶
pthread_sigmask()
的 how 参数的可能值,指示要替换信号掩码。在 3.3 版本中添加。
signal
模块定义了一个异常:
- exception signal.ItimerError¶
当底层
setitimer()
或getitimer()
实现发生错误时引发此异常。如果向setitimer()
传递了无效的间隔定时器或负时间,则会引发此错误。此错误是OSError
的子类型。
signal
模块定义了以下函数
- signal.alarm(time)¶
如果 time 为非零值,此函数请求在 time 秒后向进程发送
SIGALRM
信号。任何先前计划的警报都会被取消(任何时候只能计划一个警报)。返回的值是任何先前设置的警报被传递之前的秒数。如果 time 为零,则不会计划警报,并且任何计划的警报都会被取消。如果返回值是零,则当前没有计划任何警报。
- signal.getsignal(signalnum)¶
返回信号 signalnum 的当前信号处理程序。 返回的值可以是可调用的 Python 对象,或者特殊值
signal.SIG_IGN
、signal.SIG_DFL
或None
之一。 这里,signal.SIG_IGN
表示该信号先前被忽略,signal.SIG_DFL
表示先前正在使用默认的信号处理方式,而None
表示先前的信号处理程序不是从 Python 安装的。
- signal.strsignal(signalnum)¶
返回信号 signalnum 的描述,例如
SIGINT
的 “中断”。 如果 signalnum 没有描述,则返回None
。如果 signalnum 无效,则引发ValueError
。3.8 版本新增。
- signal.valid_signals()¶
返回此平台上有效的信号编号集合。如果系统保留某些信号以供内部使用,则此值可能小于
range(1, NSIG)
。3.8 版本新增。
- signal.pause()¶
使进程休眠直到收到信号;然后将调用适当的处理程序。不返回任何值。
另请参阅
sigwait()
,sigwaitinfo()
,sigtimedwait()
和sigpending()
。
- signal.raise_signal(signum)¶
向调用进程发送信号。不返回任何值。
3.8 版本新增。
- signal.pidfd_send_signal(pidfd, sig, siginfo=None, flags=0)¶
将信号 sig 发送到由文件描述符 pidfd 引用的进程。Python 当前不支持 siginfo 参数;它必须是
None
。flags 参数是为将来的扩展提供的;目前没有定义标志值。有关更多信息,请参阅 pidfd_send_signal(2) 手册页。
可用性: Linux >= 5.1,Android >=
build-time
API 级别 313.9 版本新增。
- signal.pthread_kill(thread_id, signalnum)¶
将信号 signalnum 发送到线程 thread_id,即与调用者在同一进程中的另一个线程。目标线程可以执行任何代码(Python 或非 Python)。但是,如果目标线程正在执行 Python 解释器,则 Python 信号处理程序将由主解释器的主线程执行。因此,向特定 Python 线程发送信号的唯一目的是强制运行的系统调用以
InterruptedError
失败。使用
threading.get_ident()
或ident
属性threading.Thread
对象来获取 thread_id 的合适值。如果 signalnum 为 0,则不会发送信号,但仍会执行错误检查;这可用于检查目标线程是否仍在运行。
引发带有参数
thread_id
,signalnum
的 审计事件signal.pthread_kill
。可用性: Unix。
有关更多信息,请参阅手册页 pthread_kill(3)。
另请参阅
os.kill()
。在 3.3 版本中添加。
- signal.pthread_sigmask(how, mask)¶
获取和/或更改调用线程的信号掩码。信号掩码是当前阻止向调用者传递的信号集。以信号集的形式返回旧的信号掩码。
调用的行为取决于 how 的值,如下所示。
SIG_BLOCK
: 被阻止的信号集是当前集和 mask 参数的并集。SIG_UNBLOCK
: 从当前被阻止的信号集中删除 mask 中的信号。可以尝试取消阻止未被阻止的信号。SIG_SETMASK
: 被阻止的信号集设置为 mask 参数。
mask 是一组信号编号(例如 {
signal.SIGINT
,signal.SIGTERM
})。使用valid_signals()
获取包含所有信号的完整掩码。例如,
signal.pthread_sigmask(signal.SIG_BLOCK, [])
读取调用线程的信号掩码。SIGKILL
和SIGSTOP
无法被阻止。可用性: Unix。
有关更多信息,请参阅手册页 sigprocmask(2) 和 pthread_sigmask(3)。
另请参阅
pause()
,sigpending()
和sigwait()
。在 3.3 版本中添加。
- signal.setitimer(which, seconds, interval=0.0)¶
设置由 which 指定的给定间隔计时器(
signal.ITIMER_REAL
,signal.ITIMER_VIRTUAL
或signal.ITIMER_PROF
中的一个),在 seconds 秒后触发(接受浮点数,与alarm()
不同),之后每隔 interval 秒触发一次(如果 interval 非零)。 可以通过将 seconds 设置为零来清除由 which 指定的间隔计时器。当间隔计时器触发时,会向进程发送一个信号。发送的信号取决于正在使用的计时器;
signal.ITIMER_REAL
将传递SIGALRM
,signal.ITIMER_VIRTUAL
发送SIGVTALRM
,并且signal.ITIMER_PROF
将传递SIGPROF
。旧值以元组形式返回:(延迟, 间隔)。
尝试传递无效的间隔计时器将导致
ItimerError
。可用性: Unix。
- signal.set_wakeup_fd(fd, *, warn_on_full_buffer=True)¶
将唤醒文件描述符设置为 fd。当收到信号时,信号编号将作为单个字节写入 fd。库可以使用它来唤醒轮询或选择调用,从而允许完全处理信号。
返回旧的唤醒 fd(如果未启用文件描述符唤醒,则返回 -1)。如果 fd 为 -1,则禁用文件描述符唤醒。如果不是 -1,则 fd 必须是非阻塞的。库有责任在再次调用 poll 或 select 之前从 fd 中删除任何字节。
启用线程时,此函数只能从 主解释器的主线程 调用;尝试从其他线程调用它会导致引发
ValueError
异常。有两种常见的方式来使用此函数。在这两种方法中,你都使用 fd 在信号到达时唤醒,但它们在如何确定 哪个 或 哪些 信号已到达方面有所不同。
在第一种方法中,我们从 fd 的缓冲区中读取数据,字节值会给你信号编号。这很简单,但在极少数情况下可能会遇到问题:通常 fd 的缓冲区空间有限,如果太多信号到达太快,那么缓冲区可能会被填满,并且可能会丢失一些信号。如果使用此方法,则应设置
warn_on_full_buffer=True
,这至少会在信号丢失时导致向 stderr 打印警告。在第二种方法中,我们仅将唤醒 fd 用于唤醒,而忽略实际的字节值。在这种情况下,我们关心的只是 fd 的缓冲区是空的还是非空的;完整的缓冲区根本不表示问题。如果使用此方法,则应设置
warn_on_full_buffer=False
,以便用户不会因虚假的警告消息而感到困惑。在 3.5 版本中更改: 在 Windows 上,该函数现在也支持套接字句柄。
在 3.7 版本中更改: 添加了
warn_on_full_buffer
参数。
- signal.siginterrupt(signalnum, flag)¶
更改系统调用重启行为:如果 flag 为
False
,则当被信号 signalnum 中断时,系统调用将重新启动,否则系统调用将被中断。不返回任何内容。可用性: Unix。
有关更多信息,请参阅手册页 siginterrupt(3)。
请注意,使用
signal()
安装信号处理程序将通过隐式调用siginterrupt()
并为给定信号使用 true flag 值,将重启行为重置为可中断。
- signal.signal(signalnum, handler)¶
将信号 signalnum 的处理程序设置为函数 handler。handler 可以是接受两个参数的可调用 Python 对象(见下文),或者可以是特殊值
signal.SIG_IGN
或signal.SIG_DFL
。将返回之前的信号处理程序(请参阅上面getsignal()
的描述)。(有关更多信息,请参阅 Unix 手册页 signal(2)。)启用线程时,此函数只能从 主解释器的主线程 调用;尝试从其他线程调用它会导致引发
ValueError
异常。调用 handler 时会传入两个参数:信号编号和当前堆栈帧(
None
或帧对象;有关帧对象的描述,请参见 类型层次结构中的描述 或查看inspect
模块中的属性描述)。在 Windows 上,只能使用
SIGABRT
,SIGFPE
,SIGILL
,SIGINT
,SIGSEGV
,SIGTERM
或SIGBREAK
调用signal()
。在任何其他情况下都会引发ValueError
。请注意,并非所有系统都定义了相同的信号名称集;如果信号名称未定义为SIG*
模块级常量,则会引发AttributeError
。
- signal.sigpending()¶
检查等待传递到调用线程的信号集(即,在阻塞时引发的信号)。返回待处理信号集。
可用性: Unix。
有关更多信息,请参阅手册页 sigpending(2)。
另请参阅
pause()
,pthread_sigmask()
和sigwait()
。在 3.3 版本中添加。
- signal.sigwait(sigset)¶
暂停调用线程的执行,直到传递信号集 sigset 中指定的某个信号。 该函数接受该信号(将其从待处理信号列表中移除),并返回信号编号。
可用性: Unix。
更多信息请参阅手册页 sigwait(3)。
另请参阅
pause()
、pthread_sigmask()
、sigpending()
、sigwaitinfo()
和sigtimedwait()
。在 3.3 版本中添加。
- signal.sigwaitinfo(sigset)¶
暂停调用线程的执行,直到传递信号集 sigset 中指定的某个信号。该函数接受该信号并将其从待处理信号列表中移除。如果 sigset 中的某个信号已经处于调用线程的待处理状态,该函数将立即返回有关该信号的信息。不会为传递的信号调用信号处理程序。如果该函数被 sigset 中未包含的信号中断,则会引发
InterruptedError
异常。返回值是一个表示
siginfo_t
结构中包含的数据的对象,即:si_signo
,si_code
,si_errno
,si_pid
,si_uid
,si_status
,si_band
。可用性: Unix。
更多信息请参阅手册页 sigwaitinfo(2)。
另请参阅
pause()
、sigwait()
和sigtimedwait()
。在 3.3 版本中添加。
在 3.5 版本中更改: 如果被 sigset 中未包含的信号中断,并且信号处理程序没有引发异常,则该函数现在会重试(有关基本原理,请参阅 PEP 475)。
- signal.sigtimedwait(sigset, timeout)¶
与
sigwaitinfo()
类似,但它接受一个额外的 timeout 参数来指定超时。 如果将 timeout 指定为0
,则执行轮询。 如果发生超时,则返回None
。可用性: Unix。
更多信息请参阅手册页 sigtimedwait(2)。
另请参阅
pause()
、sigwait()
和sigwaitinfo()
。在 3.3 版本中添加。
在 3.5 版本中更改: 如果被 sigset 中未包含的信号中断,并且信号处理程序没有引发异常,则该函数现在会使用重新计算的 timeout 值进行重试(有关基本原理,请参阅 PEP 475)。
示例¶
这是一个最小的示例程序。它使用 alarm()
函数来限制等待打开文件的时间; 如果该文件用于可能未打开的串行设备,则这很有用,通常会导致 os.open()
无限期地挂起。 解决方案是在打开文件之前设置一个 5 秒的警报;如果操作时间过长,将发送警报信号,并且处理程序将引发异常。
import signal, os
def handler(signum, frame):
signame = signal.Signals(signum).name
print(f'Signal handler called with signal {signame} ({signum})')
raise OSError("Couldn't open device!")
# Set the signal handler and a 5-second alarm
signal.signal(signal.SIGALRM, handler)
signal.alarm(5)
# This open() may hang indefinitely
fd = os.open('/dev/ttyS0', os.O_RDWR)
signal.alarm(0) # Disable the alarm
关于 SIGPIPE 的注意事项¶
当标准输出的接收者提前关闭时,将程序的输出通过管道传输到 head(1) 等工具会导致向你的进程发送 SIGPIPE
信号。 这会导致类似 BrokenPipeError: [Errno 32] Broken pipe
这样的异常。 要处理这种情况,请如下所示包装你的入口点以捕获此异常
import os
import sys
def main():
try:
# simulate large output (your code replaces this loop)
for x in range(10000):
print("y")
# flush output here to force SIGPIPE to be triggered
# while inside this try block.
sys.stdout.flush()
except BrokenPipeError:
# Python flushes standard streams on exit; redirect remaining output
# to devnull to avoid another BrokenPipeError at shutdown
devnull = os.open(os.devnull, os.O_WRONLY)
os.dup2(devnull, sys.stdout.fileno())
sys.exit(1) # Python exits with error code 1 on EPIPE
if __name__ == '__main__':
main()
不要将 SIGPIPE
的处置设置为 SIG_DFL
,以避免 BrokenPipeError
。 这样做会导致你的程序在程序仍在向其中写入内容时,每当任何套接字连接中断时意外退出。
关于信号处理程序和异常的注意事项¶
如果信号处理程序引发异常,则该异常将传播到主线程,并且可能在任何 字节码 指令之后引发。 最值得注意的是, KeyboardInterrupt
可能在执行期间的任何时候出现。 大多数 Python 代码(包括标准库)都无法对此进行可靠的防御,因此, KeyboardInterrupt
(或任何其他由信号处理程序导致的异常)在极少数情况下可能会使程序处于意外状态。
为了说明这个问题,请考虑以下代码
class SpamContext:
def __init__(self):
self.lock = threading.Lock()
def __enter__(self):
# If KeyboardInterrupt occurs here, everything is fine
self.lock.acquire()
# If KeyboardInterrupt occurs here, __exit__ will not be called
...
# KeyboardInterrupt could occur just before the function returns
def __exit__(self, exc_type, exc_val, exc_tb):
...
self.lock.release()
对于许多程序,特别是那些只想在 KeyboardInterrupt
时退出的程序,这不是问题,但复杂或需要高可靠性的应用程序应避免从信号处理程序引发异常。 它们还应避免捕获 KeyboardInterrupt
作为正常关闭的手段。 相反,它们应安装自己的 SIGINT
处理程序。 下面是一个 HTTP 服务器的示例,它避免了 KeyboardInterrupt
import signal
import socket
from selectors import DefaultSelector, EVENT_READ
from http.server import HTTPServer, SimpleHTTPRequestHandler
interrupt_read, interrupt_write = socket.socketpair()
def handler(signum, frame):
print('Signal handler called with signal', signum)
interrupt_write.send(b'\0')
signal.signal(signal.SIGINT, handler)
def serve_forever(httpd):
sel = DefaultSelector()
sel.register(interrupt_read, EVENT_READ)
sel.register(httpd, EVENT_READ)
while True:
for key, _ in sel.select():
if key.fileobj == interrupt_read:
interrupt_read.recv(1)
return
if key.fileobj == httpd:
httpd.handle_request()
print("Serving on port 8000")
httpd = HTTPServer(('', 8000), SimpleHTTPRequestHandler)
serve_forever(httpd)
print("Shutdown...")