Python内核阅读(二十五):信号处理机制

Python,C/C++ 2018-09-26

起步

Python处理信号是在 signal 模块中,这个模块其实是纯python代码对 _signal 的封装。要想知道Python解释器本身如何处理信号以及如何实现的,还需要去了解 signalmodule.c 。其中,比较需要了解的是python解释器与操作系统有关信号的交互。

大体上,Python解释器不太可能会操作系统发出的信号立即做回调。因为Python的Opcode操作是原子操作,不允许被中断。所以Python解释器对信号做一层封装,并做好标记,待时机得当的时候来检查并触发相关的回调函数。

信号机制的初始化

信号机制的初始化是在Python初始化整个解释器时开始的,Python在初始化函数中调用 initsigs() 来进行整个系统以及 singal 模块的初始化。

[Python/pylifecycle.c]
_PyInitError
_Py_InitializeMainInterpreter(PyInterpreterState *interp,
                              const _PyMainInterpreterConfig *config)
{
  ...
    if (interp->config.install_signal_handlers) {
        err = initsigs(); /* Signal handling stuff, including initintr() */
        if (_Py_INIT_FAILED(err)) {
            return err;
        }
    }
  ...
}

而在 initsigs(void) 函数中,则是直接对系统调用的封装:

[Python/pylifecycle.c]
static _PyInitError
initsigs(void)
{
#ifdef SIGPIPE
    PyOS_setsig(SIGPIPE, SIG_IGN);        // 忽略SIGPIPE 
#endif
#ifdef SIGXFZ
    PyOS_setsig(SIGXFZ, SIG_IGN);         // 忽略SIGXFZ 
#endif
#ifdef SIGXFSZ
    PyOS_setsig(SIGXFSZ, SIG_IGN);        // 忽略SIGXFSZ  file size exceeded
#endif
    PyOS_InitInterrupts(); /* May imply initsignal() */
    if (PyErr_Occurred()) {
        return _Py_INIT_ERR("can't import signal");
    }
    return _Py_INIT_OK();
}

暂时不知道忽略了那几个信号的原因。而 PyOS_InitInterrupts(void) 函数中其实就是 import _signal :

[Modules/signalmodule.c]
void
PyOS_InitInterrupts(void)
{
    PyObject *m = PyImport_ImportModule("_signal");
    if (m) {
        Py_DECREF(m);
    }
}

_signal 模块的初始化中:

[Modules/signalmodule.c]
PyMODINIT_FUNC
PyInit__signal(void)
{
    PyObject *m, *d, *x;
    int i;

    main_thread = PyThread_get_thread_ident();
    main_pid = getpid();

    // 创建signal模块
    m = PyModule_Create(&signalmodule);
    if (m == NULL)
        return NULL;
    ...

    /* Add some symbolic constants to the module */
    d = PyModule_GetDict(m);
    // 将SIG_DFL、SIGIGN 转化成Python整数对象
    x = DefaultHandler = PyLong_FromVoidPtr((void *)SIG_DFL);
    if (!x || PyDict_SetItemString(d, "SIG_DFL", x) < 0)
        goto finally;

    x = IgnoreHandler = PyLong_FromVoidPtr((void *)SIG_IGN);
    if (!x || PyDict_SetItemString(d, "SIG_IGN", x) < 0)
        goto finally;

    x = PyLong_FromLong((long)NSIG);
    if (!x || PyDict_SetItemString(d, "NSIG", x) < 0)
        goto finally;
    Py_DECREF(x);
    ...
    /*
    * 获取signal模块中的默认中断处理函数,
    * 实际就是 signal_default_int_handler    
    */
    x = IntHandler = PyDict_GetItemString(d, "default_int_handler");
    if (!x)
        goto finally;
    Py_INCREF(IntHandler);

    /*
    * 初始化Python解释器中的Handler,
    * 这个数组存储每个用户自定义的信号处理函数
    * 以及标志是否发生该信号的标志。
    */
    _Py_atomic_store_relaxed(&Handlers[0].tripped, 0);
    for (i = 1; i < NSIG; i++) {
        void (*t)(int);
        t = PyOS_getsig(i);
        _Py_atomic_store_relaxed(&Handlers[i].tripped, 0);
        if (t == SIG_DFL)
            Handlers[i].func = DefaultHandler;
        else if (t == SIG_IGN)
            Handlers[i].func = IgnoreHandler;
        else
            Handlers[i].func = Py_None; /* None of our business */
        Py_INCREF(Handlers[i].func);
    }

    //为 SIGINT 设置默认的信号处理函数signal_handler
    if (Handlers[SIGINT].func == DefaultHandler) {
        /* Install default int handler */
        Py_INCREF(IntHandler);
        Py_SETREF(Handlers[SIGINT].func, IntHandler);
        PyOS_setsig(SIGINT, signal_handler);
    }

// 实现signal模块中的各个 SIGXXX 信号值和名称
#ifdef SIGHUP
    if (PyModule_AddIntMacro(m, SIGHUP))
         goto finally;
#endif
       ....

    if (PyErr_Occurred()) {
        Py_DECREF(m);
        m = NULL;
    }

  finally:
    return m;
}

可以看到,用户自定义的处理函数将会保存在 Handler 数组中,而实际上向操作系统注册 signal_signal_impl 函数。这个函数将作为Python解释器和用户自定义处理函数的桥梁:

[Modules/signalmodule.c]
static PyObject *
signal_signal_impl(PyObject *module, int signalnum, PyObject *handler)
/*[clinic end generated code: output=b44cfda43780f3a1 input=deee84af5fa0432c]*/
{
    PyObject *old_handler;
    void (*func)(int);
#ifdef MS_WINDOWS
    /* Validate that signalnum is one of the allowable signals */
    switch (signalnum) {
        case SIGABRT: break;
        case SIGTERM: break;
        ...
        default:
            PyErr_SetString(PyExc_ValueError, "invalid signal value");
            return NULL;
    }
#endif
    // 只有主线程才能设置信号处理函数
    if (PyThread_get_thread_ident() != main_thread) {
        PyErr_SetString(PyExc_ValueError,
                        "signal only works in main thread");
        return NULL;
    }
    if (signalnum < 1 || signalnum >= NSIG) {
        PyErr_SetString(PyExc_ValueError,
                        "signal number out of range");
        return NULL;
    }
    if (handler == IgnoreHandler)
        func = SIG_IGN;
    else if (handler == DefaultHandler)
        func = SIG_DFL;
    else if (!PyCallable_Check(handler)) {
        PyErr_SetString(PyExc_TypeError,
"signal handler must be signal.SIG_IGN, signal.SIG_DFL, or a callable object");
                return NULL;
    }
    else
        func = signal_handler;  // Python解释器向系统注册的都是signal_handler函数
    /* Check for pending signals before changing signal handler */
    if (PyErr_CheckSignals()) {
        return NULL;
    }
    if (PyOS_setsig(signalnum, func) == SIG_ERR) {
        PyErr_SetFromErrno(PyExc_OSError);
        return NULL;
    }
    // 把实际的用户自定义信号处理函数,放入对应的Handler数组中进行替换
    old_handler = Handlers[signalnum].func;
    Py_INCREF(handler);
    Handlers[signalnum].func = handler;
    if (old_handler != NULL)
        return old_handler;
    else
        Py_RETURN_NONE;
}

函数 signal_handler 是直接由C信号机制进行的回调。如果用户注册了信号处理函数,那么会取代旧的处理函数。

信号产生时

当C层发出信号并进行回调 signal_handler(int sig_num) :

[Modules/signalmodule.c]
static void
signal_handler(int sig_num)
{
    int save_errno = errno;

    /* See NOTES section above */
    if (getpid() == main_pid)
    {
        trip_signal(sig_num);
    }

#ifndef HAVE_SIGACTION
#ifdef SIGCHLD
    /* To avoid infinite recursion, this signal remains
       reset until explicit re-instated.
       Don't clear the 'func' field as it is our pointer
       to the Python handler... */
    if (sig_num != SIGCHLD)
#endif
    /* If the handler was not set up with sigaction, reinstall it.  See
     * Python/pylifecycle.c for the implementation of PyOS_setsig which
     * makes this true.  See also issue8354. */
    PyOS_setsig(sig_num, signal_handler);
#endif

    /* Issue #10311: asynchronously executing signal handlers should not
       mutate errno under the feet of unsuspecting C code. */
    errno = save_errno;

#ifdef MS_WINDOWS
    if (sig_num == SIGINT)
        SetEvent(sigint_event);
#endif
}

static void
trip_signal(int sig_num)
{
    unsigned char byte;
    int fd;
    Py_ssize_t rc;

    // 标记位设为1 ,表示信号产生了
    _Py_atomic_store_relaxed(&Handlers[sig_num].tripped, 1);

    /* Set is_tripped after setting .tripped, as it gets
       cleared in PyErr_CheckSignals() before .tripped. */
    // 如果正在处理信号,则不再向Python虚拟机提交 
    _Py_atomic_store(&is_tripped, 1);

    /* Notify ceval.c */
    _PyEval_SignalReceived();

#ifdef MS_WINDOWS
    fd = Py_SAFE_DOWNCAST(wakeup.fd, SOCKET_T, int);
#else
    fd = wakeup.fd;
#endif

    if (fd != INVALID_FD) {
        byte = (unsigned char)sig_num;
#ifdef MS_WINDOWS
        ...
#endif
        {
            /* _Py_write_noraise() retries write() if write() is interrupted by
               a signal (fails with EINTR). */
            rc = _Py_write_noraise(fd, &byte, 1);
            if (rc < 0) {
                if (wakeup.warn_on_full_buffer ||
                    (errno != EWOULDBLOCK && errno != EAGAIN))
                {
                    // 向Python虚拟机提交pending_call,纳入到整个虚拟机的执行过程中
                    Py_AddPendingCall(report_wakeup_write_error,
                                      (void *)(intptr_t)errno);
                }
            }
        }
    }
}

当C语言触发回调后,该回调函数会进行设置标记位并将 report_wakeup_write_error 加入到虚拟机的执行过程中,通过跟踪会调用 PyErr_CheckSignals() 进行信号的检查:

[Modules/signalmodule.c]
int
PyErr_CheckSignals(void)
{
    int i;
    PyObject *f;

    if (!_Py_atomic_load(&is_tripped))
        return 0;

    if (PyThread_get_thread_ident() != main_thread)
        return 0;

    // 在处理信号了,将标志位设为0
    _Py_atomic_store(&is_tripped, 0);

    if (!(f = (PyObject *)PyEval_GetFrame()))
        f = Py_None;

    // 按照信号值从小到大依次调用对应的信号处理函数
    for (i = 1; i < NSIG; i++) {
        if (_Py_atomic_load_relaxed(&Handlers[i].tripped)) {
            PyObject *result = NULL;
            PyObject *arglist = Py_BuildValue("(iO)", i, f);
            _Py_atomic_store_relaxed(&Handlers[i].tripped, 0);

            if (arglist) {
                // 调用回调函数
                result = PyEval_CallObject(Handlers[i].func,
                                           arglist);
                Py_DECREF(arglist);
            }
            if (!result) {
                _Py_atomic_store(&is_tripped, 1);
                return -1;
            }

            Py_DECREF(result);
        }
    }

    return 0;
}

这里面的 PyErr_CheckSignals 函数允许被其他模块调用直接信号的处理。

总结

可以看到整个信号处理的流程:

  • 初始化signal模块,将对应的操作系统信号值、函数转化成Python对象
  • 用户设置信号就向操作系统注册函数signal_handler,并将用户自定义信号处理函数设置到对应的Handler数组中
  • 当信号发生时,操作系统调用signal_handler设置tripped=1,然后调用trip_signal将统一处理函数checksignals_witharg作为pendingcall注册到Python虚拟机的执行栈中。
  • Python虚拟机在处理pendingcall时调用checksignals_withargs,从而信号处理函数得以执行。
  • 另外,Python其他模块可以直接调用PyErr_CheckSignals进行信号处理。

对于书写python代码的开发这而言:

  • 只有主线程能够设置、捕获和处理信号
  • 信号设置一直有效(signal_handler中会再次注册信号处理函数)
  • 多次信号,可能会被合并处理一次
  • 按照信号值从小到大处理

本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

如果对您有用,您的支持将鼓励我继续创作!