Linux的信号以及Python处理

初次接触信号这个概念实在gpiozero库文档中的这么一个专题,How do I keep my script running?,我最最开始是在button.when_pressed = hello用一个while True的无限循环保持脚本运行,后来发现有个个内置库的signal.pause()函数可以保持脚本运行,直到按下Ctrl+C程序才会退出,我当时仅仅认识到这个地步,没往下深究,直到后来一次mqtt没加loop循环之后自动断开,我后来就在想我用pause还是loop_forever呢? 接下来我就慢慢揭开了信号的序幕,也算当补了下操作系统的一些知识。

1. 信号基础

Linux信号是一种较高层的软件形式的异常,它允许进程和内核中断其他进程。
一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件,Linux系统上支持30种不同类型的信号,每种信号类型都对应某种系统事件,红笔标注为常见信号:
Linux信号

可以用man 7 signal 查看signal信息

如果觉得上面专业的术语较为枯燥,可以查看Vamei大神(看了这篇文章才知道Vamei博主因为抑郁症去世,R.I.P.)的Linux信号基础以及之前几篇关于进程的文章,通熟易懂

2. 信号发送

2.1 用/bin/kill发送信号

我之前一直用kill -9 15213强制杀掉某个pid进程,对这个数字9一直没啥反应,其实就是信号9,也就是SIGKILL,等同于kill -SIGTSTP 15213

2.2 从键盘发送信号

键盘快捷键我已经在上面图中用红笔写出来,不展开

2.3 用函数发送信号

Python的os.kill()函数: os.kill(os.getpid(), signal.SIGUSR1)

2.4 用alarm函数向它自己发送信号

signal.alarm(n)函数会安排内核在n秒后发送一个SIGALRM信号给调用进程。如果n是0,那么不会调度安排新的alarm

3. 接收以及处理信号

Linux信号处理

当进程决定执行信号的时候,有下面几种可能:

  • 无视(ignore)信号,信号被清除,进程本身不采取任何特殊的操作
  • 默认(default)操作。每个信号对应有一定的默认操作。比如上面SIGCONT用于继续进程。
  • 自定义操作。也叫做获取 (catch) 信号。执行进程中预设的对应于该信号的操作。

进程会采取哪种操作,要根据该进程的程序设计。特别是获取信号的情况,程序往往会设置一些比较长而复杂的操作(通常将这些操作放到一个函数中)。

3.1 Python signal函数信号处理

signal包定义了各个信号名及其对应的整数,核心是使用signal.signal()函数来预设(register)信号处理函数singnal.signal(signalnum, handler)中signalnum为某个信号,handler为该信号的处理函数。我们在信号基础里提到,进程可以无视信号,可以采取默认操作,还可以自定义操作。当handler为signal.SIG_IGN时,信号被无视(ignore)。当handler为singal.SIG_DFL,进程采取默认操作(default)。当handler为一个函数名时,进程采取函数中定义的操作。

1
2
3
4
5
6
7
8
9
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')

信号处理程序和主程序以及其他信号处理程序并发地进行,如果处理程序和主程序并发的访问同样的全局数据结构,那么结果可能就是不可预知的,而且经常是致命的!

在主程序中,我们首先使用signal.signal()函数来预设信号处理函数。然后我们执行signal.pause()来让该进程暂停以等待信号,以等待信号。当信号SIGUSR1被传递给该进程时,进程从暂停中恢复,并根据预设,执行SIGTSTP的信号处理函数myHandler()myHandler的两个参数一个用来识别信号(signum),另一个用来获得信号发生时,进程栈的状况(stack frame)。这两个参数都是由signal.singnal()函数来传递的。

上面的程序可以保存在一个文件中(比如test.py)。我们使用如下方法运行:
$python3 test.py

以便让进程运行。当程序运行到signal.pause()的时候,进程暂停并等待信号。此时,通过按下CTRL+Z向该进程发送SIGTSTP信号。我们可以看到,进程执行了myHandle()函数, 随后返回主程序,继续执行。(当然,也可以用$ps查询process ID, 再使用$kill来发出信号。)

(进程并不一定要使用signal.pause()暂停以等待信号,它也可以在进行工作中接受信号,比如将上面的signal.pause()改为一个需要长时间工作的循环。)

我们可以根据自己的需要更改myHandler()中的操作,以针对不同的信号实现个性化的处理。

3.2 Python alarm函数自发自收信号

再来看个官网的例子,它使用alarm()函数来限制等待打开文件所花费的时间;这很有用,因为如果该文件作用于可能无法打开的串行设备,这通常会导致os.open()然后无限期挂起。解决的办法是在打开文件之前设置5秒警报。如果操作花费的时间太长,则会发送警报信号,并且处理程序将引发异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import signal, os

def handler(signum, frame):
print('Signal handler called with signal', 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

4. 说说在C语言中的实现

深入理解计算机系统书中,讲解Linux信号用了shell以及C来进行演示,这是必须的,理解这些底层机制用C实在是太契合了,但是回过头来看,Python实现这些功能时是完全照着C来的,无论是函数名参数名,只要底层原理相通,编程语言的函数实现也大体一致,如果想看更多关于C的演示,可以看Linux 信号机制,很棒的教程!

但是,Python3的文档中明确指出,handler并不是运行在底层的C handler中的,所以不要希望捕捉同步错误例如SIGFPE与SIGSEGV。
同时新增了几个功能,大概用于阻塞signal这种,不过我还不太会用:
signal.pthread_kill(thread_id, signum)
signal.pthread_sigmask(how, mask)