Есть две программы, общающиеся между собой по именованным каналам. Одна на 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('
Got sigterm!
')
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)
То функция вызовется.
Подскажите, как решить данную проблему!
Ответ
При получении сигнала, обработчик в Си выставляет флаг и сразу же завершается (здесь и далее я описываю С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() не прерывался сигналами).
Комментариев нет:
Отправить комментарий