Python信号相关概念及运用

Python信号相关概念及运用

介绍

信号是软件中断。信号提供了一种处理异步事件的方法。

不存在编号为0的信号。kill函数对信号编号0有特殊的应用。POSIX.1将此信号编号值称为空信号。

很多条件可以产生信号:

  • 当用户按某些按键时,引发终端产生的信号。(DELETE,Ctrl+C,Ctrl+\,Ctrl+Z)。这些键可以自定义。
  • 硬件异常产生信号:除数为0、无效的内存引用等。
  • 进程调用kill(2)函数可将信号发送到另一个进程或进程组。有限制:只能发送给同一用户的进程,或者发送信号的进程所有者是超级用户。
  • 用户可用kill(1)命令将信号发送给其他进程。
  • 当检测到某种软件条件发生,并应将其通知有关进程时也产生信号。例句SIGURG、SIGPIPE、SIGALRM。

信号是异步事件的经典实例。产生信号的事件对进程而言是随机出现的。
可以要求内核在某个信号出现时按照下列方式之一进行处理:

  1. 忽略此信号。大多数都可以,但有两个不能忽略,他们是SIGKILL和SIGSTOP。原因是:它们向超级用户提供了使进程终止或停止的可靠方法。 另外,有些如果忽略由硬件异常引发的异常(如除0),则进程的行为是未定义的。
  2. 捕捉信号。自定义信号处理函数。注意:不能捕捉SIGKILL和SIGSTOP信号。
  3. 执行系统默认动作。针对大多数信号的系统默认动作是终止进程。

UNIX系统信号

名字 说明 默认动作 细节说明
SIGABRT 异常终止(abort) 终止+core 调用abort函数时产生此信号
SIGALRM 超时(alarm) 终止 调用alarm函数设置的计时器超时,由setitimer函数设置的间隔时间超时时,产生此信号
SIGBUS 硬件故障 终止+core 指示一个实现定义的 硬件故障。当出现某些类型的内存故障时,实现常常产生此信号
SIGCANCEL 线程库内部使用 忽略 Solaris线程库内部使用的信号
SIGCHLD 子进程状态改变 忽略 在一个进程终止或停止时,将SIGCHLD信号发送给其父进程。
SIGCONT 使暂停进程继续 继续/忽略 此作业控制信号被发送给需要继续运行,但当前处于停止状态的进程。
SIGEMT 硬件故障 终止+core 指示一个实现定义的 硬件故障。(emt = emulator trap)。
SIGFPE 算术异常 终止+core 此信号表示算术运算异常,例如除以0,浮点溢出等。
SIGFREEZE 检查点冻结 忽略 仅由Solaris定义。用于通知进程在冻结系统状态之前需要采取特定的动作。
SIGHUP 连接断开 终止 如果终端接口检测到一个连接断开,则将此信号发送给与该终端相关的控制进程(会话首进程)
SIGILL 非法硬件指令 终止+core 此信号指示进程已执行一条非法硬件指令。
SIGINFO 键盘状态请求 忽略 BSD信号
SIGINT 终端中断符 终止 当用户按中断键(一般采用DELETE或Ctrl+C)时,终端驱动程序产生此号并送至前台进程组中的每一个进程。
SIGIO 异步IO 终止/忽略 指示一个异步IO事件。
SIGIOT 硬件异常 终止+core 指示一个实现定义的 硬件故障 (iot = input/output trap)
SIGKILL 终止 终止 这是两个不能被捕捉或忽略的信号之一。它向系统管理员提供了一种可以杀死任一进程的可靠方法。
SIGLWP 线程库内部使用 忽略 Solaris线程库内部使用
SIGPIPE 写至无读进程的管道 终止 如果在写到管道时读进程已终止,则产生此信号。
SIGPOLL 可轮询事件 终止 当在一个 可轮询设备 上发生 一特定事件 时产生此信号。
SIGPROF 梗概时间超时(setitimer) 终止 当setitimer(2)函数设置的梗概统计间隔计时器已到期时产生此信号。
SIGPWR 电源失效/重启 终止/忽略 依赖于系统。主要用于具有不间断电源(UPS)的系统。如果电源失效,则UPS起作用,而且通常软件会接到通知。
SIGQUIT 终端退出符 终止+core 当用户在终端上按退出键(一般是Ctrl+)时,产生此信号,并送至前台进程组中的所有进程。
SIGSEGV 无效的内存引用 终止+core 此信号指示进程进行了一次无效的内存引用 (segv = segmentation violation)
SIGSTKFLT 协处理器栈故障 终止 此信号仅由Linux定义。
SIGSTOP 停止 暂停进程 作业控制信号,用于停止一个进程。类似于交互停止信号(SIGTSTP),但是SIGSTOP不能被捕捉或忽略。
SIGSYS 无效系统调用 终止+core 该信号指示一个无效的系统调用。
SIGTERM 终止 终止 这是由kill(1)命令发送的系统默认终止信号。
SIGTHAW 检查点解冻 忽略 此信号仅由Solaris定义。
SIGTRAP 硬件故障 终止+core 指示一个实现定义的 硬件故障 。当执行断点指令时,实现常用此信号将控制转移至调试程序
SIGTSTP 终端停止符 暂停进程 交互式停止指令。Ctrl+Z
SIGTTIN 后台读控制tty 暂停进程 当一个后台进程组中进程试图读其控制终端时,终端驱动程序产生此信号。有两种情况不产生。
SIGTTOU 后台写至控制tty 暂停进程 当一个后台进程组中的进程试图写到其控制终端时。一个进程可以允许后台进程写控制终端,不允许时也有两种情况不产生此信号
SIGURG 紧急情况(套接字) 忽略 此信号通知进程已经发生了一个紧急情况。在网络连接上接收到带外的数据时,可选择产生此信号。
SIGUSR1 用户定义的信号 终止 用户定义的信号,可用于应用程序。
SIGUSR2 用户定义的信号 终止 用户定义的信号,可用于应用程序。
SIGVTALRM 虚拟时间闹钟(setitimer) 终止 当一个由setitimer(2)函数设置的虚拟间隔时间到期时产生此信号。
SIGWAITING 线程库内部使用 忽略 此信号由Solaris线程库内部使用
SIGWINCH 终端窗口大小改变 忽略 内核维持与每个终端或伪终端相关联的窗口大小。进程可以用ioctl函数得到或设置窗口的大小。
SIGXCPU 超过CPU限制(setrlimit) 终止+core/忽略 如果进程超过了其软CPU时间限制,则产生SIGXCPU信号。
SIGXFSZ 超过文件长度限制(setrlimit) 终止+core/忽略 如果进程超过了其软文件长度限制,则产生此限制。
SIGXRES 超过资源限制 忽略 此信号仅由Solaris定义。

在下列条件下并不会产生core文件:

  • 进程是设置用户ID的,而且当前用户并非程序文件的所有者。
  • 进程是设置组ID的,而且当前用户并非该程序的组所有者。
  • 用户没有写当前工作目录的权限。
  • 该文件已存在,而且用户对该文件设有写权限。
  • 该文件太大。

signal包

signal包负责在Python程序内部处理信号,典型的操作包括预设信号处理函数,暂停并等待信号,以及定时发出SIGALRM等。要注意,signal包主要是针对UNIX平台(比如Linux, MAC OS),而Windows内核中由于对信号机制的支持不充分,所以在Windows上的Python不能发挥信号系统的功能。

捕捉信号

signal包的核心是使用signal.signal()函数来预设(register)信号处理函数,如下所示:

singnal.signal(signalnum, handler)

signalnum为某个信号,handler为该信号的处理函数。我们在信号基础里提到,进程可以无视信号,可以采取默认操作,还可以自定义操作。当handler为signal.SIG_IGN时,信号被无视(ignore)。当handler为singal.SIG_DFL,进程采取默认操作(default)。当handler为一个函数名时,进程采取函数中定义的操作。

示例代码s1.py:

#coding: utf-8
import signal
# Define signal handler function
def myHandler(signum, frame):
    print('I received: ', signum)

# register signal.SIGTSTP’s handler
signal.signal(signal.SIGTSTP, myHandler)
signal.pause()#等待信号产生,是任意信号。
print('End of Signal Demo')

执行过程1:

执行 python s1.py
键入Ctrl+Z

输出结果:

Z(‘I received: ‘, 18)
End of Signal Demo

执行过程2:

执行 python s1.py
键入Ctrl+C

输出结果:

CTraceback (most recent call last):
File “s1.py”, line 9, in
signal.pause()
KeyboardInterrupt

因为Ctrl+Z产生的SIGTSTP由自定义函数处理了。打印完返回原来代码执行处,继续打印’End of Signal Demo‘后程序退出。
而Ctrl+C产生的SIGINT信号没有设置捕捉函数,按系统默认操作执行,终止程序运行。

发送信号

signal.alarm(),它被用于在一定时间之后,向进程自身发送SIGALRM信号:

import signal
import time
# Define signal handler function
def myHandler(signum, frame):
    print("Now, it’s the time ", time.ctime())
    exit()

# register signal.SIGALRM’s handler
signal.signal(signal.SIGALRM, myHandler)
signal.alarm(1)
while True:
    print('not yet')

输出结果:

not yet
not yet

not yet
Now, it’s the time Thu Feb 9 11:49:05 2017

os包的kill发送信号

signal只有alarm可以发送SIGALRM信号。其它信号需要借助os包。

os.kill(pid, sid) #向进程pid发送信号sid

os.killpg(pgid, sid)#向进程组pgid发送信号sid

其他注意事项

进程捕捉到信号并对其进行处理时,进程正在执行的指令会被临时中断,转而去处理该信号处理程序中的指令,返回继续处理之前被中断的指令。
但是之前被中断的指令可能是malloc,getpwnam等,而信号处理函数也可能调用malloc,getpwnam,结果导致继续执行之前的malloc,getpwnam出错。malloc\getpwnam就是不可重入函数。
相对应的就是可重入函数。

Single UNIX Specification说明了保证可重入函数。一共有117个。
不可重入的原因有:

  • 已知它们使用静态数据结构
  • 它们调用malloc或free
  • 它们是标准I/O函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

应当了解,即使使用可重入函数,但是由于每个线程只有一个errno变量,所以信号处理程序可能会修改其原先值。
因此,作为一个通用的规则,当在信号处理程序中调用可重入函数时,应当在其前保存,在其后恢复errno。

若在信号处理程序中调用一个不可重入函数,则其结果是不可预见的。

#coding: utf-8
import signal
# Define signal handler function
i = 0
def myHandler(signum, frame):
    global i;
    i += 1;
    print('I received: ', signum)

# register signal.SIGTSTP’s handler
signal.signal(signal.SIGTSTP, myHandler)
signal.pause()
i += 1;
print('End of Signal Demo, i=',i)

输出结果:

python s1.py
Z(‘I received: ‘, 18)
(‘End of Signal Demo, i=’, 2)

这个例子比较简单。但是在更复杂的环境下,i被信号捕捉函数修改过并不是显而易见的。

Tags: