Обработчики сигналов предохраняются внутри функций
exec
несколько неочевидным образом. Этот механизм рассматривается в главе 12.
10.4.4. Ускоренное создание процессов с помощью vfork
Обычно процессы, в которых вызывается
fork
, немедленно вызывают
exec
для другой программы (это то, что оболочка делает всякий раз, когда вы вводите команду), что делает полную семантику
fork
более расточительной по вычислительным ресурсам, чем это необходимо.
Чтобы оптимизировать этот общий случай, существует
vfork
.
#include <unistd.h>
pid_t vfork(void);
Вместо создания совершенно новой среды выполнения для нового процесса
vfork
создает новый процесс, который разделяет память с исходным процессом. Ожидается,
что новый процесс запустит другой процесс посредством
exit
или
exec
очень быстро, но его поведение непредсказуемо, если он модифицирует память, возвратит управление из функции
vfork
, содержащейся в нем, либо вызовет любую новую функцию. В дополнение к этому исходный процесс приостанавливается, до тех пор, пока новый либо не будет прерван, либо вызовет функцию
exec
[24] . Однако не все системы обеспечивают семантику разделения памяти и приостановки родительского процесса
vfork
, поэтому приложения не должны полагаться на такое поведение.
24
Появление
vfork
было мотивировано старыми системами, которым необходимо было копировать всю память, используемую исходным процессом, как часть
fork
.Современные операционные системы используют копирование при записи, которое копирует области памяти только по необходимости, как это описано во многих источниках, посвященных операционным системам, в частности [40] и [2]. Это свойство делает
fork
почти таким же быстрым, как
vfork
, и намного более простым в использовании.
10.4.5. Уничтожение процессом самого себя
Процессы прерывают себя вызовом либо
exit
, либо
_exit
. Когда функция процесса
main
возвращает управление, стандартная библиотека С вызывает
exit
со значением, возвращаемым
main
в качестве параметра.
void exit(int exitCode);
void _exit(int exitCode);
Две формы,
exit
и
_exit
, отличаются тем, что
exit
— функция из библиотеки С, a
_exit
— системный вызов. Системный вызов
_exit
прерывает программу немедленно, и
exitCode
сохраняется в качестве кода возврата процесса. Когда используется
exit
, то перед тем, как запустить системный вызов
_exit(exitCode)
, вызываются функции, зарегистрированные в
atexit
. Помимо всего прочего, это позволяет стандартной библиотеке ввода-вывода ANSI/ISO сбросить все свои буферы.
Регистрация функций, которые должны быть запущены при вызове
exit
, выполняется с помощью функции
atexit
:
int atexit(void (*function) (void));
Единственный параметр, переданный
atexit
— это указатель на функцию. Когда вызывается
exit
, все функции, зарегистрированные через
atexit
, вызываются в порядке, обратном тому, в котором они регистрировались. Следует отметить, что если используется
_exit
либо процесс прерывается сигналом (подробно о сигналах читайте в главе 12), то функции, зарегистрированные
atexit
, не вызываются.
10.4.6. Уничтожение других процессов
Разрушение другого процесса почти столь же просто, как создание нового — нужно просто уничтожить его:
int kill(pid_t pid, int signum);
pid
должен быть идентификатором процесса, который требуется уничтожить, а
signum
описывает, как это нужно сделать. Доступны два варианта выполнения операции [25] прерывания дочернего процесса. Вы можете применить
SIGTERM
, чтобы прервать его "вежливо". Это означает, что процесс при этом может сообщить ядру о том, что кто-то пытается его уничтожить; в результате появляется возможность завершить его корректно (сохранив файлы, например). Процесс может в этом случае игнорировать запрос на прерывание такого типа и продолжать выполняться. Применение значения
SIGKILL
в качестве параметра
signum
вызывает немедленное прерывание процесса без каких-либо вопросов. Если
signum
равно
0
, то
kill
проверяет, имеет ли тот процесс, что вызвал
kill
, соответствующие полномочия, возвращает ноль, если это так, либо ненулевое значение, если полномочий недостаточно. Это обеспечивает процессу возможность проверки корректности
pid
.
25
Это — существенное упрощение. В действительности
kill
посылает сигнал, а сигналы сами по себе достаточно сложная тема. См. полное описание того, что такое сигналы и как их применять, в главе 12.
Параметр
pid
в среде Linux может принимать перечисленные ниже значения.
pid > 0
Сигнал отправляется процессу с идентификатором
pid
. Если такого процесса нет, возвращается
ESRCH
.
pid < -1
Сигнал посылается всем процессам, принадлежащим группе с pgid, равным
– pid
. Например,
kill(-5316, SIGKILL)
немедленно прерывает все процессы из группы 5316. Такая возможность используется оболочками управления заданиями, как описано в главе 15.
pid = 0
Сигнал отправляется всем процессам группы, к которой относится текущий процесс.
pid = -1
Сигнал посылается всем процессам системы за исключением инициализирующего процесса (init). Это применяется для полного завершения системы.
Процессы могут нормально уничтожать вызовом
kill
только те процессы, которые разделяют тот же эффективный идентификатор пользователя, что и у них самих. Существуют два исключения из этого правила. Во-первых, процессы с эффективным uid, равным 0, могут уничтожать любые процессы в системе. Во-вторых, любой процесс может посылать сигнал
Это нужно для того, чтобы управляющая заданиями оболочка могла перезапускать процессы, у которых изменился эффективный идентификатор пользователя. Более подробно об управлении заданиями рассказывается в главе 15.
Хотя мы уже упоминали, что передача
SIGTERM
и
SIGKILL
функции
kill
прерывает процесс, вы также можете использовать несколько других значений (все они описаны в главе 12). Некоторые из них, такие как
SIGABRT
, заставляют программу перед уничтожением сбрасывать дамп ядра (dump core).
Дамп ядра программы содержит полную хронологию состояния программы перед ее уничтожением [27] . Большинство отладчиков, включая
gdb
, могут анализировать файл дампа и рассказывать, что программа делала непосредственно перед тем, как была уничтожена, а также поможет исследовать образ памяти процесса. Дамп ядра выгружается в файл по имени core, расположенный в текущем каталоге процесса.
27
Одна из популярных ранее форм компьютерной памяти выглядела как набор маленьких железных колечек, расположенных на матрице, к каждому из которых подводились два проводка, служащих для установки и считывания магнитной полярности кольца. Эти кольца назывались ядрами (cores), а все вместе — ядерной памятью. Поэтому дамп ядра — это копия состояния системной памяти в определенный момент времени.
Когда процесс нарушает какие-то системные требования (например, пытается обратиться к памяти, доступ к которой запрещен), ядро прерывает процесс, вызывая встроенную версию
kill
с параметром, который заставляет выгрузить дамп ядра. Ядро может уничтожать процессы по разным причинам, включая арифметические ошибки, такие как деление на ноль, либо по причине выполнения программой некорректных инструкций, либо при попытке доступа к запрещенной области памяти. Последняя причина вызывает ошибку сегментации, что выражается в сообщении
segmentation fault (core dumped)
(ошибка сегментации (дамп ядра сброшен)). Если вы обладаете хоть каким-нибудь опытом программирования в Linux, то наверняка неоднократно получали это сообщение.
Если для процесса установлен лимит на размер файла дампа, равный 0 (рассматривался ранее в этой главе), то никакой дамп ядра не выгружается.
10.5. Простые дочерние процессы
Хотя функции
fork
,
exec
и
wait
позволяют программам в полной мере использовать модель процессов Linux, многим приложениям не нужен такой контроль дочерних процессов. Существуют две библиотечных функции, которые упрощают создание дочерних процессов: