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


62

Учитывая процесс оболочки (например, sh) и его дочерний процесс (например cat), как я могу имитировать поведение Ctrl+, Cиспользуя идентификатор процесса оболочки?


Вот что я пробовал:

Запуск shи затем cat:

[user@host ~]$ sh
sh-4.3$ cat
test
test

Отправка SIGINTна catдругой терминал:

[user@host ~]$ kill -SIGINT $PID_OF_CAT

cat получил сигнал и прекратил (как и ожидалось).

Отправка сигнала родительскому процессу, похоже, не работает. Почему сигнал не распространяется catпри отправке его родительскому процессу sh?

Это не работает:

[user@host ~]$ kill -SIGINT $PID_OF_SH

1
В оболочке есть способ игнорировать сигналы SIGINT, не отправленные с клавиатуры или терминала.
konsolebox

Ответы:


86

Как CTRL+ Cработает

Прежде всего, чтобы понять, как работает CTRL+ C.

Когда вы нажимаете CTRL+ C, ваш эмулятор терминала отправляет символ ETX (конец текста / 0x03).
TTY настроен так, что, когда он получает этот символ, он отправляет SIGINT в группу процессов переднего плана терминала. Эту конфигурацию можно посмотреть, выполнив sttyи посмотрев intr = ^C;. В спецификации POSIX говорится, что при получении INTR он должен отправить SIGINT в группу процессов переднего плана этого терминала.

Что такое группа процессов переднего плана?

Итак, теперь вопрос в том, как вы определяете, что такое группа процессов переднего плана? Группа процессов переднего плана - это просто группа процессов, которые будут получать любые сигналы, генерируемые клавиатурой (SIGTSTOP, SIGINT и т. Д.).

Простейшим способом определения идентификатора группы процессов является использование ps:

ps ax -O tpgid

Второй столбец будет идентификатором группы процессов.

Как отправить сигнал группе процессов?

Теперь, когда мы знаем, что такое идентификатор группы процессов, нам нужно смоделировать поведение POSIX при отправке сигнала всей группе.

Это можно сделать kill, поставив -перед идентификатором группы.
Например, если идентификатор группы процессов равен 1234, вы должны использовать:

kill -INT -1234

 


Имитация CTRL+ Cс использованием номера терминала.

Таким образом, выше описано, как имитировать CTRL+ Cкак ручной процесс. Но что, если вы знаете номер TTY и хотите имитировать CTRL+ Cдля этого терминала?

Это становится очень легко.

Предположим, $ttyэто терминал, на который вы хотите ориентироваться (вы можете получить его, запустив tty | sed 's#^/dev/##'в терминале).

kill -INT -$(ps h -t $tty -o tpgid | uniq)

Это отправит SIGINT любой группе процессов переднего плана $tty.


6
Стоит отметить, что сигналы, которые поступают непосредственно из терминала, обходят проверку прав доступа, поэтому Ctrl + C всегда успешно доставляет сигналы, если вы не отключите его в атрибутах терминала, в то время как killкоманда может завершиться ошибкой.
Брайан Би

4
+1, дляsends a SIGINT to the foreground process group of the terminal.
Энди

Стоит упомянуть, что группа процессов ребенка такая же, как родительская группа после fork. Пример минимального запуска C: unix.stackexchange.com/a/465112/32558
Ciro Santilli lli 改造 中改造 心 六四 事件

15

Как говорит vinc17, нет причин для этого. Когда вы набираете последовательность ключей, генерирующих сигнал (например, Ctrl+ C), сигнал отправляется всем процессам, которые подключены (связаны с) к терминалу. Не существует такого механизма для сигналов, генерируемых kill.

Тем не менее, команда как

kill -SIGINT -12345

отправит сигнал всем процессам в группе процессов 12345; см. kill (1) и kill (2) . Дочерние элементы оболочки обычно находятся в группе процессов оболочки (по крайней мере, если они не асинхронны), поэтому отправка сигнала на отрицательный идентификатор PID оболочки может сделать то, что вы хотите.


ой

Как указывает vinc17, это не работает для интерактивных оболочек. Вот альтернатива, которая может работать:

kill -SIGINT - $ (echo $ (ps -p PID_of_shell o tpgid =))

ps -pPID_of_shellполучает информацию о процессе в оболочке.  o tpgid=говорит psвыводить только идентификатор группы процессов терминала без заголовка. Если это меньше, чем 10000, psбудет отображаться с пробелом; $(echo …)быстрый трюк , чтобы сдирать ведущих (и конечные) пространства.

Я сделал это, чтобы работать в кратком тестировании на компьютере Debian.


1
Это не работает, когда процесс запускается в интерактивной оболочке (это то, что использует OP). У меня нет ссылки на это поведение, хотя.
vinc17

12

Вопрос содержит свой ответ. Посылая SIGINTк catпроцессу с killидеальным моделированием того , что происходит при нажатии кнопки ^C.

Чтобы быть более точным, символ прерывания ( ^Cпо умолчанию) отправляется SIGINTкаждому процессу в группе процессов переднего плана терминала. Если бы вместо catвас выполнялась более сложная команда, включающая несколько процессов, вам пришлось бы убить группу процессов, чтобы добиться того же эффекта, что и ^C.

Когда вы запускаете любую внешнюю команду без &оператора фона, оболочка создает новую группу процессов для команды и уведомляет терминал о том, что эта группа процессов теперь находится на переднем плане. Оболочка все еще находится в собственной группе процессов, которая больше не находится на переднем плане. Затем оболочка ожидает команды на выход.

Вот где вы, кажется, стали жертвой из-за распространенного заблуждения: идея о том, что оболочка что-то делает для облегчения взаимодействия между дочерним процессом (процессами) и терминалом. Это просто неправда. Как только он выполнил работу по настройке (создание процесса, настройка режима терминала, создание каналов и перенаправление других файловых дескрипторов и выполнение целевой программы), оболочка просто ждет . То, что вы вводите, catне проходит через оболочку, будь то обычный ввод или специальный символ, генерирующий сигнал ^C. catПроцесс имеет прямой доступ к терминалу через его собственных файловых дескрипторов, и терминал имеет возможность передавать сигналы непосредственно в catпроцессе , потому что это на переднем плане группы процессов.

После того, как catпроцесс умирает, оболочка будет уведомлена, потому что это родительский catпроцесс. Затем оболочка становится активной и снова ставит себя на передний план.

Вот упражнение, чтобы улучшить ваше понимание.

В приглашении оболочки в новом терминале выполните эту команду:

exec cat

execКлючевое слово вызывает оболочку для выполнения catбез создания дочернего процесса. Оболочка заменена на cat. PID, который раньше принадлежал оболочке, теперь является PID cat. Проверьте это с psпомощью другого терминала. Наберите несколько случайных строк и посмотрите, что catони вам возвращаются, доказывая, что он по-прежнему ведет себя нормально, несмотря на отсутствие процесса оболочки в качестве родителя. Что будет, когда вы нажмете ^Cсейчас?

Ответ:

SIGINT доставляется в кошачий процесс, который умирает. Поскольку это был единственный процесс на терминале, сеанс завершается, как если бы вы сказали «выйти» в командной строке. В действительности, кошка была твоей скорлупой на некоторое время.


Снаряд ушел с дороги. +1
Петр Доброгост

Я не понимаю, почему после exec catнажатия ^Cне попал бы ^Cв кошку. Почему бы это прекратить тот, catкоторый сейчас заменил оболочку? Поскольку оболочка была заменена, оболочка - это то, что реализует логику отправки SIGINT его дочерним элементам при получении ^C.
Стивен Лу

Дело в том, что оболочка не отправляет SIGINT своим дочерним элементам. SIGINT поступает из драйвера терминала и отправляется всем процессам переднего плана.

3

Там нет причин для размножения SIGINTребенка. Более того, system()спецификация POSIX гласит: «Функция system () должна игнорировать сигналы SIGINT и SIGQUIT и должна блокировать сигнал SIGCHLD, ожидая завершения команды».

Если оболочка распространит полученное SIGINT, например, после реального нажатия Ctrl-C, это будет означать, что дочерний процесс получит SIGINTсигнал дважды, что может привести к нежелательному поведению.


Оболочка не должна реализовывать это с system(). Но вы правы: если он ловит сигнал (очевидно, он это делает), то нет причин распространять его вниз.
Златовласка

@goldilocks Я завершил свой ответ, возможно, объясняя причину. Обратите внимание, что оболочка не может знать, получил ли ребенок уже сигнал, отсюда и проблема.
vinc17

1

setpgid Минимальный пример группы процессов POSIX C

Это может быть легче понять с помощью минимального работоспособного примера базового API.

Это показывает, как сигнал действительно отправляется ребенку, если ребенок не изменил свою группу процессов с помощью setpgid.

main.c

#define _XOPEN_SOURCE 700
#include <assert.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

volatile sig_atomic_t is_child = 0;

void signal_handler(int sig) {
    char parent_str[] = "sigint parent\n";
    char child_str[] = "sigint child\n";
    signal(sig, signal_handler);
    if (sig == SIGINT) {
        if (is_child) {
            write(STDOUT_FILENO, child_str, sizeof(child_str) - 1);
        } else {
            write(STDOUT_FILENO, parent_str, sizeof(parent_str) - 1);
        }
    }
}

int main(int argc, char **argv) {
    pid_t pid, pgid;

    (void)argv;
    signal(SIGINT, signal_handler);
    signal(SIGUSR1, signal_handler);
    pid = fork();
    assert(pid != -1);
    if (pid == 0) {
        is_child = 1;
        if (argc > 1) {
            /* Change the pgid.
             * The new one is guaranteed to be different than the previous, which was equal to the parent's,
             * because `man setpgid` says:
             * > the child has its own unique process ID, and this PID does not match
             * > the ID of any existing process group (setpgid(2)) or session.
             */
            setpgid(0, 0);
        }
        printf("child pid, pgid = %ju, %ju\n", (uintmax_t)getpid(), (uintmax_t)getpgid(0));
        assert(kill(getppid(), SIGUSR1) == 0);
        while (1);
        exit(EXIT_SUCCESS);
    }
    /* Wait until the child sends a SIGUSR1. */
    pause();
    pgid = getpgid(0);
    printf("parent pid, pgid = %ju, %ju\n", (uintmax_t)getpid(), (uintmax_t)pgid);
    /* man kill explains that negative first argument means to send a signal to a process group. */
    kill(-pgid, SIGINT);
    while (1);
}

GitHub вверх по течению .

Компилировать с:

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -Wpedantic -o setpgid setpgid.c

Бегать без setpgid

Без каких-либо аргументов CLI setpgidне обойтись:

./setpgid

Возможный результат:

child pid, pgid = 28250, 28249
parent pid, pgid = 28249, 28249
sigint parent
sigint child

и программа зависает.

Как мы видим, pgid обоих процессов одинаков, поскольку он наследуется fork.

Тогда всякий раз, когда вы нажмете:

Ctrl + C

Это выводит снова:

sigint parent
sigint child

Это показывает, как:

  • отправить сигнал всей группе процессов с kill(-pgid, SIGINT)
  • Ctrl + C на терминале отправляет уничтожение всей группе процессов по умолчанию

Выйдите из программы, отправив разные сигналы обоим процессам, например, SIGQUIT с помощью Ctrl + \.

Бежать с setpgid

Если вы запускаете с аргументом, например:

./setpgid 1

потом потомок меняет свой pgid, и теперь каждый раз из одного родителя печатается только один sigint:

child pid, pgid = 16470, 16470
parent pid, pgid = 16469, 16469
sigint parent

А теперь, когда вы нажмете:

Ctrl + C

только родитель также получает сигнал:

sigint parent

Вы по-прежнему можете убить родителя, как и раньше, используя SIGQUIT:

Ctrl + \

однако у ребенка теперь есть другой PGID, и он не получает этот сигнал! Это видно из:

ps aux | grep setpgid

Вам придется убить его явно с помощью:

kill -9 16470

Это проясняет, почему существуют группы сигналов: в противном случае мы бы получили кучу процессов, которые будут очищаться вручную все время.

Проверено на Ubuntu 18.04.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.