Чому 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 新疆 改造 中心 法轮功 六四 事件

15

Як каже vinc17, немає причин для цього. Коли ви вводите послідовність ключів, що генерує сигнал (наприклад, Ctrl+ C), сигнал надсилається всім процесам, які приєднані до (пов'язаному) з терміналом. Не існує такого механізму для сигналів, що генеруються kill.

Однак така команда, як

kill -SIGINT -12345

посилатиме сигнал усім процесам у групі процесів 12345; див. вбити (1) і вбити (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
Це не працює, коли процес запускається в інтерактивній оболонці (для чого використовується ОП). Я не маю посилання на таку поведінку.
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

@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, і тепер тільки один знак надрукується щоразу від батьків лише:

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.