#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. взято из комментариев
Комментариев нет:
Отправить комментарий