2016-11-08 1 views
6

Это является продолжением How to prevent SIGINT in child process from propagating to and killing parent process?Вызов убийство на дочерний процесс с SIGTERM завершается родительский процесс, но называть его SIGKILL держит родителей в живых

В приведенном выше вопрос, я узнал, что SIGINT не барботировании вверх от от дочернего к родительскому, но, скорее, выдается всей группе процессов переднего плана, то есть мне нужно написать обработчик сигнала, чтобы предотвратить выход родителя, когда я ударил CTRL + C.

Я попытался реализовать это, но вот проблема. Что касается конкретно сценария kill, я вызываю, чтобы закончить его, если я перехожу в SIGKILL, все работает так, как ожидалось, но если я перейду в SIGTERM, он также завершит родительский процесс, показывая Terminated: 15 в командной строке позже.

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

Код ниже урезанный пример того, что я придумал

#include <stdio.h> 
#include <signal.h> 
#include <stdlib.h> 
#include <unistd.h> 

pid_t CHILD = 0; 
void handle_sigint(int s) { 
    (void)s; 
    if (CHILD != 0) { 
    kill(CHILD, SIGTERM); // <-- SIGKILL works, but SIGTERM kills parent 
    CHILD = 0; 
    } 
} 

int main() { 
    // Set up signal handling 
    char str[2]; 
    struct sigaction sa = { 
    .sa_flags = SA_RESTART, 
    .sa_handler = handle_sigint 
    }; 
    sigaction(SIGINT, &sa, NULL); 

    for (;;) { 
    printf("1) Open SQLite\n" 
      "2) Quit\n" 
      "-> " 
     ); 
    scanf("%1s", str); 
    if (str[0] == '1') { 
     CHILD = fork(); 
     if (CHILD == 0) { 
     execlp("sqlite3", "sqlite3", NULL); 
     printf("exec failed\n"); 
     } else { 
     wait(NULL); 
     printf("Hi\n"); 
     } 
    } else if (str[0] == '2') { 
     break; 
    } else { 
     printf("Invalid!\n"); 
    } 
    } 
} 

Моего образованным предположение относительно того, почему это происходит, был бы что-то перехватывает SIGTERM, и убивает всю группу процессов. Принимая во внимание, что, когда я использую SIGKILL, он не может перехватить сигнал, поэтому мой вызов на убийство работает так, как ожидалось. Это всего лишь удар в темноте.

Может кто-нибудь объяснить, почему это происходит?

Как я отмечаю, я не в восторге от своей функции handle_sigint. Существует ли более стандартный способ убийства интерактивного дочернего процесса?

+2

Хорошо представленный вопрос, что я слишком сонлив, чтобы поиграть. Посмотрите также, как маски сигналов передаются от родителя к дочернему; правила для этого настолько сложны, что мне приходилось каждый раз искать их. Ваша гипотеза «не может перехватывать» является хорошей, но, вероятно, неправильной, поскольку ядро ​​перетасовывает сигналы в зависимости от того, что было запрошено; процессы не участвуют. – msw

+1

Я был бы очень удивлен, если бы sqlite не запустил собственную группу процессов, и я гарантирую, что он изменит свою собственную сигнальную маску. – msw

+2

@msw: Я не потрудился проверить источники, но, используя мою примерную программу ниже, могу сказать, что 'sqlite3' не блокирует' HUP', 'TERM',' QUIT', 'USR1' или' Сигналы USR2'; и похоже, что «INT» обрабатывается или игнорируется (поскольку он отображается в терминале как «^ C», если отправляется через 'kill' в другом месте). Обратите внимание, что вы всегда можете использовать, например. 'kill -HUP $ (ps -C sqlite3 -o pid =)' для отправки сигнала HUP для всех запущенных процессов 'sqlite3'; это то, что я использовал для тестирования. –

ответ

9

У вас слишком много ошибок в вашем коде (из-за того, что вы не очищаете сигнальную маску на struct sigaction), чтобы кто-нибудь мог объяснить эффекты, которые вы видите.

Вместо этого рассмотрим следующий рабочий пример кода, скажем example.c:

#define _POSIX_C_SOURCE 200809L 
#include <stdlib.h> 
#include <unistd.h> 
#include <sys/types.h> 
#include <sys/wait.h> 
#include <signal.h> 
#include <string.h> 
#include <stdio.h> 
#include <errno.h> 

/* Child process PID, and atomic functions to get and set it. 
* Do not access the internal_child_pid, except using the set_ and get_ functions. 
*/ 
static pid_t internal_child_pid = 0; 
static inline void set_child_pid(pid_t p) { __atomic_store_n(&internal_child_pid, p, __ATOMIC_SEQ_CST); } 
static inline pid_t get_child_pid(void) { return __atomic_load_n(&internal_child_pid, __ATOMIC_SEQ_CST); } 

static void forward_handler(int signum, siginfo_t *info, void *context) 
{ 
    const pid_t target = get_child_pid(); 

    if (target != 0 && info->si_pid != target) 
     kill(target, signum); 
} 

static int forward_signal(const int signum) 
{ 
    struct sigaction act; 

    memset(&act, 0, sizeof act); 
    sigemptyset(&act.sa_mask); 
    act.sa_sigaction = forward_handler; 
    act.sa_flags = SA_SIGINFO | SA_RESTART; 

    if (sigaction(signum, &act, NULL)) 
     return errno; 

    return 0; 
} 

int main(int argc, char *argv[]) 
{ 
    int status; 
    pid_t p, r; 

    if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) { 
     fprintf(stderr, "\n"); 
     fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]); 
     fprintf(stderr, "  %s COMMAND [ ARGS ... ]\n", argv[0]); 
     fprintf(stderr, "\n"); 
     return EXIT_FAILURE; 
    } 

    /* Install signal forwarders. */ 
    if (forward_signal(SIGINT) || 
     forward_signal(SIGHUP) || 
     forward_signal(SIGTERM) || 
     forward_signal(SIGQUIT) || 
     forward_signal(SIGUSR1) || 
     forward_signal(SIGUSR2)) { 
     fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno)); 
     return EXIT_FAILURE; 
    } 

    p = fork(); 
    if (p == (pid_t)-1) { 
     fprintf(stderr, "Cannot fork(): %s.\n", strerror(errno)); 
     return EXIT_FAILURE; 
    } 

    if (!p) { 
     /* Child process. */ 

     execvp(argv[1], argv + 1); 

     fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno)); 
     return EXIT_FAILURE; 
    } 

    /* Parent process. Ensure signals are reflected. */   
    set_child_pid(p); 

    /* Wait until the child we created exits. */ 
    while (1) { 
     status = 0; 
     r = waitpid(p, &status, 0); 

     /* Error? */ 
     if (r == -1) { 
      /* EINTR is not an error. Occurs more often if 
       SA_RESTART is not specified in sigaction flags. */ 
      if (errno == EINTR) 
       continue; 

      fprintf(stderr, "Error waiting for child to exit: %s.\n", strerror(errno)); 
      status = EXIT_FAILURE; 
      break; 
     } 

     /* Child p exited? */ 
     if (r == p) { 
      if (WIFEXITED(status)) { 
       if (WEXITSTATUS(status)) 
        fprintf(stderr, "Command failed [%d]\n", WEXITSTATUS(status)); 
       else 
        fprintf(stderr, "Command succeeded [0]\n"); 
      } else 
      if (WIFSIGNALED(status)) 
       fprintf(stderr, "Command exited due to signal %d (%s)\n", WTERMSIG(status), strsignal(WTERMSIG(status))); 
      else 
       fprintf(stderr, "Command process died from unknown causes!\n"); 
      break; 
     } 
    } 

    /* This is a poor hack, but works in many (but not all) systems. 
     Instead of returning a valid code (EXIT_SUCCESS, EXIT_FAILURE) 
     we return the entire status word from the child process. */ 
    return status; 
} 

Собирать использованием, например,

gcc -Wall -O2 example.c -o example 

и работать, используя, например,

./example sqlite3 

Вы заметите, что Ctrl + C не прерывает sqlite3 - но опять же, это не даже если вы запустите sqlite3 напрямую -; вместо этого вы просто видите ^C на экране. Это связано с тем, что sqlite3 устанавливает терминал таким образом, что Ctrl + C не вызывает сигнал и просто интерпретируется как обычный ввод.

Вы можете выйти из sqlite3 с помощью команды .quit, или нажав Ctrl + D в начале строки.

Вы увидите, что исходная программа выведет строку Command ... [], прежде чем возвращать вас в командную строку. Таким образом, родительский процесс не убивается/не повреждается/не беспокоится о сигналах.

Вы можете использовать ps f, чтобы посмотреть дерево процессов вашего терминала, и таким образом узнать PID родительского и дочернего процессов и отправить сигналы одному из них, чтобы наблюдать, что происходит.

Обратите внимание, что из-за SIGSTOP сигнал не может быть пойман, заблокирован или игнорируется, было бы нетривиальной отражать сигналы управления заданиями (как при использовании Ctrl +Z). Для правильного управления заданиями родительский процесс должен будет настроить новый сеанс и группу процессов и временно отключиться от терминала. Это тоже вполне возможно, но немного выходит за рамки этой области, поскольку это требует довольно подробного поведения сеансов, групп процессов и терминалов для правильного управления.

Давайте разберем приведенную выше примерную программу.

Пример самой программы сначала устанавливает некоторые отражатели сигнала, затем разворачивает дочерний процесс, и этот дочерний процесс выполняет команду sqlite3. (Вы можете speficy любого исполняемого файла и любые параметры строки в программу.)

internal_child_pid переменных и set_child_pid() и get_child_pid() функции, которые используются для управления дочернего процесса атомарно. __atomic_store_n() и __atomic_load_n() являются встроенными встроенными компиляторами; для GCC, see here для деталей. Они избегают проблемы с сигналом, когда дочерний pid только частично назначается. На некоторых общих архитектурах это не может произойти, но это предназначено в качестве осторожного примера, поэтому атомарные обращения используются для обеспечения только полного (старого или нового) значения. Мы могли бы избежать их использования полностью, если бы мы временно заблокировали связанные сигналы во время перехода. Опять же, я решил, что доступ к атомам проще, и может быть интересно увидеть на практике.

Функция forward_handler() получает физический идентификатор дочернего процесса, а затем проверяет, что он отличен от нуля (мы знаем, что у нас есть дочерний процесс), и что мы не пересылаем сигнал, отправленный дочерним процессом (просто чтобы убедиться, t вызывают сигнальный шторм, два бомбардируют друг друга сигналами). Различные поля в структуре siginfo_t перечислены в справочной странице man 2 sigaction.

Функция forward_signal() устанавливает вышеуказанный обработчик для заданного сигнала signum. Обратите внимание, что сначала мы используем memset(), чтобы очистить всю структуру до нулей. Очистка таким образом обеспечивает будущую совместимость, если часть дополнения в структуре преобразуется в поля данных.

Поле .sa_mask в struct sigaction является неупорядоченным набором сигналов. Сигналы, установленные в маске, блокируются от доставки в потоке, который выполняет обработчик сигнала. (В приведенной выше примерной программе мы можем с уверенностью сказать, что эти сигналы блокируются во время работы обработчика сигналов, а в многопоточных программах сигналы блокируются только в конкретном потоке, который используется для запуска обработчика.)

Для очистки маски сигнала важно использовать sigemptyset(&act.sa_mask). Просто установка структуры на ноль не достаточна, даже если она работает (вероятно) на практике на многих машинах. (Я не знаю, я даже не проверял. Я предпочитаю надежный и надежный, ленивый и хрупкий в любой день!)

Используемые флаги включают SA_SIGINFO, потому что обработчик использует форму с тремя аргументами (и использует поле si_pidsiginfo_t). SA_RESTART флаг существует только потому, что ОП хотел его использовать; это просто означает, что, если возможно, библиотека C и ядро ​​пытаются избежать ошибки errno == EINTR, если сигнал доставлен с использованием потока, который в настоящий момент блокируется в syscall (например, wait()). Вы можете удалить флаг SA_RESTART и добавить отладку fprintf(stderr, "Hey!\n"); в подходящее место в цикле в родительском процессе, чтобы узнать, что произойдет потом.

Функция sigaction() вернет 0, если нет ошибки, или -1 с errno установить иначе. Функция forward_signal() возвращает 0, если forward_handler был назначен успешно, но в противном случае ненулевое число errno. Некоторым не нравится этот тип возвращаемого значения (они предпочитают просто возвращать -1 для ошибки, а не значение errno), но я по какой-то необоснованной причине полюбил эту идиому. Измените его, если хотите, во что бы то ни стало.

Теперь мы добираемся до main().

Если вы запустите программу без параметров или с помощью одного параметра -h или --help, он распечатает резюме использования. Опять же, делать это таким образом - это то, что мне нравится - getopt() и getopt_long() чаще используются для анализа параметров командной строки. Для этой тривиальной программы я просто жестко запрограммировал проверку параметров.

В этом случае я намеренно оставил вывод использования очень коротким. Было бы намного лучше с дополнительным абзацем о точно , что программа делает. Эти тексты - и особенно комментарии в коде (объяснение цели , идея того, что должен делать код, а не описание того, что на самом деле делает код), очень важны. Прошло уже более двух десятилетий с тех пор, как я впервые заплатил за то, что написал код, и я все еще учусь, как комментировать, - опишите цель моего кода лучше, поэтому я думаю, что чем раньше он начнет работать над этим, лучше.

Часть fork() должна быть знакомой. Если он возвращает -1, вилка потерпела неудачу (вероятно, из-за ограничений или некоторых таких), и это очень хорошая идея, чтобы напечатать сообщение errno. Возвращаемое значение будет равно 0, а дочерний идентификатор процесса - в родительском процессе.

Функция execlp() принимает два аргумента: имя двоичного файла (каталоги, указанные в переменной среды PATH, будут использоваться для поиска такого двоичного файла), а также массив указателей на аргументы этого двоичного файла , Первый аргумент будет argv[0] в новом двоичном файле, то есть самом имени команды.

Звонок execlp(argv[1], argv + 1); на самом деле довольно прост для анализа, если сравнить его с приведенным выше описанием. argv[1] обозначает исполняемый двоичный файл. argv + 1 в основном эквивалентен (char **)(&argv[1]), то есть это массив указателей, начинающийся с argv[1] вместо argv[0]. Еще раз, я просто люблю идиому execlp(argv[n], argv + n), потому что она позволяет выполнять другую команду, указанную в командной строке, не беспокоясь о синтаксическом анализе командной строки или выполняя ее через оболочку (что иногда совершенно нежелательно).

Справочная страница man 7 signal объясняет, что происходит с обработчиками сигналов на fork() и exec(). Короче говоря, обработчики сигналов наследуются над fork(), но сбрасываются до значений по умолчанию exec(). К счастью, именно то, что мы хотим, здесь.

Если бы мы сначала разблокировали, а затем установили обработчики сигналов, у нас было бы окно, в течение которого дочерний процесс уже существует, но у родителя все еще есть диспозиции по умолчанию (в основном завершение) для сигналов.

Вместо этого мы могли бы просто заблокировать эти сигналы, используя, например, sigprocmask() в родительском процессе перед форкировкой. Блокировка сигнала означает, что он сделан «ждать»; он не будет доставлен до тех пор, пока сигнал не будет разблокирован. В дочернем процессе сигналы могут оставаться заблокированными, так как расположение сигналов по умолчанию сбрасывается на значения по умолчанию exec(). В родительском процессе мы могли бы - или до разветвления - не иметь значения - установить обработчики сигналов и, наконец, разблокировать сигналы. Таким образом, нам не понадобится атомный материал или даже проверить, равен ли ребенок pid нулю, так как дочерний pid будет установлен на его фактическое значение задолго до того, как любой сигнал будет доставлен!

Цикл while - это всего лишь цикл вокруг вызова waitpid(), до тех пор, пока не начнется точный дочерний процесс, или что-то смешное (дочерний процесс как-то пропадает). Этот цикл содержит довольно тщательную проверку ошибок, а также правильную обработку EINTR, если обработчики сигналов должны были быть установлены без флагов SA_RESTART.

Если дочерний процесс мы разветвляем, мы проверяем состояние выхода и/или причину его смерти, а также выводим диагностическое сообщение на стандартную ошибку.

И наконец, программа заканчивается ужасным взломом: вместо того, чтобы возвращать EXIT_SUCCESS или EXIT_FAILURE, мы возвращаем полное слово состояния, которое мы получили с waitpid, когда завершился дочерний процесс. Причина, по которой я оставил это, заключается в том, что он иногда используется на практике, когда вы хотите вернуть тот же или похожий код статуса выхода, что и возвращаемый дочерний процесс. Итак, это для иллюстрации. Если вы когда-либо оказываетесь в ситуации, когда ваша программа должна возвращать тот же статус выхода, что и дочерний процесс, который он разветвлял и выполнял, это все же лучше, чем создание механизма, чтобы процесс убил себя тем же сигналом, который убил ребенка обработать. Просто разместите там замечательный комментарий, если вам когда-либо понадобится его использовать, а также примечание в инструкциях по установке, чтобы те, кто компилирует программу на архитектурах там, где это может быть нежелательно, могут ее исправить.

+0

Это потрясающе ... Я все еще обрабатываю все это, но спасибо, что написал все. Я склонен ждать, пока не получу щедрость, а затем добавлю это тоже. – m0meni

+1

@ AR7: Не нужно. Я написал это в надежде, что ваш вопрос будет таким, с которым другие столкнутся, а также найдет объясненный пример полезным.Это много, чтобы принять сразу, но это также то, что вы можете сделать лучше, и играть с, если вы пробираетесь сквозь всю эту стену текста ... Если у вас есть какие-либо вопросы или проблемы с примером или его поведением, просто добавьте его в качестве комментария здесь, поэтому я получаю уведомление; Я только спорадически в сети. –

+0

Я полностью намерен пробираться через него несколько раз ... еще раз спасибо, и я обязательно сообщу, если у меня возникнут вопросы, как только я полностью его понял. – m0meni

 Смежные вопросы

  • Нет связанных вопросов^_^