Вдохновлен этот ответ из SO я пытаюсь найти версию fork()/exec()/wait(), которая:
- запускает дочерний процесс;
- ловит stdout, stderr и код возврата отдельно;
- должным образом обрабатывает все возможные ошибки от системных вызовов (например, нехватка памяти, невозможность открытия файлов, файл не найден) и гарантирует отсутствие утечек ресурсов, несмотря ни на что.
- не будет падать, даже если дочерний процесс рухнул, и может сообщить, что дочерний процесс рухнул;
- Потокобезопасный (
int exec()
только).
Мне не удалось найти такую реализацию в Интернете в качестве эталона, поэтому я пишу свою собственную. Но я не уверен, что все крайние случаи обрабатываются должным образом.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
int exec() {
int pipefd_out[2], pipefd_err[2];
FILE* fp_out;
FILE* fp_err;
char buff[256];
int status;
if (pipe(pipefd_out) == -1) {
// man 2 pipe
perror("pipe()");
return EXIT_FAILURE;
}
if (pipe(pipefd_err) == -1) {
// man 2 pipe
close(pipefd_out[0]);
close(pipefd_out[0]);
perror("pipe()");
return EXIT_FAILURE;
}
pid_t child_pid = fork(); //span a child process
if (child_pid == -1) { // fork() failed, no child process created
perror("fork(): ");
return EXIT_FAILURE;
}
if (child_pid == 0) { // fork() succeeded, we are in the child process
close(pipefd_out[0]);
close(pipefd_err[0]);
dup2(pipefd_out[1], STDOUT_FILENO);
dup2(pipefd_err[1], STDERR_FILENO);
char *const args[] = {"/bin/ls", "-l",
"/path/that/definitely/does/not/exist/", NULL};
execv(args[0], args);
perror("execl()/execv()");
// The exec() functions return only if an error has occurred.
// The return value is -1, and errno is set to indicate the error.
exit(EXIT_FAILURE);
// Have to exit() explicitly in case of execl() failure.
}
//Only parent gets here
close(pipefd_out[1]);
close(pipefd_err[1]);
if ((fp_out = fdopen(pipefd_out[0], "r")) == NULL) {
perror("fdopen()");
return EXIT_FAILURE;
}
if ((fp_err = fdopen(pipefd_err[0], "r")) == NULL) {
perror("fdopen()");
fclose(fp_out);
return EXIT_FAILURE;
}
printf("===== stdout =====\n");
while(fgets(buff, sizeof(buff) - 1, fp_out)) {
printf("%s", buff);
}
printf("===== stdout =====\n");
printf("===== stderr =====\n");
while(fgets(buff, sizeof(buff) - 1, fp_err)) {
printf("%s", buff);
}
printf("===== stderr =====\n");
fclose(fp_out);
fclose(fp_err);
// wait for the child process to terminate
if (waitpid(child_pid, &status, 0) == -1) {
perror("waitpid()");
return EXIT_FAILURE;
}
if (WIFEXITED(status)) {
printf("Child process exited normally, rc: %d\n", WEXITSTATUS(status));
} else {
printf("Child process exited unexpectedly ");
if (WIFSIGNALED(status)) {
printf("(terminated by a signal: %d)\n", WTERMSIG(status));
} else if (WIFSTOPPED(status)) {
printf("(stopped by delivery of a signal: %d)\n", WSTOPSIG(status));
} else {
printf("(unknown status: %d)\n", status);
}
}
}
int main(int argc, char** argv) {
if (argc != 2) {
printf("Usage: %s <0|1|2|3>\n", argv[0]);
return EXIT_FAILURE;
}
exec(argv);
}
Полную версию можно нашел здесьна тот случай, если вы захотите скомпилировать его, чтобы увидеть, что именно может произойти.
DJ Элкинд
4 ответа
Код рискует вызвать неопределенное поведение:
if (argc != 2) {
printf("Usage: %s <0|1|2|3>\n", argv[0]);
return EXIT_FAILURE;
}
Разыменование argv[0]
вызовет неопределенное поведение, если оно NULL
.
Добавьте к нему чек.
/* Sanity check. POSIX requires the invoking process to pass a
* non-NULL argv[0].
*/
if (!argv[0]) {
fputs ("A NULL argv[0] was passed in through an exec() system call.\n", stderr);
return EXIT_FAILURE;
}
Не используйте магические числа:
char buff[256];
Здесь магическое число 256 должно быть именованной константой.
#define BUFSIZE 256
Это улучшает читаемость кода и упрощает его сопровождение; требуется только одно изменение именованной константы вместо каждого экземпляра числа. См. этот вопрос SO: Что такое магические числа и почему они плохие?
Указатели на строковые литералы должны быть объявлены с атрибутом const
квалификатор:
// char *const args[] = {"/bin/ls", "-l",
"/path/that/definitely/does/not/exist/", NULL};
const char *const args[] = {"/bin/ls", "-l", "/path/that/definitely/does/not/exist/", NULL};
С использованием const
помогает сохранить целостность строки, потому что вы можете изменить ее только с помощью нового указателя на строку или передав его функции, которая изменяет строку.
Дочерние процессы должны использовать _exit()
вместо exit()
:
Основное различие между
exit()
и_exit()
заключается в том, что первый выполняет очистку, связанную с конструкциями пользовательского режима в библиотеке, и вызывает предоставленные пользователем функции очистки, тогда как второй выполняет только очистку ядра для процесса.В дочерней ветке a
fork()
обычно неправильно использоватьexit()
потому что это может привести к двойной очистке буферов stdio и неожиданному удалению временных файлов.
Код не соответствует всем требованиям:
Возвращаемые значения dup2()
, close()
и fclose()
игнорируются.
Из справочной страницы для close()
::
Работа с ошибками, возвращаемыми из
close()
:Внимательный программист проверит возвращаемое значение
close()
так как вполне возможно, что ошибки на предыдущемwrite(2)
операции сообщаются только в окончательномclose()
который выпускает описание открытого файла. Отсутствие проверки возвращаемого значения при закрытии файла может привести к скрытой потере данных. Это особенно заметно при использовании NFS и дисковой квоты.
Из справочной страницы для fclose()
:
fclose()
функция также может дать сбой и установить errno для любой из ошибок, указанных для подпрограмм.close(2)
,write(2)
илиfflush(3)
.
Вызов close()
на закрытом дескрипторе может привести к катастрофическим результатам:
В exec()
у нас есть следующие повторяющиеся вызовы close()
с тем же файловым дескриптором:
close(pipefd_out[0]);
close(pipefd_out[0]);
Со страницы руководства:
Повторная попытка
close()
после неудачного возврата — это неправильно, так как это может привести к закрытию повторно используемого файлового дескриптора из другого потока. Это может произойти из-за того, что ядро Linux всегда освобождает файловый дескриптор в начале операции закрытия, освобождая его для повторного использования; шаги, которые могут вернуть ошибку, такие как сброс данных в файловую систему или устройство, происходят только позже в операции закрытия.
Вышесказанное справедливо для успешного close()
операция.
Сохранять errno
если это требуется для последующего использования:
if (pipe(pipefd_err) == -1) {
// man 2 pipe
close(pipefd_out[0]);
close(pipefd_out[0]);
perror("pipe()");
}
Звонки в close()
может перезаписать значение errno
. Если значение требуется для последующего использования, рекомендуется сохранить его как можно скорее перед вызовами любых других функций.
if (pipe(pipefd_err) == -1) {
// man 2 pipe
int saved_errno = errno;
close(pipefd_out[0]);
close(pipefd_out[0]);
errno = saved_errno;
perror("pipe()");
}
А может просто позвонить perror()
прежде чем звонить close()
:
if (pipe(pipefd_err) == -1) {
// man 2 pipe
perror("pipe()");
close(pipefd_out[0]);
close(pipefd_out[0]);
}
Нам нужно включить больше предупреждений компилятора — эти проблемы решаются тривиально:
gcc-12 -std=c17 -fPIC -gdwarf-4 -g -Wall -Wextra -Wwrite-strings -Wno-parentheses -Wpedantic -Warray-bounds -Wmissing-braces -Wconversion -Wstrict-prototypes -fanalyzer 283851.c -o 283851
283851.c:10:5: warning: function declaration isn’t a prototype [-Wstrict-prototypes]
10 | int exec() {
| ^~~~
283851.c: In function ‘exec’:
283851.c:44:31: warning: initialization discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
44 | char *const args[] = {"/bin/ls", "-l",
| ^~~~~~~~~
283851.c:44:42: warning: initialization discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
44 | char *const args[] = {"/bin/ls", "-l",
| ^~~~
283851.c:45:13: warning: initialization discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
45 | "/path/that/definitely/does/not/exist/", NULL};
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
283851.c:101:1: warning: control reaches end of non-void function [-Wreturn-type]
101 | }
| ^
Не хватает очистки нашего файлового дескриптора. Здесь мы дважды закрываем конец для чтения, но оставляем конец для записи открытым:
if (pipe(pipefd_err) == -1) { // man 2 pipe close(pipefd_out[0]); close(pipefd_out[0]); perror("pipe()"); return EXIT_FAILURE; }
И здесь мы оставляем все четыре дескриптора канала открытыми:
if (child_pid == -1) { // fork() failed, no child process created perror("fork(): "); return EXIT_FAILURE; }
Там намного больше не хватает close()
вызовы (даже в пути успеха).
Этот тип обработки ошибок часто лучше всего обрабатывается с помощью goto
заявление. Настройте последовательность очистки, которая является отражением инициализации:
err_out_file:
fclose(fp_out);
err_err_fds:
close(pipefd_err[0]);
close(pipefd_err[1]);
err_out_fds:
close(pipefd_out[0]);
close(pipefd_out[1]);
err_initial:
return EXIT_FAILURE;
Это гарантирует, что ошибки выхода на каждом этапе очистят все ресурсы от предыдущих этапов:
if (pipe(pipefd_out) == -1) {
perror("pipe()");
goto err_initial;
}
if (pipe(pipefd_err) == -1) {
perror("pipe()");
goto err_out_fds;
}
pid_t child_pid = fork(); //span a child process
if (child_pid == -1) { // fork() failed, no child process created
perror("fork()");
goto err_err_fds;
}
if ((fp_out = fdopen(pipefd_out[0], "r")) == NULL) {
perror("fdopen()");
goto err_err_fds;
}
if ((fp_err = fdopen(pipefd_err[0], "r")) == NULL) {
perror("fdopen()");
goto err_out_file;
}
Чтение всего выходного потока перед чтением всего потока ошибок отлично подходит для этого простого ls
но может вызвать взаимоблокировку, когда дочерний элемент записывает в поток ошибок больше, чем емкость канала, прежде чем он закроет выходной поток, здесь:
printf("===== stdout =====\n"); while(fgets(buff, sizeof(buff) - 1, fp_out)) { printf("%s", buff); } printf("===== stdout =====\n"); printf("===== stderr =====\n"); while(fgets(buff, sizeof(buff) - 1, fp_err)) { printf("%s", buff); } printf("===== stderr =====\n");
Нам нужно использовать select()
для правильного чередования чтений обоих потоков.
main()
функция вводит в заблуждение. Он настаивает на том, чтобы пользователь предоставил аргумент, но он никогда не используется, так как exec
игнорирует его необязательные аргументы (запись int exec(void)
заметил бы это). Кроме того, мы игнорируем возвращаемое значение из exec()
хотя это точно из чего мы должны возвращаться main()
.
Что касается стиля, мне крайне не нравятся пробелы в конце многих строк.
Кроме того, в современном C нет необходимости объявлять все переменные в начале их области видимости. Предпочитайте объявлять переменные там, где они могут быть инициализированы значениями, чтобы исключить возможность их использования до того, как они будут назначены.
В целом, это выглядит как хорошее начало этой функции, но оно выглядит поспешным и немного неаккуратным. Нам нужно исправить освобождение ресурсов и устранить возможную взаимоблокировку при чтении двух потоков.
Использовать posix_spawn
Или, возможно, posix_spawnp
. Оно в стандарт POSIX именно для того, чтобы позаботиться об этом. Здесь не нужно изобретать велосипед.
Вы передаете этот системный вызов объекту атрибутов спавна. Например, чтобы переназначить произвольные файловые дескрипторы, такие как ваши каналы, на стандартный ввод и вывод дочернего процесса, вы должны использовать posix_spawn_file_actions_adddup2
дважды, чтобы инициализировать объект файловых действий, который вы передаете posix_spawn
. Или для перенаправления файлов в качестве стандартного ввода или стандартного вывода вы должны использовать последовательность вызовов, включая posix_spawn_file_actions_addopen
.
Запрос API POSIX
Вы, по-видимому, используете библиотеку, которая не объявляет exec
для вас и написание вашей собственной декларации до ANSI.
Вместо этого добавьте в начало файла перед любыми заголовками библиотек:
#define _POSIX_C_SOURCE 200809L
#define _XOPEN_SOURCE 700
Или добавьте соответствующий -D
параметры вашего make-файла.
Необходимые функции, в том числе exec()
теперь будет объявлено в <unistd.h>
. В Linux вы также должны иметь возможность использовать переносимый API в man -s 3p exec
и т. д.
Дэвислор
Необязательный -1
Не нужно вычитать 1.
fgets
Функция считывает не более чем на один символ меньше, чем число символов, указанное параметром n, из потока, на который указывает параметр stream, в массив, на который указывает параметр s.
// while(fgets(buff, sizeof(buff) - 1, fp_err)) {
while(fgets(buff, sizeof buff, fp_err)) {
Чукс — Восстановить Монику