Unix信号机制的简单介绍
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unix信号机制的简单介绍相关的知识,希望对你有一定的参考价值。
参考技术A 信号是一种软中断,它提供了异步响应和处理事件的机制。比如用户在终端上按下Ctrl-C终止程序运行,此时运行的程序就会收到对应的信号,然后终止程序运行。
运行程序,然后按Ctr-c键会得到如下结果:
一般情况下,按下Ctrl-C后程序直接退出,这个简单的例子里我们捕获了终止信号,进行
了自己的处理:打印出一段信息,然后main函数运行完毕。事实上,我们完全可以进行自由
地进行其他处理,比如不退出运行,虽然这不符合该信号的常规定义。
下面我们从这个简单的例子出发,详细分析
根据是否会被信号中断,系统调用分为两类:“慢”系统调用和其他。“慢”系统是一类可能
永远阻塞调用者的系统调用。
如果线程捕获了一个信号,但线程被阻塞在一个“慢”系统调用(例如从网络中读取一些数据)
,此时系统调用会被中断,返回错误且 errno 被设置为 EINTR 。
这样设计的原因是我们认为当一个信号被捕获到时,说明发生了重要的事件,应当唤醒线程
来处理事件。
这样的设计也带来了一些问题,因为某些读取调用属于可中断的系统调用,每次返回时,
程序都要判断是否是因为捕获信号导致的失败,如果是,需要进行重试。这样一来编码
非常繁琐,因此有些系统对一些“慢”系统调用支持自动重试。
信号处理函数的安全性问题产生的原因在于,信号可能随时被捕获,信号被捕获时,线程
可能处于任何状态,因此,在信号处理函数中,调用某些函数,可能导致不可预测的危险
行为。
比如,当信号被捕获时,线程正在调用 printf ,如果信号处理函数中也调用 printf 函数
可能会导致不可预测的风险。
风险的原因和 printf 函数的实现密切相关。因为 printf 函数包含了一个缓冲区,缓冲区
里包含了很多状态变量,当正在调用 printf 被中断时,缓冲区中的很多变量处于中间状态
如果在信号处理函数中在调用 printf 的话,就会导致不可预测的危险。
确保信号处理函数安全性的做法是在信号处理函数内部,只能调用可重入的函数(reentrant
function)。
用Python处理Unix信号
UNIX / Linux系统提供了在每个单独进程之间进行通信的特殊机制。这些机制之一是信号,属于进程之间的不同通信方法(进程间通信,缩写为IPC)。
简而言之,信号是软件中断,它被发送到程序(或进程),将重要事件或请求通知程序,以便运行特殊的代码序列。接收到信号的程序要么停止或继续执行其指令,要么在有或没有内存转储的情况下终止,甚至干脆忽略该信号。
虽然在POSIX标准中定义了它,但是实际的情况取决于开发人员如何编写脚本和实现信号处理。
在本文中,我们将解释什么是信号,向您展示如何从命令行向另一个进程发送信号,以及如何处理接收到的信号。在其他模块中,程序代码主要基于信号模块。这个模块将操作系统的according C头文件与Python连接起来
信号简介
在基于unix的系统中,有三类信号:
- 系统信号(硬件和系统错误):SIGILL, SIGTRAP, SIGBUS, SIGFPE, SIGKILL, SIGSEGV, SIGXCPU, SIGXFSZ, SIGIO
- 设备信号:SIGHUP、SIGINT、SIGPIPE、SIGALRM、SIGCHLD、SIGCONT、SIGSTOP、SIGTTIN、SIGTTOU、SIGURG、SIGWINCH、SIGIO
- 用户自定义信号:SIGQUIT、SIGABRT、SIGUSR1、SIGUSR2、SIGTERM
每个信号都用一个整数值表示,可用的信号列表相当长,而且不同UNIX/Linux发行版之间不一致。在Ubuntu18.04系统上,命令kill -l显示信号列表如下:
信号1到15基本上是标准化的,在大多数Linux系统中含义如下:
- 1 (SIGHUP): 终止连接,或重新加载守护进程的配置
- 2 (SIGINT): 当用户希望中断该过程时,SIGINT信号由其控制终端发送到进程。这通常通过按Ctrl+C启动,但在某些系统上,可以使用"delete" 或者 "break" 替代
- 3 (SIGQUIT): 当用户请求进程退出并执行核心转储时,SIGQUIT信号通过其控制终端发送给进程。
- 4 (SIGILL): 当进程试图执行非法、未知或特权指令时,SIGILL信号被发送到进程。
- 5 (SIGTRAP): 当异常发生时,SIGTRAP信号被发送到进程:调试器请求被告知的一个条件——例如,当一个特定的函数被执行时,或者当一个特定的变量改变值时。
- 6 (SIGABRT): 异常终止
- 7 (SIGBUS): 系统总线错误
- 8 (SIGFPE): 算术运算错误
- 9 (SIGKILL): 立即终止进程
- 10 (SIGUSR1): 用户定义的信号
- 11 (SIGSEGV): 由于非法访问内存段而导致的分割错误,它做了一个无效的虚拟内存引用,或分割故障
- 12 (SIGUSR2): 用户定义的信号
- 13 (SIGPIPE): 当进程试图写入没有连接到另一端的进程的管道时,SIGPIPE信号被发送到进程
- 14 (SIGALRM):计时器终止(alarm)
- 15 (SIGTERM): SIGTERM信号被发送到进程请求终止。与SIGKILL信号不同,它可以被进程捕获并解释或忽略。这允许进程执行良好的终止释放资源和保存状态(如果合适的话)。SIGINT与SIGTERM几乎相同。
为了向Linux终端中的进程发送信号,可以使用上面列表中的信号号(或信号名)和进程id (pid)调用kill命令。下面的示例命令向pid为12345的进程发送信号15 (SIGTERM):
$ kill -15 12345
一个等效的方法是使用信号名而不是它的编号:
$ kill -SIGTERM 12345
使用Python信号库
自Python 1.4以来,信号库是每个Python发行版的内置库。为了使用信号库,请将信号库导入Python程序,如下
import signal
捕获并对接收到的信号做出正确的反应是由一个回调函数完成的。 一个所谓的信号处理程序。一个相当简单的信号处理程序receiveSignal()可以编写如下:
def receiveSignal(signalNumber, frame): print(‘Received:‘, signalNumber) return
这个信号处理程序只报告接收到的信号的数量。下一步是注册信号处理程序捕获的信号。对于Python程序,所有的信号(除了9,SIGKILL)都可以在您的脚本中捕获:
if __name__ == ‘__main__‘: # register the signals to be caught signal.signal(signal.SIGHUP, receiveSignal) signal.signal(signal.SIGINT, receiveSignal) signal.signal(signal.SIGQUIT, receiveSignal) signal.signal(signal.SIGILL, receiveSignal) signal.signal(signal.SIGTRAP, receiveSignal) signal.signal(signal.SIGABRT, receiveSignal) signal.signal(signal.SIGBUS, receiveSignal) signal.signal(signal.SIGFPE, receiveSignal) #signal.signal(signal.SIGKILL, receiveSignal) signal.signal(signal.SIGUSR1, receiveSignal) signal.signal(signal.SIGSEGV, receiveSignal) signal.signal(signal.SIGUSR2, receiveSignal) signal.signal(signal.SIGPIPE, receiveSignal) signal.signal(signal.SIGALRM, receiveSignal) signal.signal(signal.SIGTERM, receiveSignal)
接下来,我们添加当前进程的进程信息,并使用os模块中的getpid()方法检测进程id。在无休止的while循环中,我们等待传入的信号。我们使用另外两个Python模块来实现它——os和time。我们在Python脚本的开头也导入了它们:
import os import time
在主程序的while循环中,print语句输出“Waiting…”。函数调用time.sleep()使程序等待三秒钟。
# output current process id print(‘My PID is:‘, os.getpid()) # wait in an endless loop for signals while True: print(‘Waiting...‘) time.sleep(3)
最后,我们必须测试脚本。将脚本保存为signal-handling.py后,我们可以在终端中调用它,如下所示:
$ python3 signal-handling.py My PID is: 5746 Waiting... ...
在第二个终端窗口中,我们向进程发送一个信号。我们通过上面屏幕上打印的进程id来标识第一个进程——Python脚本。
$ kill -1 5746
Python程序中的信号事件处理程序接收我们发送给进程的信号。它做出相应的反应,简单地确认接收到的信号:
... Received: 1 ...
忽略Signal
信号模块定义了忽略接收信号的方法。为此,信号必须与预定义的函数signal. sig_ign连接。下面的示例演示了这一点,因此Python程序不能再被CTRL+C中断。为了停止Python脚本,示例脚本中实现了另一种方法——信号SIGUSR1终止Python脚本。此外,我们使用signal.pause()方法而不是循环。它只是等待接收到一个信号。
import signal import os import time def receiveSignal(signalNumber, frame): print(‘Received:‘, signalNumber) raise SystemExit(‘Exiting‘) return if __name__ == ‘__main__‘: # register the signal to be caught signal.signal(signal.SIGUSR1, receiveSignal) # register the signal to be ignored signal.signal(signal.SIGINT, signal.SIG_IGN) # output current process id print(‘My PID is:‘, os.getpid()) signal.pause()
适当地处理信号
到目前为止,我们使用的信号处理程序相当简单,只报告接收到的信号。这向我们展示了Python脚本的接口工作得很好。让我们尝试改进它。
捕捉信号已经是一个很好的基础,但是需要一些改进才能符合POSIX标准的规则。为了获得更高的准确度,每个信号都需要适当的反应(见上面的列表)。这意味着Python脚本中的信号处理程序需要通过每个信号的特定例程进行扩展。如果我们理解了信号的作用,以及一个常见的反应是什么,这种方法就会发挥最佳效果。接收信号1、2、9或15的进程终止。在任何其他情况下,它也应该编写一个核心转储。
到目前为止,我们已经实现了一个覆盖所有信号的例子,并以相同的方式处理它们。下一步是为每个信号实现一个单独的例子。下面的示例代码演示了信号1 (SIGHUP)和信号15 (SIGTERM)。
def readConfiguration(signalNumber, frame): print (‘(SIGHUP) reading configuration‘) return def terminateProcess(signalNumber, frame): print (‘(SIGTERM) terminating the process‘) sys.exit()
上述两个函数与信号连接如下:
signal.signal(signal.SIGHUP, readConfiguration)
signal.signal(signal.SIGTERM, terminateProcess)
运行Python脚本,然后UNIX命令kill -1 42096和kill -15 42096发送信号1 (SIGHUP)和信号15 (SIGTERM),得到如下输出:
[email protected] /u/l/g/tests> python daemon.py
(‘My PID is:‘, 42096)
Waiting...
Waiting...
Waiting...
Waiting...
(SIGHUP) reading configuration
Waiting...
Waiting...
Waiting...
Waiting...
(SIGTERM) terminating the process
程序接收信号,并正确地处理它们。以下为完整代码:
import signal import os import time import sys def readConfiguration(signalNumber, frame): print (‘(SIGHUP) reading configuration‘) return def terminateProcess(signalNumber, frame): print (‘(SIGTERM) terminating the process‘) sys.exit() def receiveSignal(signalNumber, frame): print(‘Received:‘, signalNumber) return if __name__ == ‘__main__‘: # register the signals to be caught signal.signal(signal.SIGHUP, readConfiguration) signal.signal(signal.SIGINT, receiveSignal) signal.signal(signal.SIGQUIT, receiveSignal) signal.signal(signal.SIGILL, receiveSignal) signal.signal(signal.SIGTRAP, receiveSignal) signal.signal(signal.SIGABRT, receiveSignal) signal.signal(signal.SIGBUS, receiveSignal) signal.signal(signal.SIGFPE, receiveSignal) #signal.signal(signal.SIGKILL, receiveSignal) signal.signal(signal.SIGUSR1, receiveSignal) signal.signal(signal.SIGSEGV, receiveSignal) signal.signal(signal.SIGUSR2, receiveSignal) signal.signal(signal.SIGPIPE, receiveSignal) signal.signal(signal.SIGALRM, receiveSignal) signal.signal(signal.SIGTERM, terminateProcess) # output current process id print(‘My PID is:‘, os.getpid()) # wait in an endless loop for signals while True: print(‘Waiting...‘) time.sleep(3)
以上是关于Unix信号机制的简单介绍的主要内容,如果未能解决你的问题,请参考以下文章