Вызов kill для дочернего процесса с помощью SIGTERM завершает родительский процесс, но вызов с помощью SIGKILL сохраняет родительский процесс

продолжение как предотвратить SIGINT в дочернем процессе от распространения и убийства родительского процесса?

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

Я попытался реализовать это, но вот проблема. По отношению к в частности,kill syscall я вызываю, чтобы завершить ребенка, если я передаю 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 SQLiten"
           "2) Quitn"
           "-> "
          );
    scanf("%1s", str);
    if (str[0] == '1') {
      CHILD = fork();
      if (CHILD == 0) {
        execlp("sqlite3", "sqlite3", NULL);
        printf("exec failedn");
      } else {
        wait(NULL);
        printf("Hin");
      }
    } else if (str[0] == '2') {
      break;
    } else {
      printf("Invalid!n");
    }
  }
}

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

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

как я отмечаю, я не в восторге от моего

1 ответов


у вас слишком много ошибок в коде (не сняв маску сигналов на 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 С помощью или клавишей Ctrl+D в начале строки.

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

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

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

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

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

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

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

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

на 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 man page объясняет, что происходит с обработчиками сигналов в fork() и exec(). Короче говоря, обработчики сигналов наследуются по a fork(), но сбросить значения по умолчанию на exec(). Что, к счастью, именно так и есть. мы хотим здесь.

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

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

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

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

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