Как Ctrl-C завершает дочерний процесс?

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

Не могли бы вы объяснить, как это работает и, в частности, почему не завершен родительский процесс (оболочка)?

Должна ли оболочка выполнять специальную обработку события CTRL+C, и если да, что именно она делает?

5 ответов

Решение

Сигналы по умолчанию обрабатываются ядром. Старые системы Unix имели 15 сигналов; теперь у них больше. Ты можешь проверить </usr/include/signal.h> (или убить -l). CTRL+C - это сигнал с именем SIGINT,

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

Все сигналы (но SIGKILL) может быть обработано программой.

И вот что делает оболочка:

  • Когда оболочка работает в интерактивном режиме, она имеет специальную обработку сигналов для этого режима.
  • Например, при запуске программы findоболочка:
    • forkсама
    • а для ребенка установите обработку сигналов по умолчанию
    • замените ребенка с помощью данной команды (например, с помощью поиска)
    • когда вы нажимаете CTRL+C, родительская оболочка обрабатывает этот сигнал, но дочерний элемент получает его - с действием по умолчанию - завершается. (ребенок может также реализовать обработку сигналов)

Вы можете trap сигналы в вашем сценарии оболочки тоже...

И вы можете настроить обработку сигналов для вашей интерактивной оболочки, попробуйте ввести это в верхней части ~/.profile, (Убедитесь, что вы уже вошли в систему и протестируйте его с другим терминалом - вы можете заблокировать себя)

trap 'echo "Dont do this"' 2

Теперь, каждый раз, когда вы нажимаете CTRL+C в вашей оболочке, он будет печатать сообщение. Не забудьте убрать строку!

Если интересно, можете проверить старую старую /bin/sh обработка сигналов в исходном коде здесь.

Выше были некоторые дезинформации в комментариях (теперь удаленные), так что, если кто-то заинтересован здесь, очень хорошая ссылка - как работает обработка сигналов.

Во-первых, полностью прочитайте статью из Википедии об интерфейсе терминала POSIX.

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

Сам процесс оболочки находится в еще одной отдельной группе процессов и поэтому не получает сигнал, когда одна из этих групп процессов находится на переднем плане. Это так просто.

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

Ни один процесс не обязательно принудительно закрывается (хотя по умолчанию, если вы не обрабатываете sigint, я считаю, что поведение должно вызывать abort(), но мне нужно это посмотреть).

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

Если вы хотите, чтобы вышла родительская оболочка, запустите вашу программу с exec:

exec ./myprogram

Таким образом, родительская оболочка заменяется дочерним процессом

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

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

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

Тогда, конечно, если ребенок обрабатывает сигнал, то он не убивается как обычно.

main.c:

void signal_handler(int sig) {
    char sigint_str[] = "sigint\n";
    signal(sig, signal_handler);
    if (sig == SIGINT) {
        write(STDOUT_FILENO, sigint_str, sizeof(sigint_str) - 1);
    }
}

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

    signal(SIGINT, signal_handler);
    signal(SIGUSR1, signal_handler);
    pid = fork();
    assert(pid != -1);
    if (pid == 0) {
        /* 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.
         */
        if (argc == 1) {
            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);
}

Скомпилируйте и запустите:

gcc -g -std=c99 -Wall -Wextra -o setpgid setpgid.c -lpthread
./setpgid

Результат:

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

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

Pgid обоих процессов одинаков, так как он наследуется через fork,

Тогда, если вы нажмете:

Ctrl + C

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

sigint
sigint

Вот как:

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

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

./setpgid 1

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

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

Однако если вы изменили идентификатор дочерней группы, он также не получит этот SIGQUIT, и вы должны явно убить его:

ps aux | grep setpgid
kill -9 $PID

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

Проверено на Ubuntu 18.04. GitHub вверх по течению.

CTRL+C - это карта для команды kill. Когда вы нажимаете их, kill отправляет сигнал SIGINT, который прерывает процесс.

Убить: http://en.wikipedia.org/wiki/kill_%28command%29

SIGINT: http://en.wikipedia.org/wiki/SIGINT_%28POSIX%29

Другие вопросы по тегам