Хотя мы уже упоминали, что передача SIGTERM
и SIGKILL
функции kill()
прерывает процесс, вы также можете использовать несколько других значений (все они описаны в главе 12). Некоторые из них, такие как SIGABRT
, заставляют программу перед уничтожением сбрасывать дамп ядра (dump core).
Дамп ядра программы содержит полную хронологию состояния программы перед ее уничтожением[27]. Большинство отладчиков, включая gdb
, могут анализировать файл дампа и рассказывать, что программа делала непосредственно перед тем, как была уничтожена, а также поможет исследовать образ памяти процесса. Дамп ядра выгружается в файл по имени core, расположенный в текущем каталоге процесса.
Когда процесс нарушает какие-то системные требования (например, пытается обратиться к памяти, доступ к которой запрещен), ядро прерывает процесс, вызывая встроенную версию kill()
с параметром, который заставляет выгрузить дамп ядра. Ядро может уничтожать процессы по разным причинам, включая арифметические ошибки, такие как деление на ноль, либо по причине выполнения программой некорректных инструкций, либо при попытке доступа к запрещенной области памяти. Последняя причина вызывает ошибку сегментации, что выражается в сообщении segmentation fault (core dumped)
(ошибка сегментации (дамп ядра сброшен)). Если вы обладаете хоть каким-нибудь опытом программирования в Linux, то наверняка неоднократно получали это сообщение.
Если для процесса установлен лимит на размер файла дампа, равный 0 (рассматривался ранее в этой главе), то никакой дамп ядра не выгружается.
10.5. Простые дочерние процессы
Хотя функции fork()
, exec()
и wait()
позволяют программам в полной мере использовать модель процессов Linux, многим приложениям не нужен такой контроль дочерних процессов. Существуют две библиотечных функции, которые упрощают создание дочерних процессов: system()
и popen()
.
10.5.1. Запуск и ожидание с помощью system()
Программам часто требуется запускать другие программы и ожидать их завершения, прежде чем продолжать свою работу. Функция system()
позволяет это делать достаточно просто.
int system (const char* cmd);
system()
порождает дочерний процесс, который выполняет exec()
для /bin/sh
, который, в свою очередь, запускает cmd
. Исходный процесс ожидает завершения дочерней оболочки и возвращает тот же код, что wait()
[28]. Если вам не нужно оставлять в памяти оболочку (что случается редко), cmd
должна включать предшествующее слово "exec"
, которое заставляет оболочку вызывать exec()
вместо запуска cmd
как подпроцесса.
Поскольку cmd
запускается из оболочки /bin/sh
, то здесь применимы все обычные правила расширения команд. Ниже показан пример вызова system()
, который отображает исходные тексты С из текущего каталога.
#include <stdlib.h>
#include <sys/wait.h>
int main() {
int result;
result = system("exec ls *.c");
if (!WIFEXITED(result))
printf("(аварийный выход)\n");
exit(0);
}
Команда system()
должна применяться с большой осторожностью в программах, которые запускаются со специальными полномочиями. Поскольку системная оболочка предоставляет множество мощных средств и сильно зависит от переменных окружения, system()
является уязвимым местом в плане безопасности, которым могут воспользоваться злоумышленники для проникновения в систему. Однако до тех пор, пока приложение не является демоном или программой setuid/setgid
, вызов system()
совершенно безопасен.
10.5.2. Чтение и запись из процесса
Хотя system()
отображает результат работы команды на устройство стандартного вывода и позволяет дочерним программам читать стандартный ввод, это не всегда идеально. Часто процесс желает читать вывод другого процесса либо отправлять текст на стандартный ввод. popen()
облегчает процессам решение этой задачи[29].
FILE * popen(const char *cmd, const char *mode);
cmd
выполняется через оболочку, как и в system()
. Параметр mode
должен быть "r"
, если родительский процесс желает читать командный вывод, и "w"
— для записи в стандартный ввод дочернего процесса. Следует отметить, что с помощью popen()
делать одновременно чтение и запись нельзя.
Два процесса, которые читают и пишут друг в друга, достаточно сложны[30] и выходят за рамки возможностей popen()
[31].
popen()
возвращает FILE*
(как это определено в стандартной библиотеке ввода-вывода ANSI/ISO), который может быть прочитан и записан подобно любому другому потоку stdio
[32], либо NULL
, если операция не удается. Когда завершается родительский процесс, он может воспользоваться pclose()
для закрытия потока и прерывания дочернего процесса, если он все еще выполняется. Подобно system()
, pclose()
возвращает состояние дочернего процесса из wait4()
.
int pclose(FILE *stream);
Ниже приведен пример простой программы-калькулятора, которая использует программу bc
для выполнения всей реальной работы. Важно сбрасывать поток, полученный от popen()
, после записи в него, чтобы предотвратить буферизацию stdio
от задержки вывода (подробности о буферизации стандартных функций библиотеки stdio
можно найти в [15]).
1: /*calc.c*/
2:
3: /* Это очень простой калькулятор, который использует внешнюю команду bc
4: для выполнения всей работы. Открывает канал к bc, читает команду,
5: передает ее bc и завершается. */
6: #include <stdio.h>
7: #include <sys/wait.h>
8: #include <unistd.h>
9:
10: int main(void) {
11: char buf[1024];
12: FILE *bc;
13: int result;
14:
15: /* открыть канал на bc и выйти в случае неудачи */
16: bc = popen("bc", "w");
17: if (!bc) {
18: perror("popen");
19: return 1;
20: }
21:
22: /* пригласить ввести выражение, и прочитать его */
23: printf("expr:"); fflush(stdout);
24: fgets(buf, sizeof(buf), stdin);
25:
26: /* послать выражение bc для вычисления */
27: fprintf(bc, "%s\n", buf);
28: fflush(bc);
29:
30: /* закрыть канал на bc и ожидать выхода из нее */
31: result = pclose(bc);
32:
33: if (!WIFEXITED(result))
27
Одна из популярных ранее форм компьютерной памяти выглядела как набор маленьких железных колечек, расположенных на матрице, к каждому из которых подводились два проводка, служащих для установки и считывания магнитной полярности кольца. Эти кольца назывались ядрами (cores), а все вместе — ядерной памятью. Поэтому дамп ядра — это копия состояния системной памяти в определенный момент времени.
28
В процессе работы system()
блокирует SIGCHILD
, что заставляет передавать этот сигнал программе непосредственно перед тем, как system()
вернет управление (но после того, как system()
вызовет wait()
для порожденного процесса), поэтому программы, которые используют обработчики сигналов, должны это учитывать и обрабатывать такие ложные сигналы осторожно. Функция system()
также игнорирует SIGINT
и SIGQUIT
, а это означает, что быстрые циклические повторные вызовы system()
может оказаться невозможно прервать ничем, кроме SIGSTOP
и SIGKILL
.
29
Хотя функция popen()
это делает просто, с ней связаны некоторые побочные эффекты, которые не сразу становятся очевидны. Она создает дочерний процесс, который может быть прерван перед тем, как будет вызвана pclose()
, что заставит функцию wait()
вернуть состояние процесса. Когда этот процесс завершится, он также сгенерирует SIGCHLD
, что может привести в замешательство упрощенно написанный обработчик сигналов.
30
Этот тип обработки часто приводит к взаимоблокировкам, при которых процесс А ожидает, пока процесс В выполнит какую-то работу, в то время как процесс В ожидает процесса А, в результате чего ничего не происходит.
31
Если вам понадобится делать это, запустите дочерний процесс с помощью fork()
и exec()
, а потом воспользуйтесь poll()
для чтения и записи в дочерний процесс. Для этого предназначена программа под названием expect
.