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

С zsh Вы могли сделать что-то как:

#! /bin/zsh -
(($+ARG0_SET)) || ARG0_SET= ARGV0="#Running on 10.10.45.10" exec zsh "$0" "$@"
ps -f -p "$$"

Который, когда выполненный дает:

~$ ./a
UID        PID  PPID  C STIME TTY          TIME CMD
chazelas 20157  8822  0 21:51 pts/1    00:00:00 #Running on 10.10.45.10 ./a

ksh93 эквивалентный был бы:

#! /bin/ksh93 -
((${#ARG0_SET})) || ARG0_SET=yes exec -a "#Running on 10.10.45.10" ksh93 "$0" "$@"
ps -f -p "$$"
62
16.09.2015, 22:49
5 ответов

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

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

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

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

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

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

ps ax -O tpgid

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

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

Теперь, когда мы знаем, что такое ID группы процесса, нам нужно смоделировать поведение 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.

86
27.01.2020, 19:32

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

Однако команда типа

kill -SIGINT -12345

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


Упс

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

kill -SIGINT -$(echo $(ps -pPID_of_shell o tpgid=))

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

Я заставил это работать в беглом тестировании на машине Debian.

15
27.01.2020, 19:32

Нет причин распространять ЗАПИСЬ на ребенка. Более того, спецификация system() POSIX гласит: "Функция system() должна игнорировать сигналы SIGINT и SIGQUIT и блокировать сигнал SIGCHLD, ожидая завершения команды"

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

3
27.01.2020, 19:32

Кажется, что так и должно быть:

Из журнала изменений selinux-policy-3.12.1-139:

- Разрешить ввод systemd_cronjob_t через, bin_t

Do у вас есть какие-либо ошибки в /var/log/audit/audit.log , относящиеся к mariadb ? Быстрая и простая проверка состоит в том, чтобы setenforce = 0 и запустить задания cron . Если им лучше, то это был SELinux, вызвавший проблему.

-121--230623-

Если в системе имеется systemd, для этого можно использовать события таймеров. Просто определите новую службу , которая должна содержать команду/задачу, которую необходимо выполнить, а затем создайте событие таймера с параметром OnUnitActiveSec :

[Unit]
Description=daily + 1 hour task

[Timer]
OnUnitActiveSec=25h # run 25 hours after service was last started
AccuracySec=10min

[Install]
WantedBy=timers.target

Используйте то же имя для файлов, за исключением того, что вместо .service используется .timer .

Синтез:

  1. Создайте файл с именем job.service в каталоге /etc/systemd/system/.
  2. Заполните его необходимой информацией. Проверить конфигурацию можно с помощью systemctl status job.service .
  3. Создайте файл с именем job.timer в /etc/systemd/system/.
  4. Заполните его необходимой информацией:

      [Единица измерения]
    Описание = ежедневная + 1 часовая задача
    
    [Таймер]
    OnUnitActiveSec = 25h # запуск через 25 часов после последнего запуска службы
    Рс = 10мин
    
    [Установить]
    PaydBy = timers.target
    
  5. Проверьте таймер, используя systemctl list-timers
  6. Done.
-121--26842-

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

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

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

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

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

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

В ответ на запрос оболочки в новом терминале выполните следующую команду:

exec cat

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

Ответ:

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

12
27.01.2020, 19:32

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.

1
27.01.2020, 19:32

Теги

Похожие вопросы