Как 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, который прерывает процесс.