Python进程如何处理操作系统信号?

程序优雅的退出

当我们的Python进程被kill的时候,怎样完成一些进程内的清理工作,如消费完队列缓存、flush文件到磁盘等,然后进行优雅的退出?

通常情况下通过监听SIGTERM信号可以做到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import os
import time
import signal

flag_exit = False

def my_signal_handler(signum, _):
"""信号处理函数"""
print('Received signal: {}'.format(signum))
print('Do some clean work...')
global flag_exit
flag_exit = True

def main():
signal.signal(signal.SIGTERM, my_signal_handler)
print('pid: {}'.format(os.getpid()))
while not flag_exit:
print('main thread running...')
time.sleep(1)
print('main thread exit.')


if __name__ == '__main__':
main()

运行输出如下:

1
2
3
4
5
6
7
8
9
10
pid: 85470
main thread running...
main thread running...
main thread running...

# 执行 kill 85470 命令

Received signal: 15
Do some clean work...
main thread exit.

输出符合预期。

程序并发

问题: my_signal_handler 函数执行完是怎样回到main 函数继续执行的?

梳理一下执行顺序:

  1. 执行main
  2. 收到SIGTERM信号,跳转执行my_signal_handler
  3. my_signal_handler执行完成
  4. 跳转执行main

主函数控制流与信号处理函数的控制流产生了重叠,两个函数交替执行,这显然是一个程序并发问题。

操作系统中的信号

并发问题是由信号带来的,再来看一下信号的概念。

在计算机科学中,信号是Unix、类Unix以及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。

信号相关的系统调用: sigaction(2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 函数的原型
int sigaction(int signum,
const struct sigaction *act,
struct sigaction *oldact);

// signum: 信号值
// act: 如果act非NULL, 将被设置为信号处理函数
// oldact: 如果oldact非NULL, 将被用来保存前一版的信号处理函数

// 结构体
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};

// sa_handler:信号处理函数的函数指针,其函数原型和signal(2)接受的信号处理函数相同。
// sa_sigaction:另一种信号处理函数指针,它能在处理信号时获取更多信号相关的信息。
// sa_mask:允许设置信号处理函数执行时需要阻塞的信号。
// sa_flags:修改信号处理函数执行时的默认行为,具体可选值请参照手册。

也就是说,我们的进程如果要处理信号,可以使用系统调用sigaction来设置我们定义的信号处理函数。

接下来,我们看一下在C语言中怎样使用。

处理信号 - C语言

CPython使用C语言实现,在我们进入CPython代码一探究竟之前,先来看一下在C语言中如何处理信号。

以下实现的功能同上面的Python版本。

PS: 这部分代码从这里 copy修改得来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int flag_exit = 0;

/* 信号处理函数 */
void my_signal_handler(int signum)
{
printf("Received signal: %d\n", signum);
printf("Do some clean work...\n");
flag_exit = 1;
}


int main() {
struct sigaction new_action, old_action;

/* Set up the structure to specify the new action. */
new_action.sa_handler = my_signal_handler;
sigemptyset (&new_action.sa_mask);
new_action.sa_flags = 0;

sigaction (SIGTERM, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN)
sigaction (SIGTERM, &new_action, NULL);

printf("pid: %d\n", getpid());
while(!flag_exit) {
printf("main thread running...\n");
sleep(1);
}

printf("main thread exit.\n");
return 0;
}

下面是输出:

1
2
3
4
5
6
7
8
9
10
pid: 91477
main thread running...
main thread running...
main thread running...

# 执行 kill 91477 命令

Received signal: 15
Do some clean work...
main thread exit.

输出符合预期。

回到之前的问题:my_signal_handler 函数执行完是怎样回到main 函数继续执行的?

答案并不新鲜:程序在内核态收到信号,保存用户态堆栈,并为信号处理函数创建用户态堆栈,切换至用户态执行信号处理函数,执行完切换回内核态,恢复之前保存的用户态堆栈。

Python版本与C版本有什么不同?

处理信号C与Python版本功能和输出相同,唯一不同的点在设置信号处理函数的地方。

Python使用了signal.signal 设置信号处理函数。

C使用了sigaction设置信号处理函数。

所以差异点在signal.signal是怎样使用sigaction系统调用的。

我们看一下Python文档中写的

A Python signal handler does not get executed inside the low-level (C) signal handler. Instead, the low-level signal handler sets a flag which tells the virtual machine to execute the corresponding Python signal handler at a later point(for example at the next bytecode instruction).

Python对信号的处理做了一个延后处理,具体的原因可以查看signal模块文档。

让我们从CPython实现上去一探究竟~

CPython中的信号处理

从signal.signal开始

现在看一下Python中的signal.signal的实现函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# CPython v3.7.3 | signalmodule.c:401

static PyObject *
signal_signal_impl(PyObject *module, int signalnum, PyObject *handler)
{
// ...
func = signal_handler;
if (PyOS_setsig(signalnum, func) == SIG_ERR) {
PyErr_SetFromErrno(PyExc_OSError);
return NULL;
}

// ...
old_handler = Handlers[signalnum].func;
Py_INCREF(handler);
Handlers[signalnum].func = handler;
// ...
}

可以看到:

  1. 对应信号signalnum设置的异常处理函数为signal_handler(通过PyOS_setsig)

  2. 而我们自定义的信号处理函数放在了Handlers这个数组中

设置信号处理函数PyOS_setsig

跟我们写的会有什么不同吗?来看一下.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# CPython v3.7.3 | pylifecycle.c:2387

PyOS_sighandler_t
PyOS_setsig(int sig, PyOS_sighandler_t handler)
{
#ifdef HAVE_SIGACTION
struct sigaction context, ocontext;
context.sa_handler = handler;
sigemptyset(&context.sa_mask);
context.sa_flags = 0;
if (sigaction(sig, &context, &ocontext) == -1)
return SIG_ERR;
return ocontext.sa_handler;
//...
}

没有什么差别,也就是说程序的控制流是完全同我们写的C语言版本一致。

信号处理函数在哪里执行?

当然是访问Handlers的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# CPython v3.7.3 | signalmodule.c:1510

int
PyErr_CheckSignals(void)
{
int i;
// ..
for (i = 1; i < NSIG; i++) {
if (_Py_atomic_load_relaxed(&Handlers[i].tripped)) {
PyObject *result = NULL;
PyObject *arglist = Py_BuildValue("(iO)", i, f);
// ..
if (arglist) {
result = PyEval_CallObject(Handlers[i].func,
arglist);
}
}
}
return 0;
}

现在我们就来看一下signal_handler是如何使得PyErr_CheckSignals被执行的。

收到信号

当信号发生时,

  1. signal_handler通过Py_AddPendingCall把一个pendingcall加入了pendingcall队列

  2. 在执行字节码循环中(cevil.c) 如果pandingcall队列非空,则执行Py_MakePendingCalls

  3. Py_MakePendingCalls调用了

参考:
[1] The GNU C Library - 24 Signal Handling

[2] Unix环境高级编程 人民邮电出版社

[3] https://www.infoq.cn/article/linux-signal-system


© 2019 丘名鹤