Страницы

Поиск по вопросам

вторник, 10 декабря 2019 г.

Перехват сигналов в Python

#cpp #python #процесс #обработка_сигналов #python_internals


Есть две программы, общающиеся между собой по именованным каналам. Одна на C++, вторая
на Python. Причём первая запускает вторую (стандартным способом, через fork + exec).

Участок коммуникации родительской программы (C++):

// ...
int pipeDescr;
std::string outputPipeName{"inputPipe"};
std::string inputPipeName{"outputPipe"};
char *message = new char[BUFSIZE];
// ...
while (true)
{
    int bytesNumber = 0;
    // ...
    if ((pipeDescr = open(outputPipeName.c_str(), O_WRONLY)) <= 0)
        break;

    bytesNumber = write(pipeDescr, message, strlen(message));
    if (bytesNumber <= 0)
        break;

    close(pipeDescr);

    message[0] = '\0';
    if ((pipeDescr = open(inputPipeName.c_str(), O_RDONLY)) <= 0)
        break;

    bytesNumber = read(pipeDescr, message, BUFSIZE);
    if (bytesNumber <= 0)
        break;

    close(pipeDescr);
    // ...
}


В отдельном потоке работает функция watchDog, которая несёт ответственность за работу
дочернего приложения на Python.

void watchDog(int clientSocket, pid_t pid, bool &stopWatchDog)
{
    while (true)
    {
        // Дочерняя программа завершилась с ошибкой
        if (waitpid(pid, NULL, WNOHANG) != 0)
        {
            // ...
        }

        // Родительская программа закрывается
        mutexClosing.lock();
        if (closing)
        {
            mutexClosing.unlock();
            if (waitpid(pid, NULL, WNOHANG) == 0)
            {
                kill(pid, SIGTERM);
                waitpid(pid, NULL, 0);
            }
            break;
        }
        mutexClosing.unlock();

        // Программное отключение WatchDog-а
        mutexWatchDog.lock();
        if (stopWatchDog)
        {
            if (waitpid(pid, NULL, WNOHANG) == 0)
            {
                kill(pid, SIGTERM);
                waitpid(pid, NULL, 0);
            }
            mutexWatchDog.unlock();
            break;
        }
        mutexWatchDog.unlock();
    }
}


При этом есть три исхода:


падение дочерней программы
завершение родительской программы
завершение дочерней программы без завершения родительской


В последних двух вариантах необходимо плавно завершить дочернюю программу, поэтому,
я отправляю ей сигнал SIGTERM и ожидаю завершения.

Участок коммуникации дочерней программы (Python):

def sigterm_handler(signal, frame):
    print('\nGot sigterm!\n')
    sys.exit(0)

def main():
    input_pipe_name = "inputPipe"
    output_pipe_name = "outputPipe"
    # ...
    signal.signal(signal.SIGTERM, sigterm_handler)
    # ...
    while True:
        pipe_descr = os.open(input_pipe_name, os.O_RDONLY)
        request = os.read(pipe_descr, 10000)
        os.close(pipe_descr)

        reply = work_func(request)

        pipe_descr = os.open(output_pipe_name, os.O_WRONLY)
        os.write(pipe_descr, bytes(reply, 'UTF-8'))
        os.close(pipe_descr)


В код я добавил обработку сигнала SIGTERM. Однако, при передаче этого сигнала, функция
sigterm_handler не вызывается.

Но! Если написать что-то типа этого:

def main():
    signal.signal(signal.SIGTERM, sigterm_handler)

    while True:
        print('waiting...')
        time.sleep(2)


То функция вызовется.

Подскажите, как решить данную проблему!
    


Ответы

Ответ 1



При получении сигнала, обработчик в Си выставляет флаг и сразу же завершается (здесь и далее я описываю С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) Когда контроль возвращается зависит от того, где выполнение происходит в данный момент (разное поведение возможно для разных функций на разных версиях Питона на разных платформах). Например, os.open() на POSIX системе на Python 3.5, сводится к: do { Py_BEGIN_ALLOW_THREADS fd = open(path->narrow, flags, mode); Py_END_ALLOW_THREADS } while (fd < 0 && errno == EINTR && !(async_err = PyErr_CheckSignals())); Py_BEGIN_ALLOW_THREADS макрос отпускает GIL, что позволяет другим потокам выполнять Питон-код, пока текущий поток блокирован на open(2) системном вызове. При установлении своего обработчика signal.signal() вызов сбрасывает SA_RESTART флаг поэтому системные вызовы такие как open(2) прерываются сигналом и возвращают EINTR, что в данном случае вызывает PyErr_CheckSignals() функцию, которая ничего не делает, если вызов не из главного потока в программе. В главном потоке PyErr_CheckSignals() проверяет был ли сигнал (по флагу, установленному Си обработчиком) и вызывает обработчик, написанный на Питоне. Если обработчик выбросит исключение, то PyErr_CheckSignals() возвращает не ноль и цикл прерывается, что ведёт к возникновению исключения на месте вызова os.open в Питон коде, когда os.open был вызван из главного потока (иначе PyErr_CheckSignals() возвращает 0). В других случаях возможно много вариантов: отпущен/не отпущен GIL (в Си коде), прерывается/автоматически перезапускается ли сам блокирующий вызов (от платформы, Си библиотеки, версии Питона может зависеть), вызывается ли PyErr_CheckSignals() в главном потоке, не сбрасывается ли где-то флаг, что сигнал произошёл до вызова обработчика (signal(SIGINT, custom_handler) не работает на Windows на Python 2.7). Если вы не можете изменить ваш блокирующий Си код, чтобы он PyErr_CheckSignals() вызывал, как это делает os.open при получении сигнала, то чтобы обойти это: вызывайте блокирующий код в фоновом потоке, а в главном потоке, спите с небольшим интервалом (это не спасёт, если ваше Си расширение для CPython не отпускает GIL, как это к примеру re модуль может делать): import threading background_thread = threading.Thread(target=fifo_loop) background_thread.daemon = True background_thread.start() while background_thread.is_alive(): background_thread.join(1) # здесь никакого другого кода, это весь цикл Обратите внимание, что это отличается от предложения добавить time.sleep(1) в ваш цикл, который с FIFO(7) работает. Если сигнал произойдёт вне вызова time.sleep(1) в вашем fifo цикле, то проблема так и останется. Обходное решение работает, потому что цикл с fifo исполняется в фоновом потоке, а главный поток только спит с перерывами. В Питоне 3, можно background_thread.join() использовать без timeout (в Питоне 2, этот .join() не прерывался сигналами).

Ответ 2



после while True: необходимо добавить time.sleep(1), так как нужно немного ждать, чтобы выдать сигнал для процесса. p.s. взято из комментариев

Комментариев нет:

Отправить комментарий