int main(void) {
pid_t child;
if (!(child = fork())) {
printf("в дочернемn");
exit (0);
}
printf("в родительском - дочерний: %dn", child);
return 0;
}
10.4.2. Наблюдение за уничтожением дочерних процессов
Сбор состояний возврата дочернего процесса называется ожиданием процесса. Это можно делать четырьмя способами, хотя только один из вызовов предоставляется ядром. Остальные три метода реализованы в стандартной библиотеке С. Поскольку системный вызов ядра принимает четыре аргумента, он называется wait4().
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
Первый аргумент, pid, представляет собой процесс, код возврата которого должен быть возвращен. Он может принимать ряд специальных значений.
pid < -1 Ожидать завершения любого дочернего процесса, чей pgid равен абсолютному значению pid.
pid = -1 Ожидать прерывания любого дочернего процесса.
pid = 0 Ожидать завершения дочернего из той же группы процессов, что и текущий[20].
pid > 0 Ожидать выхода процесса pid.
Второй параметр — это указатель на целое, которое устанавливается в значение, равное соду возврата того процесса, который заставляет wait4() вернуть управление (мы будем зазывать его "проверяемым" процессом). Формат возвращенного состояния довольно закрученный, и для того, чтобы сделать его осмысленным, существует набор макросов.
Три события заставляют wait4() вернуть состояние проверяемого процесса. Процесс может завершиться, он может быть прерван вызовом kill() (получит фатальный сигнал) либо он может быть остановлен по какой-либо причине[21]. Вы можете узнать, что именно случилось, с помощью описанных ниже макросов, каждый из которых принимает возвращаемое состояние wait4() в качестве единственного параметра.
WIFEXITED(status) Возвращает true, если процесс завершился нормально. Процесс завершается нормально, когда его функция main() выходит из программы посредством вызова exit(). Если WIFEXITED истинно, то WEXITSTATUS(status) возвращает код возврата процесса. WIFSIGNALED(status) Возвращает true, если процесс был прерван сигналом (это происходит, когда он прерывается вызовом kill()). В этом случае WTERMSIG(status) возвращает номер сигнала, прервавшего процесс. WIFSTOPPED(status) Если процесс приостановлен сигналом, WIFSTOPPED() возвращает true, a WSTOPSIG(status) возвращает номер сигнала, приостановившего процесс. wait4() возвращает информацию только о приостановленных процессах, если указана опция WUNTRACED.
Аргумент options управляет поведением вызова. WHOHANG заставляет функцию немедленно вернуть управление. Если в данный момент нет ни одного процесса, готового сообщить свое состояние, то возвращается 0 вместо допустимого pid. WUNTRACED заставляет wait4() возвратить соответствующий остановленный дочерний процесс. Более подробно о приостановленных процессах рассказывается в главе 15. Оба флажка могут быть объединены вместе битовой операцией "или".
Финальный параметр wait4(), указатель на struct rusage, наполняется информацией об использовании ресурсов проверяемым процессом и всеми его потомками. Более подробная информация об этом давалась при обсуждении getrusage() и RUSAGE_BOTH ранее в главе. Если этот параметр равен NULL, информация о состоянии не возвращается.
Существуют три других интерфейса к wait4(), каждый из которых представляет подмножество его функциональности.
pid_t wait(int *status) Единственный параметр wait() — это указатель на место, куда следует поместить код возврата прерванного процесса. Эта функция всегда блокирует выполнение до тех пор, пока дочерний процесс не будет прерван. pid_t waitpid (pid_t pid, int *status, int options) Функция waitpid() подобна wait4(). Единственное отличие в том, что она не возвращает информации об использовании ресурсов прерванным процессом. pid_t wait3(int *status, int options, struct rusage *rusage) Эта функция также подобна wait4(), но не позволяет специфицировать дочерний процесс, который должен быть проверен.
10.4.3. Запуск новых программ
Хотя доступно целых шесть способов запустить одну программу из другой, все они делают почти одно и то же — заменяют текущую выполняющуюся программу другой программой. Обратите внимание на слово "заменяет" — все следы текущей выполняющейся программы при этом исчезают. Если вы хотите оставить исходную программу работающей, вы должны создать новый процесс вызовом fork(), а затем запустить новую программу из дочернего процесса.
Эти шесть функций лишь слегка отличаются по интерфейсу. Только одна из них — execve() — является системным вызовом Linux. Остальные реализованы в библиотеках пользовательского пространства и вызывают execve() для запуска новой программы. Ниже представлены прототипы семейства функций exec().
int execl(const char *path, const char *arg0, ...);
int execlp(const char *file, const char *arg0, ...);
int execle(const char *path, const char *arg0, ...);
int execv(const char *path, const char **argv);
int execvp(const char *file, const char **argv);
int execve(const char *file, const char **argv, const char **envp);
Как уже упоминалось, все эти программы пытаются заменить текущую программу новой. Если это удается, то управление не возвращается (то есть программа, которая вызвала другую программу, уже не выполняется). Если не удается, то возвращается значение -1 и устанавливается код ошибки в errno, как при любом другом системном вызове. Когда новая программа запускается, она принимает массив аргументов (argv) и массив переменных окружения (envp). Каждый элемент envp имеет форму ПЕРЕМЕННАЯ=значение[22].
Основная разница между функциями семейства exec() состоит в том, как новой программе передаются аргументы командной строки. Функции execl() передают каждый элемент в отдельном аргументе argv, причем список завершается NULL. Традиционно первый элемент argv — это команда, использованная для запуска программы. Например, команда оболочки /bin/cat /etc/passwd /etc/group обычно получается в результате следующей вызова exec:
execl("/bin/cat", "/bin/cat", "/etc/passwd", "/etc/group", NULL);
Первый аргумент — это полный путь к программе, которую требуется выполнить, а остальные аргументы передаются программе в виде argv. Заключительный параметр execl() должен быть равен NULL — это служит признаком конца списка параметров. Если вы пропустите NULL, то, скорее всего, функция завершится ошибкой сегментации либо вернет EINVAL. Окружение, переданное новой программе — это то, на что указывает глобальная переменная environ, как упоминалось ранее в настоящей главе.
Функциям execv аргументы командной строки передаются как массив С строк[23], имеющих тот же формат, что применяется для передачи argv новой программе.
Последним элементом должен быть NULL для обозначения конца массива, а первый элемент (argv[0]) должен содержать имя вызываемой программы.
Наш пример с bin/cat /etc/passwd /etc/group может быть закодирован, используя execv, следующим образом:
char *argv[] = { "/bin/cat", "/bin/cat", "/etc/passwd", "/etc/group", NULL }; execv("/bin/cat", argv);
Если нужно передать специфическое окружение новой программе, для этого подойдут execle() и execve(). Они в точности похожи на execl() и execv(), но принимают указатель на окружение в качестве последнего аргумента. Окружение устанавливается так же, как argv.
Например, ниже показан один способ запуска /usr/bin/env (эта программа печатает окружение, которое ей передано) с небольшим набором переменных окружения:
char *newenv[] = { "PATH=/bin:/usr/bin", "HOME=/home/sweethome", NULL };
execle("/usr/bin/env", "/usr/bin/env", NULL, newenv);
Вот та же идея, реализованная с помощью execve():
char *argv[] = { "/usr/bin/env", NULL };
char *newenv[] = { "PATH=/bin:/usr/bin", "HOME=/home/sweethome", NULL };
execve("/usr/bin/env", argv, newenv);
Последние две функции, execlp() и execvp(), отличаются от первых двух тем, что выполняют поиск программы, которую нужно запустить, в текущем пути (установленном переменной окружения PATH). Аргументы программы, однако, не модифицируются, поэтому argv[0] не содержит полного пути к запускаемой программе. Ниже показана модифицированная версия нашего первого примера, который ищет cat в текущем PATH.