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 42096kill -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信号机制的简单介绍的主要内容,如果未能解决你的问题,请参考以下文章

linux中的signal机制(转)

Unix环境_信号处理机制

UNIX信号

linux信号机制分析

Linux信号(signal) 机制分析

Linux信号(signal) 机制分析