LINUX.ORG.RU

Как кушать вилкой. ... или о fork() в двух словах

 


20

8

Статья о создании процессов в Linux

Как люди решают задачи

Обычно у каждой задачи есть одно простое решение, которое воспринимается всеми как правильное. Люди воспринимают такое решение правильным либо исходя из личного опыта¹; исходя из опыта других людей² или просто не задумываясь о правильности³. И самое удивительное, что мир не взорвался, никто (массово) от этого не умер, код работает и приносит деньги.

  1. «всегда так пишу код, никто не умер»
  2. «копирую код из stack overflow который набрал больше всех плюсов»
  3. «копирую первый попавшийся код из stack overflow»

Однако самый простой - не всегда самый правильный. Да, можно скопировать самый заплюсованный ответ и сказать, что миллионы не могут ошибаться, но ведь миллионы людей делают прививки и миллионы людей не делают прививки и одна из групп, исходя из простейшей логики, совершила ошибку.

Однако мы отвлеклись. Поставим перед собой задачу:

Нам необходимо наиболее правильным способом запустить из своего кода другую программу.

Не так важно, зачем. Это может быть запуск игры из лаунчера, запуск утилиты ping чтобы не реализовывать отправку ICMP-пакетов самостоятельно, запуск программы по клику на ярлык, миллион вариантов, думаю, что вы сами хотя бы раз в жизни сталкивались с такой задачей. Кстати, познакомьтесь, на КДПВ Картошка. Она будет нам помогать учиться пользоваться вилкой

Содержание статьи:

  1. Как кушать пингвина вилкой?
    • Общие знания о запуске процессов под LINUX-системами
  2. Как кушать корову если есть вилка?
    • Copy-on-write, что это и зачем? vfork и почему он не лучше
  3. Как кушать икру?
    • posix_spawn и почему он не замещает fork()
  4. Как кушают клоны?
    • clone() под капотом у fork()
  5. Почему когда ешь суп вилкой, он утекает?
    • Утечка дескрипторов после fork() и как этого избежать
  6. Почему у вилки три зуба?
    • Важность обработки всех вариантов возврата fork()
  7. Как кушать демонов вилкой?
    • Запуск демонизирующихся процессов при помощи fork()
  8. Как наложить вилкой в другую тарелку?
    • Переназначение дескрипторов вывода для нового процесса
  9. Как сигналить вилке?
    • Взаимоотношения обработки сигналов и fork()
  10. Как пользоваться вилкой когда сломалась ручка?
    • Самоликвидация дочернего процесса после завершения материнского
  11. Как подготовиться к использованию вилки?
    • Сценарии использования pthread_atfork()
  12. Как поцарапать окно вилкой?
    • Запуск дочернего процесса под Windows-системой
  13. Как систематически пользоваться вилкой?
    • Почему вам не стоит пользоваться system()
  14. Заключение.
    • Благодарности и выводы

Самое простое решение

Войдем в hivemind и зададим вопрос «как запустить программу из своей программы?». И, о чудо, мы сразу же видим ответ (с наибольшим количеством положительных оценок):

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> /* for fork */
#include <sys/types.h> /* for pid_t */
#include <sys/wait.h> /* for wait */

int startup() {
    /*Spawn a child to run the program.*/
    pid_t pid=fork();
    if (pid==0) { /* child process */
        static char *argv[]={"echo","Foo is my name.",NULL};
        execv("/bin/echo",argv);
        exit(127); /* only if execv fails */
    } else { /* pid!=0; parent process */
        waitpid(pid,0,0); /* wait for child to exit */
    }
    return 0;
}

Ну, или если вы в отличие от меня используете самую распространенную пользовательскую операционную систему, то так (нет, ну вы, конечно, можете использовать и первый вариант, но только под специфичным окружением (вроде cygwin или WSL) и «под капотом» всё равно будет вот такой код):

#include <Windows.h>

void startup(LPCSTR lpApplicationName) {
    // additional information
    STARTUPINFOA si;
    PROCESS_INFORMATION pi;
    // set the size of the structures
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    ZeroMemory(&pi, sizeof(pi));
    // start the program up
    CreateProcessA (
        lpApplicationName, // the path
        argv[1], // Command line
        NULL, // Process handle not inheritable
        NULL, // Thread handle not inheritable
        FALSE, // Set handle inheritance to FALSE
        CREATE_NEW_CONSOLE, // Opens file in a separate console
        NULL, // Use parent's environment block
        NULL, // Use parent's starting directory
        &si, // Pointer to STARTUPINFO structure
        &pi // Pointer to PROCESS_INFORMATION structure
    );
    // Close process and thread handles.
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
}

Выглядит просто, оба варианта являются ответами с наибольшим количеством плюсов.

Вариантом для людей которые идут против течения (или выбирают ответ наугад) будет

int result = system("C:\\Program Files\\Program.exe");

…что верно и допустимо для обоих систем¹.
¹ Опустим вопрос направления слешей.

Но что, если я скажу вам, что все эти решения не являются абсолютно верными?

Как кушать пингвина вилкой?

Для начала разберем решение для Linux систем. Если целиком убрать все синтаксические костыли, то оно сводится к следующему:

  1. Создать копию текущего процесса (fork).
  2. Заместить копию новой программой (exec).

«Создать копию текущего процесса» звучит максимально странно, как если бы чтобы нарисовать вторую половинку вилки мы бы отзеркалили первую, а потом стерли её и на её месте нарисовали ручку.

       *  The child process is created  with  a  single  thread—the  one  that
          called  fork().   The  entire virtual address space of the parent is
          replicated in the child, including the states of mutexes,  condition
          variables,  and other pthreads objects; the use of pthread_atfork(3)
          may be helpful for dealing with problems that this can cause.

(man 2 fork)

Давайте тогда разберемся, почему мы копируем текущий процесс вместо того чтобы просто сказать ядру «эй ты, запусти еще один процесс и дай мне его ID», ведь так было бы намного «дешевле», чем копировать огромные объемы памяти текущего процесса для того, чтобы запустить что-то маленькое. Ответ на самом деле очень прост:

Исторически (до fork()) для запуска новой программы оболочка открывала необходимые дескрипторы для ввода\вывода, загружала подпрограмму-загрузчик и запускала программу, которая замещала процесс оболочки. После получения команды exit() нужно было восстановить предыдущее состояние и вернуться к шагу, с которого мы начали.

Когда понадобилось запускать программы в новых процессах, придумали fork(), который позволял повторно использовать уже имевшийся код замещения процесса (то, что позже стало exec()). Это было проще и обошлось всего в 27 строк ассемблерного кода. Поэтому fork() копирует (см. главу Как кушать корову если есть вилка?) память родительского процесса и не открывает новых файлов¹.

¹. А так как в UNIX «всё есть файл» то это также касается «файлов», которые используются процессом для стандартного ввода/вывода.

Почему тогда не создать системный вызов, который будет принимать на вход список переменных окружения и файлов, которые необходимо передать дочернему процессу, список сигналов, которые будет обрабатывать процесс, идентификатор процесса с битом subreaper, к которому будет привязан процесс, права доступа, флаги приоритетов, текущую директорию, еще десяток «ну точно нужных аргументов, которые никогда не будут NULL»?

Дело в том, что такой вызов уже создали.

  • Под Windows системами функция CreateProcess принимает 9 входных параметров, из которых 4 - это структуры с заполняемыми полями, 1 список строк и 1 аргумент, который представляет из себя битовую маску. И знаете, что? Этого оказалось недостаточно и чуть позже в статье я расскажу, почему.
  • В Linux (а точнее в UNIX) такой вызов тоже есть и мы его рассмотрим, но для пользователя сделали функцию максимально простой, чтобы другими функциями выставлять только необходимые значения, а не передавать около сотни параметров в надежде что через 10-20-30-40 лет никто не изобретет новую сущность, настройку безопасности которой кровь из носу нужно добавить в этот вызов.

Но, хотя это и звучит максимально просто, со временем даже в такой простой механизм пришлось добавить много дополнений и «сносок».

Как кушать корову если есть вилка?

CoW (или Copy-on-Write) - это первый механизм, который был добавлен к вызову fork(). Его суть такова, что до тех пор, пока данные не изменены (write) они не будут скопированы (copy). Если над данными проводятся только процедуры чтения, то чаще всего копия данных создаваться не будет.

Конечно, возможна ситуация, когда родительский процесс сам захочет изменить состояние памяти, которая была (не-)скопирована дочерним процессом. В таком случае дочерний процесс будет по-прежнему обращаться к странице памяти, которая была ему предоставлена, а новое значение (которое родительский процесс хочет записать) будет записано в новую страницу памяти благодаря сработавшему отказу страницы (см. первый вариант срабатывания в «легком» отказе):

Отказ происходит в следующих случаях:

  • страница присутствует в памяти, но включена в рабочее множество другого процесса (например, если несколько процессов взаимодействуют через разделяемую память).

Зачем нужно было добавлять такой механизм? Ответ прост до банальности - когда создавался fork() оперативная память измерялась в килобайтах, а скопировать несколько килобайт памяти - (достаточно) быстрый процесс, даже если вы в 1969-м. В современном мире приложения иногда оперируют сотнями гигабайт и, если бы мы продолжали копировать всю занимаемую память, это занимало бы несколько секунд, даже если бы мы использовали самое современное оборудование:

СтандартЧастота (MHz)Скорость передачи (GB/s)¹Сколько будем копировать 150 GB (с)
DDR4213317.08.8
240019.27.8
266621.37.0
300025.65.8
DDR57200²51.22.9

¹При условии что вы там запущены как единственное приложение, система ничего не копирует, а производитель заявил честную скорость.
²https://www.techtimes.com/articles/264440/20210822/samsung-ddr5-ram-7200-mhz.htm

Для решения этой проблемы была создана другая функция - vfork(). При вызове vfork вызывающая нить замораживается, покуда дочерний «процесс» не вызовет execve, _exit или не упадет, убитый защитой доступа памяти. Кроме того, для ускорения вызова память родителя не копируется, а используется как есть - вместе с кучей и стеком. В своё время это вызвало разлад в BSD сообществе, часть разработчиков считала vfork архитектурным провалом, часть - единственной возможностью адекватно пускать приложения без задержки в несколько секунд.

Но после введения CoW выигрыш от vfork стал настолько незначительным, что в современности эта функция практически не используется. Исключения - платформы, на которых CoW не реализован из-за технических ограничений (например MIPS CPU без MMU).

Например, Java 7 использовала vfork(), потому что при копировании адресных таблиц и виртуальной памяти при включенном флаге overcommit = 2 приложение могло свалиться с Out-of-Memory, поскольку в этом режиме размер виртуальной памяти не может превысить размер физической.

Сегодня Java использует posix_spawn(), про который мы еще поговорим.

Как кушать икру?

Чуть выше я упомянул функцию posix_spawn , давайте рассмотрим её немного подробнее.

ть выше я упомянул функцию posix_spawn , давайте рассмотрим её немного подробнее.
int posix_spawn(pid_t *pid, const char *path,
                const posix_spawn_file_actions_t *file_actions,
                const posix_spawnattr_t *attrp,
                char *const argv[], char *const envp[]);

int posix_spawnp(pid_t *pid, const char *file,
                const posix_spawn_file_actions_t *file_actions,
                const posix_spawnattr_t *attrp,
                char *const argv[], char *const envp[]);

Сама функция создавалась для того, чтобы системы без MMU могли запускать процессы привычным им способом. Прямой запуск fork() + exec() там обычно невозможен из-за уже упомянутых проблем с отсутствием CoW, поэтому, так как программы на C должны работать на всех доступных платформах, было необходимо решить эту проблему.

Первоначально glibc (самый популярный рантайм C) версии 2.4 реализовывал posix_spawn как обычный fork() + exec(), имея, таким образом, решение «для галочки». Затем, для того, чтобы не ломать совместимость со старыми версиями, был добавлен GNU-специфичный флаг POSIX_SPAWN_USEVFORK, который форсирует использование vfork внутри этой функции (есть еще несколько условий, но мы их рассматривать не будем).

После версии glibc 2.24 posix_spawn использует clone с флагом CLONE_VFORK + exec всегда, когда есть такая возможность. Также posix_spawn в новых версиях (в отличие от прямого вызова vfork) использует раздельный стек для дочернего процесса, во избежание повреждения родительского стека при манипуляциях с дочерним процессом. Дополнительно происходит блокировка сигналов, которую мы рассмотрим в главе «как сигналить вилке?».

Как кушают клоны?

Однако все эти функции, которые мы обсудили (fork, vfork и posix_spawn) являются всего лишь оберткой над одним системным вызовом, который называется clone .

int clone(
    int (*fn)(void *),
    void *stack,
    int flags,
    void *arg,
    ...
    // pid_t *parent_tid,
    // void * tls,
    // pid_t *child_tid
);

Это, конечно, не миллион аргументов как в CreateProcess, но тоже немало. Примитивный вызов clone будет выглядеть примерно вот так (обратите внимание, что здесь и далее автор умышленно оставляет обработку ошибок за скобками):

char * stack = mmap(
    NULL, // Стартовый адрес (не используется)
    STACK_SIZE, // Размер стека
    PROT_READ | PROT_WRITE, // Права на запись и чтение страниц
    MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK,
    // Приватная память, без отображения
    // на файл, аллоцировать память в области стека
    -1, // Не используется отображение на файл - нет идентификатора
    0 // Не инициализируем память, не нужен сдвиг инициализации
);

char * stacktop = stack + STACK_SIZE;
// Так как стек растет вниз (обычно),
// установим голову стека в конце выделенной памяти
int flags = CLONE_FS; // Копируем информацию о подлежащей файловой системе
clone(
    func,     // Точка входа в новый процесс
    stacktop, // Вершина стека
    flags,    // Флаги
    NULL      // Ничего не передаем дочернему процессу
);

Передача аргументов дочернему процессу работает через void * ptr. Примерно как в pthread_create. Да, собственно pthread_create именно так и работает:

#include <pthread.h>

void * _thread(void * ptr) {
    int * pint = ptr;
    *pint = 5;
    pthread_exit(NULL);
}

int main(void) {
    int val = 4;
    pthread_t thread;
    pthread_create(&thread, NULL, _thread, &val);
    pthread_join(thread, NULL);
    return val;
}
$ gcc pthread.c -o pthread.elf -lpthread && strace ./pthread.elf 2>&1 | grep clone
clone(child_stack=0x7ff8905a6fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_

Как итог можно сказать, что clone() , являясь самым общим вызовом, позволяет очень тонко настроить все аттрибуты создаваемого субпроцесса - общую память, дескрипторы и даже адресное пространство. Конечно, вам скорее всего, не будет нужно вызывать его самостоятельно, однако на ограниченных платформах, где, например, нет уже упомянутого CoW, даже vfork() может вам не подойти, являясь, по сути, преднастроенным clone().

Почему когда ешь суп вилкой, он утекает?

А теперь, наконец, поговорим о том, как правильно пользоваться fork. Всё это время мы рассуждали зачем он нужен, да как работает, и теперь пора узнать, что обычно идет не так если копировать код бездумно.

Представим следующую программу:

// prog1.c
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int f = open("/etc/passwd", O_RDONLY);
    switch (fork()) {
        case -1: _exit(EXIT_FAILURE);
        case 0 : {
            static char * argv[] = { "./prog2.elf", NULL };
            execv("./prog2.elf", argv);
            _exit(127);
        }
        default:
            close(f);
            wait(NULL);
    }
    return EXIT_SUCCESS;
}
// prog2.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int fd = 3;
    char buff[128] = { 0 };
    read(fd, buff, sizeof(buff) - 1);
    puts(buff);
    return EXIT_SUCCESS;
}
$ ./prog1.elf
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:

Произошло следующее:

  1. Родительский процесс открыл файл и получил его дескриптор. Так как дескрипторы раздаются по принципу «первый незанятый номер», то его значение 3. (Помним, что 0..2 заняты stdin/out/err)
  2. Дочерний процесс получил копию всех открытых дескрипторов родительского процесса.
  3. Родительский процесс закрыл дескриптор.
  4. Дочерний процесс обращается к (всё ещё открытому¹) дескриптору #3 и читает из него данные.

¹Всё еще открыт дескриптор из-за невыставленного флага CLONE_FILES, переданного «под капотом» в функцию clone.

В данный момент неважно, сработал ли механизм CoW или нет, нам в любом случае дадут в личное пользование все дескрипторы родительского процесса.

Важно помнить, что в UNIX-like системах принцип «всё есть файл» выполняется почти всегда, следовательно:

  • Файловый дескрипторы (что очевидно) - файлы.
  • Устройства (что менее очевидно, но допустим) - тоже файлы.
  • Информация о ядре (?) - файлы.
  • Информация о процессах (???) - файлы.
  • Настройки ядра (????) - файлы.
  • И даже сетевые сокеты (!!!) - тоже файлы.

Таким образом, что бы ни использовал родительский процесс и как бы ни пытался закрыть дескрипторы после вызова fork - дочерний процесс получит полный контроль над уже открытыми дескрипторами. А вы, ведь, не хотите, чтобы вызвав «зараженный» ping в торговом приложении, оно начало рассылать по сети враждебные команды или пытаться строить из себя MitM? Или перебрав список открытых дескрипторов прочло содержимое открытых файлов. Или не дало вам в следующий раз запустить сервер на выбранном порту?

Испугались? Это хорошо. А теперь научимся защищать дескрипторы. Даже если мы выставим флаг CLONE_FILES, максимум, чего мы можем добиться - избавиться от полного доступа и устроить гонку (если родитель не успел закрыть файл до того, как дочерний процесс его прочитал - доступ всё равно будет получен).

Для полного решения проблемы файлу (сокету/дескриптору) можно выставить флаг FD_CLOEXEC, который принудительно закроет данный файл в пространстве склонированного процесса при замещении процесса во время выполнения функции exec():

man fcntl
                                               If the FD_CLOEXEC bit is
       set, the file descriptor   will automatically be closed during a
       successful execve(2).          (If the execve(2) fails, the file
       descriptor is left open.)  If the FD_CLOEXEC bit is not set, the
       file descriptor will remain open across an execve(2).

Модифицируем prog1, добавив защиту:

int f = open("/etc/passwd", O_RDONLY);
fcntl(f, F_SETFD, FD_CLOEXEC);

Если у вас есть доступ к функциям open или socket (иными словами, если эти вызовы делаете вы, а не библиотека), то вы можете сразу выставлять этот флаг через передачу флагов O_CLOEXEC и SOCK_CLOEXEC соответственно).

Вот, собственно, спустя 10 минут чтения статьи, первый совет:

Всегда защищайте файловые дескрипторы флагом FD_CLOEXEC, если планируете вызывать fork+execv для программ, которые не должны их видеть.

Почему у вилки три зуба?

В самом начале статьи я привел следующий код:

pid_t pid=fork();

if (pid==0) { /* child process */
    static char *argv[]={"echo","Foo is my name.",NULL};
    execv("/bin/echo",argv);
    exit(127); /* only if execv fails */
} else { /* pid!=0; parent process */
    waitpid(pid,0,0); /* wait for child to exit */
}

И вот вам еще один нюанс: у этой «вилки» должно быть три зуба:

RETURN VALUE
       On success, the PID of the child process is returned in the parent, and
       0 is returned in the child. On failure, -1 is returned in the parent,
       no child process is created, and errno is set appropriately.

Таким образом любой код, который использует конструкцию «если ==0 … иначе» не просто неправилен с точки зрения мануала (например как если бы вы копировали пересекающиеся области памяти при помощи memcpy), но еще и может навредить всем окружающим его процессам.

Представим следующую ситуацию: по какой-либо причине (например нехватка дескрипторов или памяти) fork завершился неудачно, вернув вам -1. В случае, если вы передадите такой «дескриптор» в waitpid это будет означать «ждать любой дочерний процесс» (что уже поломает логику работы программы), но что еще хуже, если вы, вдруг, захотите досрочно завершить свой дочерний процесс при помощи kill, то…

    If pid equals -1, then sig is sent to every process for which the call‐
    ing process has permission to send signals, except for process 1
    (init), but see below.

…то вы пошлете KILL/TERM/QUIT/Какой-вы-там-хотели сигнал ВСЕМ процессам, которые запущены с теми же правами доступа. А под linux-системами будете как горец, один стоять и окровавленным мечом размахивать:

    POSIX.1 requires that kill(-1,sig) send sig to all processes that the
    calling process may send signals to, except possibly for some implemen‐
    tation-defined system processes. itself, Linux allows a process to signal
    but on Linux the call kill(-1,sig) does not signal the calling process.

Поэтому совет номер два:

всегда обрабатывайте возможный возврат ошибки fork.

(если вы, конечно, не пишете извращенный OOM-killer, который убивает все процессы в случае нехватки памяти):

pid_t p;
switch (p = fork()) {
    case -1 : // error, process it
        break;
    case 0: // child process
        break;
    default:
        // parent process
}

Как кушать демонов вилкой?

Ядро Linux 3.4 принесло много хороших изменений: частичную поддержку архитектуры Kepler от NVIDIA, огромное количество улучшений поддержки BTRFS, и, в числе всего прочего, флаг PR_SET_CHILD_SUBREAPER. О нём сейчас и поговорим.

Предположим, что вы вызвали fork, в котором запустили другое приложение (к примеру вы реализуете супервизор, ну или в принципе хотите 100% дождаться выполнения подлежащего приложения). Иногда бывает так, что это подлежащее приложение может вызывать свои субутилиты. А иногда бывает так, что приложение, почувствовав, что его выпустили на волю решает запустить себя в качестве демона.

Такой запуск приводит к следующему: когда завершается «оболочка» приложения (запустив при этом «демона») ваш вызов waitpid завершается. При этом сам «демонизированный» процесс продолжает работать. А знаете, что ещё происходит, когда завершается «оболочка»? «Демонизированный» процесс меняет своего родителя. На init (pid 1). Это происходит, потому что этот процесс становится «сиротой» и, так как такого быть не должно, его принудительно «удочеряет» init.

Как в таком случае дождаться завершения такого процесса? У нас нет ни его PID, ни каких-либо других улик, говорящим нам о том, что там что-то выполняется. Я встречался с банковской системой, которая при вызове утилиты для проведения оплаты через терминал вызывала такой процесс для обработки платежа, а вызываемое пользователем приложение спокойно завершала. Как же в таком случае понять, когда транзакция успешно завершилась?

Для этого и нужен флаг PR_SET_CHILD_SUBREAPER. Устанавливается он довольно просто:

if (prctl(PR_SET_CHILD_SUBREAPER, 1lu))
    _report_error(errno);

Обратите внимание на приведение типа. В мануале чётко сказано, что функция объявлена как

int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);

Однако на некоторых имплементациях это

int prctl(int option, ...);

Теперь, если какое-либо подлежащее приложение создаст демона, завершение которого вам нужно отследить, вы всегда можете, установив этот флаг и использовав wait()/waitpid(-1, …) определить, что он завершен.

Обратите внимание, что wait()/waitpid(-1, …) ожидает завершения любого дочернего процесса. Т.е. если вы оставите только один wait - демон всё еще будет работать, потому что wait сработает для его оболочки. Вам нужно вызвать wait несколько раз, ожидая, пока он не вернет -1 и не установит errno в ECHILD, что будет означать, что у процесса нет потомков, завершение которых можно ожидать.

Итак, совет #3:

Если вы хотите дождаться выполнения всех потомков, используйте PR_SET_CHILD_SUBREAPER.

Альтернативно вы можете использовать ptrace для отслеживания SYS_clone, но это замедлит выполнение приложения и даст ему возможность узнать, что его отслеживают (такое, например, не любят античит-системы и DRM).

Как наложить вилкой в другую тарелку?

Продолжая разговор о супервизорах. Достаточно часто перед программистом стоит задача переопределить вывод запускаемого приложения в файл. Поскольку, если этого не делать, то вывод двух приложений (родительского и дочернего) будет смешан. А если вывод осуществляется не построчно (сначала формируется вся строка лога, а затем отображается одним выводом write) а частями (например сначала дата\время, потом название функции, потом сообщение), то вывод будет перемешан даже в рамках одной строки. При этом stderr даже не буферизуется, поэтому там могут быть ещё более странные сюрпризы.

Поэтому, не оттягивая неизбежное, рассмотрим следующий код:

static bool _redirect_handler(const char * filename, int handle) {
    static const mode_t filemode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
    static const int fileflags = O_APPEND | O_CREAT | O_WRONLY;
    int fd = open(filename, fileflags, filemode);
    if (fd == -1) {
        fprintf(stderr, "Не удалось открыть файл \"%s\"\n", filename);
        return false;
    }
    if (dup2(fd, handle) < 0) {
        close(fd);
        fprintf(stderr, "Не удалось переопределить дескриптор \"%d\"\n", handle);
        return false;
    }
    close(fd);
    return true;
}

int main(void) {
    switch (fork()) {
        case -1: _exit(EXIT_FAILURE);
        case 0 : {
            if (!_redirect_handler("/tmp/out.log", STDOUT_FILENO) ||
                !_redirect_handler("/tmp/err.log", STDERR_FILENO)) {
                fprintf(stderr, "Не удалось переопределить вывод потомка\n");
                _exit(EXIT_FAILURE);
            }

            static char * argv[] = { "./prog2.elf", NULL };
            execv("./prog2.elf", argv);
            _exit(127);
        }
        default:
            wait(NULL);
    }
    return EXIT_SUCCESS;
}

Мы:

  1. Создали новый дескриптор, указывающий на файл (open).
  2. Переопределили дескриптор stdout файловым (dup2).
  3. Закрыли более не нужную копию (close).
  4. Повторили для stderr.

Если у вас появился вопрос, почему мы не делаем close(STDOUT_FILENO)+open, я отвечу, что мануал ответит лучше меня:

man dup3
       The  steps  of  closing  and reusing the file descriptor newfd are per-
       formed atomically.  This is  important,  because  trying  to  implement
       equivalent  functionality  using close(2) and dup() would be subject to
       race conditions, whereby newfd might be reused between the  two  steps.
       Such  reuse  could  happen because the main program is interrupted by a
       signal handler that allocates a file descriptor, or because a  parallel
       thread allocates a file descriptor.

Совет #4:

Используйте переопределение дескрипторов для запускаемых процессов, если хотите сохранить вывод основного приложения в порядке.

Как сигналить вилке?

Если ваше приложение использует signal/sigaction и fork одновременно - то я готов поспорить, что вы попались в ловушку.

Дело в том, что forked-процесс наследует все ваши обработчики сигналов. При этом создаются следующие проблемы:

  1. Сразу после вызова fork() к вам может прилететь сигнал, который может прилететь в оба процесса сразу, когда: сигнал был послан всей группе, а оба процесса будут в ней находиться или сигнал был послан всем находящимся в одной контрольной группе, а OOM или systemd послали сигнал всей группе. И если материнский процесс в таком случае адекватно отреагирует на сигнал (выставит флаги завершения, завершит работу), то дочерний процесс ничего этого не сделает, поскольку обработанные до замещения процесса сигналы не повлияют на работу замещенного процесса, который доступа к выставленным флагам уже не имеет
  2. В случае, если вы создаете какие-то побочные эффекты в обработчике сигнала они могут повлиять на выполнение дочернего процесса (например выведет что-то в лог, тем самым создав такой файл).

Распишем решение этой проблемы:

static __thread sigset_t g_sig_blocked;

/*! \brief Временно блокирует входящие сигналы для предотвращения их срабатывания
* в fork()-процессе до установки нормальных обработчиков
* \return Истина, если блокировка успешно завершилась */
bool _sigreset_block(void) {
    sigset_t setnew;
    sigfillset(&setnew);
    sigemptyset(&g_sig_blocked);
    return pthread_sigmask(SIG_SETMASK, &setnew, &g_sig_blocked) == 0;
}

/*! \brief Снимает блокировку сигналов, оставляя заблокированными сигналы, которые
* были заблокированы до парного вызова `_sigreset_block`
* \return Истина, если блокировка успешно снята */
bool _sigreset_unblock(void) {
    return pthread_sigmask(SIG_SETMASK, &g_sig_blocked, NULL) == 0;
}

/*! \brief Устанавливает обработчики всех сигналов на обработчики по-умолчанию.
* После вызова fork() в дочернем процессе все сигналы выставлены как у родителя,
* что не является желанным поведением для дочернего процесса.
* \return Истина, если обработчики выставлены на обработчики по-умолчанию */
bool _sigreset_default(void) {
    struct sigaction signal_act = {
        .sa_handler = SIG_DFL
    };

    if (sigfillset(&signal_act.sa_mask) < 0)
        return false;

    for (i32 i = 1; i <= SIGRTMAX; i++)
        if (sigismember(&signal_act.sa_mask, i) == 1)
            sigaction(i, &signal_act, NULL);

    return _sigreset_unblock();
}
/*! \brief Запускает процесс
* \param[in] program Имя программы
* \param[in] pargs Аргументы программы */
static int _execute_process( const char * program, char * pargs[_ARGS_COUNT]) {
    if (!_sigreset_block()) {
        _err("Не удалось подготовить блокировку обработки сигналов");
        return -1;
    }

    int process = fork();
    switch (process) {
        case -1 :
            _err("Ошибка запуска fork-процесса");
            break;
        case 0 :
            /* Пока материнский процесс не получает информацию о статусе выполнения
            * мы можем только принудительно завершить (_Exit) подлежащий процесс */
            if (!_sigreset_default()) {
                _err("Не удалось установить обработчики сигналов по-умолчанию");
                _Exit(EXIT_FAILURE);
            }
            /* Обратите внимание, что мы не используем третью ветку,
            поскольку мы возвращает pid */
            execve(program, pargs, environ);
            _err("Ошибка запуска программы \"%s\"", program);
            _Exit(EXIT_FAILURE);
    }

    if (!_sigreset_unblock()) {
        _err("Не удалось разблокировать обработчики сигналов");
        /* Мы не просто не смогли сделать действие, но и обрушили
        * workflow материнского процесса. Теперь единcтвенный способ
        * выхода из приложения - exit, так как обработчики сигналов стерты */
        _Exit(EXIT_FAILURE);
    }
    return process;
}
  1. Мы заблокировали приход всех сигналов в главный процесс.
  2. Вызвали fork() и установили обработчики по умолчанию для дочернего процесса.
  3. Вернули сохраненные обработчики для материнского процесса.

В данном примере мы опустили защиту от одновременного доступа к g_sig_blocked, а ведь если одновременно стартуют два процесса - то произойдёт состояние гонки. Вам необходимо самостоятельно добавить защиту от этого.

Обратите внимание, что сигналы, которые успели прийти в приложение выстроятся в очередь пока вы не включите обработчики. При этом обычные сигналы придут в единственном экземпляре (для них не существует очереди), в отличие от сигналов реального времени, которые сохранят исходный порядок. Помните о технической возможности прихода другого сигнала, когда вы обрабатываете предыдущий. Это может привести к повторному входу в функцию (если вы используете один обработчик для разных сигналов), что может заблокировать например mutex доступа второй раз в рамках одной нити. Поэтому повторюсь, лучший вариант обработчика сигналов - это выставление одной переменной.

Совет #5:

Блокируйте приход сигналов перед вызовом fork() и выставляйте обработчики по умолчанию для дочернего процесса, иначе вы рискуете нарушить ход работы программы.

Как пользоваться вилкой когда сломалась ручка?

Чаще всего существование дочернего процесса не имеет смысла, когда родительский процесс совершил судоку.

Но как дочерний процесс должен понять, что наступила его очередь совершать хачапури?

Постоянно отслеживать parent pid?

А что, если родительский процесс завершился до того, как мы в первый раз запомнили его? Хорошо, если это pid 1, это мы проверить сможем, а вот если кто-то установил SUBREAPER-флаг на несколько уровней выше? Да и, к тому же, чаще всего дочерний процесс это не наша программа и мониторинг ppid мы добавить не можем.

Но не бойтесь, самураи. Время фудзиямы мы не пропустим благодаря функции prctl:


       PR_SET_PDEATHSIG (since Linux 2.1.57)
              Set the parent-death signal of the calling process to arg2
              (either a signal value in the range 1..NSIG-1, or 0 to
              clear).  This is the signal that the calling process will
              get when its parent dies.

              Warning: the "parent" in this case is considered to be the
              thread that created this process.  In other words, the
              signal will be sent when that thread terminates (via, for
              example, pthread_exit(3)), rather than after all of the
              threads in the parent process terminate.

Обратите особенное внимание на последний абзац - посылка сигнала происходит не после завершения родительского процесса, а после завершения родительской (той, которая вызвала fork()) нити.

Перед вызовом дочернего процесса добавим следующий вызов:

if (prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0) == -1) {
    _err("Не удалось установить посыл сигнала после смерти родителя");
    _Exit(EXIT_FAILURE);
}

Кроме того, нужно учесть возможность «гонки», когда родитель умер до того, как fork()-нутый процесс выставил PR_SET_DEATHSIG. Для предотвращения этого состояния необходимо запомнить pid родителя до fork(), после чего проверить его в дочернем процессе после выставления флага. И если он не совпадает - значит, вы выиграли в лотерею новых родителей.

В данном случае мы используем SIGKILL, но вы в праве установить посыл любого другого сигнала, если не хотите быстрой смерти процесса. Например, SIGTERM. Правда, в таком случае завершение процесса не гарантируется, так как обработчик сигналов дочернего приложения может игнорировать этот сигнал. Так что выбирайте - либо довериться процессу и дать ему сохранить данные, но слать SIGTERM, либо не доверять процессу и слать SIGKILL с шансом повредить данные при записи.

На самом деле это сложный вопрос, поскольку всё очень сильно зависит от того, что может случиться, если дочерний процесс будет неожиданно завершен. Не сохранятся настройки будильника? Не сохранится конфигурационный файл transmission? Вал станка будет продолжать раскручиваться до максимальных оборотов?

Совет #6

Устанавливайте посыл сигнала дочернему процессу, если не хотите, чтобы он работал после смерти родителя.

Как подготовиться к использованию вилки?

Предположим, что вы используете mutex для защиты доступа к какому-нибудь ресурсу в памяти (например, массиву). В таком случае после вызова функции fork() в дочернем процессе вы получите копию массива и mutex в том состоянии, в котором он был до дубликации памяти. Следовательно, это состояние уже некорректно (так как мы его не знаем) и все подобные мьютексы должны быть заново инициализированы. Но вписывать после каждого fork() список мьютексов со всего приложения было бы неразумно, особенно учитывая, что чаще всего они являются private-свойствами модулей. В таком случае нам поможет функция pthread_atfork:

int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void)

Она позволяет установить три обработчика для данной нити, каждый из которых будет вызван во время соответствующего ему события:

  1. prepare - Во время вызова fork(), до создания нового процесса, со стороны родителя.
  2. parent - Во время вызова fork(), после создания и инициализации нового процесса, со стороны родителя.
  3. child - Во время вызова fork(), после создания и инициализации нового процесса, со стороны дочернего процесса.

По факту, это очень специфичная функция, которая была призвана решить проблему того, что дочерний процесс не может (согласно стандарту POSIX.1) вызывать не async-signal-safe функции до вызова exec, в то время как pthread_mutex_lock и pthread_mutex_unlock как раз-таки являются не async-signal-safe функциями.

С этой функцией может быть много проблем, так как вы не имеете возможности проверить, был ли этот mutex инициализирован или освобожден, доступен ли сам ресурс и так далее. Да, вы можете добавлять функции в LIFO-порядке последовательными вызовами pthread_atfork, но не имеете возможности их оттуда удалить. Моя рекомендация - не использовать в дочернем процессе эти ресурсы, чтобы в корне избежать проблем с синхронизацией. Однако ваша архитектура приложения может этого не позволить и тогда pthread_atfork будет вашей единственной возможностью адекватно переинициализировать блокировки.

Как поцарапать окно вилкой?

А что же Windows-системы? Да почти всё то же самое, за исключением сигналов (поскольку под Windows это сигналы Шрёдингера, они вроде есть, но их вроде нет):

/*! \brief Запускает процесс платформо-зависимым способом
* \param[in] program Имя программы
* \param[in] creationflags Настройки процессов
* \param[in] pargs Аргументы программы
* \return Информация о процессе */
PROCESS_INFORMATION _execute_process( const char * program, DWORD creationflags, char * pargs[_ARGS_COUNT]) {

    /* https://www.linux.org.ru/forum/development/7216318
     * Что делать, если на мингвине очень хочется форка?
     *                            knkd(04.01.12 02:10:43)
     * заплакать
     *                   alex_custov(04.01.12 02:16:55) */

    LPWSTR wprog = _stringa2w(program); // Просто переводит UTF-8 в WIDECHAR
    LPWSTR wargs = _argsa2w(pargs);
    SECURITY_ATTRIBUTES sa;
    sa.nLength = sizeof(sa);
    sa.lpSecurityDescriptor = NULL;
    sa.bInheritHandle = TRUE;
    PROCESS_INFORMATION pi = { 0 };
    STARTUPINFO si = { 0 };
    si.cb = sizeof(STARTUPINFO);
    if (!CreateProcessW(
        wprog,
        wargs,
        NULL,
        NULL,
        TRUE,
        creationflags,
        NULL,
        NULL,
        &si,
        &pi
    )) {
        _err("Ошибка запуска fork-процесса");
        memset(&process, 0, sizeof(PROCESS_INFORMATION));
    }

    free(wprog);
    free(wargs);
    return pi;
}

А теперь начнем наращивать «мясо»:

Переопределяем вывод процесса в отдельные файлы (глава «Как наложить вилкой в другую тарелку?»):

/*! \brief Переопределяет конкретный идентификатор файлом
* \param[in] filename Имя файла или NULL
* \param[in] handle Идентификатор вывода (stdout/stderr)
* \return Истина, если идентификатор переопределен */
static bool _redirect_handler(const char * filename, HANDLE * handle) {
    static const DWORD fileaccess = FILE_APPEND_DATA;
    static const DWORD fileshare = FILE_SHARE_WRITE | FILE_SHARE_READ;
    static const DWORD filedisp = OPEN_ALWAYS;
    static const DWORD fileattr = FILE_ATTRIBUTE_NORMAL;
    LPWSTR wfile = _stringa2w(filename);
    HANDLE hfile = CreateFile(
        wfile,
        fileaccess,
        fileshare,
        NULL,
        filedisp,
        fileattr,
        NULL
    );
    free(wfile);
    if (hfile == INVALID_HANDLE_VALUE) {
        _err("Не удалось открыть файл \"%s\"", filename);
        return false;
    }
    *handle = hfile;
    return true;
}
// До вызова CreateProcess:
if (!_redirect_handler(filename_out, &si->hStdOutput) ||
    !_redirect_handler(filename_err, &si->hStdError )) {
    _err("Не удалось переопределить вывод");
}

Просим процесс завершиться вместе с родителем (глава «Как пользоваться вилкой когда сломалась ручка?»):

/*! \brief Экземпляр JOB для установки дочерним процессам */
static HANDLE gJob = 0;
/*! \brief Создание экземпляра JOB */
static void _job_create(void) {
    JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli = { 0 };
    gJob = CreateJobObject(NULL, NULL);
    jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
    SetInformationJobObject(
        gJob,
        JobObjectExtendedLimitInformation,
        &jeli,
        sizeof(jeli)
    );
}

// До вызова CreateProcess:
if (diewithparent) {
    BOOL bIsProcessInJob;
    if (IsProcessInJob(GetCurrentProcess(), NULL, &bIsProcessInJob) == 0)
        return false;
    creationflags = bIsProcessInJob ? CREATE_BREAKAWAY_FROM_JOB : 0;
    static pthread_once_t ponce;
    pthread_once(&ponce, _job_create);
}

// После вызова CreateProcess:
if (diewithparent) {
    if (AssignProcessToJobObject(gJob, pi->hProcess) == 0)
    // Всё наоборот в отличие от Linux
    return ...; // Ошибка
}

Как систематически пользоваться вилкой?

Нам осталось рассмотреть только самый последний вариант, который также упоминался в самом начале - вызов system(). На первый взгляд можно подумать, что это идеальная для повседневного использования функция, если нам нужно в блокирующем режиме запустить программу и дождаться её выполнения. Да даже и не в блокирующем:

static void * _thread_func(void * data) {
    g_result = system(program);
    g_finished = true;
    pthread_exit(NULL);
    return NULL;
}
...
g_finished = false;
pthread_create(&thread, NULL, _thread_func, NULL);
while (!g_finished) {
    // Делать действия
    sleep(1);
}

Для лабораторной работы - пойдёт. Для чего-то большего - точно нет. Попробуем перечислить все недостатки system:

Блокирующий режим

Тут всё просто - system на самом деле - это связка fork() + exec() + wait(), мы даже можем написать свой примитивный system, который будет почти соответствовать системному:

int _system(const char * command) {
    pid_t child = fork();
    switch (child) {
        case -1:
            return -1;
        case 0 :
            execl("/bin/sh", "sh", "-c", command, (char *) NULL);
            _exit(127);
        default:
            int status = 0;
            while (waitpid(child, &status, 0) == -1) {
                if (errno != EINTR) {
                    status = -1;
                    break;
                }
            }
            return status;
    }
}

Блокирование главной нити программы для выполнения какой-то операции очень плохая практика. Значит нам нужно создавать отдельную нить для выполнения там system(), контролировать возврат из нити, пробрасывать результат, делать busy wait, так как мы не знаем pid. Не проще ли использовать fork() самостоятельно?

Отсутствие Thread safety

Если вы запустите man system на большинстве современных Linux-систем, вы увидите следующую картину:

       ┌──────────┬───────────────┬─────────┐
       │Interface │ Attribute     │ Value   │
       ├──────────┼───────────────┼─────────┤
       │system()  │ Thread safety │ MT-Safe │
       └──────────┴───────────────┴─────────┘

Если что - это наглая ложь, поскольку имплементация system использует функцию sigaction, которая меняет обработчики сигналов для ВСЕХ нитей сразу. Причины этого поведения мы объясним чуть позже, а пока заметим, что в других системах функция описана по-другому:

STANDARDS
    The system() function conforms to ISO/IEC 9899:1990 ("ISO C90") and is
    expected to be IEEE Std 1003.2 ("POSIX.2") compatible.

Заявлена как соответствующая POSIX, который вообще никак не утверждает её потокобезопасность.

+-----------------------------+-----------------------------+
| ATTRIBUTE TYPE              | ATTRIBUTE VALUE             |
+-----------------------------+-----------------------------+
| Interface Stability         | Standard                    |
+-----------------------------+-----------------------------+
| MT-Level                    | Unsafe                      |
+-----------------------------+-----------------------------+

В чем заключается потоконебезопасность? Во всех имплементациях, что мне удалось найти - для одновременных вызовов system защита присутствует. Но вот если одновременно с этим пользователь будет сам манипулировать обработчиками SIGINT, SIGCHLD или SIGQUIT - то наступит хаос и разруха в клозетах, не делайте так:

The system() function manipulates the signal handlers for SIGINT,
SIGQUIT, and SIGCHLD. It is therefore not safe to call system() in a 
multithreaded process, since some other thread that manipulates these
signal handlers and a thread that concurrentyl calls system() can in-
terfere with each other in a destructive manner. If, however, no such
other thread is active, system() can safely be called concurrently from
multiple threads. See popen(3C) for an alternative to system() then is
thread-safe.

Обработчики SIGQUIT и SIGINT

Давайте создадим простое приложение и запустим его в терминале:

#include <stdlib.h>
#include <signal.h>
#include <stdio.h>

void sig_handler(int signum){
    // Устаревшая функция, но для демонстрации пойдёт
    printf("Пришёл сигнал %d\n", signum);
    exit(0);
}

int main(void) {
    signal(SIGINT, sig_handler);
    system("sleep 10");
    printf("Завершилось потому что завершилась программа\n");
    return 0;
}
$ gcc sigdemo.c -o sigdemo.elf
$ ./sigdemo.elf
^CЗавершилось потому что завершилась программа

Мы послали сигнал SIGINT, вот только материнская программа его не получила. Потому что из-за переопределения обработчиков сигналов его получило и обработало дочернее приложение. Мы могли бы обрабатывать случай выхода по сигналу у дочернего приложения, но это уже как-то не клеится с концепцией «Run & Go»:

int ret = system("foo");
if (WIFSIGNALED(ret) && (WTERMSIG(ret) == SIGINT || WTERMSIG(ret) == SIGQUIT))
    break;

Кроме того о таком поведении в мануалах написано, а вот как его обрабатывать указали разве что в glibc. В любом случае простой вызов system() внезапно усложнился проверками, отсутствие которых может оставить материнскую программу в бесконечном цикле.

Привязка к shell

Достаточно забавным фактом является то, что system не вызывает напрямую переданную команду. Он не может этого сделать без парсинга аргументов (ведь входящий аргумент - единичная const char * ). Парсинг - штука сложная и дорогая, поэтому разработчики решили упросить себе жизнь и вместо этого перекладывают эту ответственность на сторону shell:

34 #define SHELL_PATH "/bin/sh" /* Path of the shell. */
35 #define SHELL_NAME "sh" /* Name to give it. */
...
147 ret = __posix_spawn (&pid, SHELL_PATH, 0, &spawn_attr,
148 (char *const[]){ (char *) SHELL_NAME,
149 (char *) "-c",
150 (char *) line, NULL },
151 __environ);

Какие ограничения это накладывает:

  • Система должна хотя бы частично соответствовать POSIX-стандартам, как минимум иметь /bin/sh . Обычно это так и есть, но приложения в контейнерах могут и не иметь полного набора всех утилит. Поэтому если ваш контейнер не имеет shell по адресу /bin/sh - программы использующие system() нормально работать не будут.
  • Экранирование и скобки становятся заботой пользователя. Если вам нужно передать несколько аргументов которые могут содержать пробелы - вам нужно экранировать пробелы вручную через добавление \␣ или через скобки, которые скорее всего тоже нужно будет экранировать, если это будут двойные скобки.
  • Раскрытие shell-переменных. Иногда это хорошо, но иногда может помешать вам передать в программу последовательность символов, которую shell попробует развернуть. Например, вы хотите передать в программу «$100»:
#include <stdlib.h>

int main(void) {
    system("echo $100");
    return 0;
}
$ ./sigdemo.elf
<пусто>

Этого можно избежать, если экранировать аргумент $100 с помощью одинарных кавычек (переменные в одинарных кавычках не раскрываются shell) или экранировав символ $ , чтобы избежать раскрытия аргументов.

  • Это даже не всегда будет sh-shell. Это под NIX-системами там будет /bin/sh (dash/bash/zsh). Под MS-DOS это будет COMMAND.COM , под MSVC - cmd.exe . Все они имеют разный синтаксис переменных, так что написать кроссплатформенную программу которая будет передавать переменные в shell просто так не получится.

Поэтому, по итогам главы, совет #7:

Не используйте system() для чего-то сложнее демонстрации.

Заключение

Надеюсь что этот гайд-справочник пригодится вам при написании ваших программ и что вы теперь станете меньше верить ответам со stackoverflow с наивысшим рейтингом.

Особая благодарность в подготовке статьи следующим людям:

  • Моему коллеге, @Rootlexx. за ревью и подсказки.
  • Моему коллеге, Вячеславу Р. за подсказки.
  • Пользователю LOR @wandrien за исследование system
  • Моей жене за изображения (к сожалению ЛОР позволяет установить только одно) к статье.

Оригинальная статья была опубликована на habr и была попячена вместе с аккаунтом по запросу автора.

★★★★★

Проверено: Dimez ()
Последнее исправление: PPP328 (всего исправлений: 2)

А вы, ведь, не хотите, чтобы вызвав «зараженный» ping в торговом приложении, оно начало рассылать по сети враждебные команды или пытаться строить из себя MitM?

Выглядит как перевод с другого языка.

Всегда защищайте файловые дескрипторы флагом FD_CLOEXEC, если планируете вызывать fork+execv для программ, которые не должны их видеть.

Или можно просто сделать closefrom(3); ну или его аналог если какие-то дескрипторы надо сохранить. Это в целом надёжнее, т.к. если используешь какие-то сторонние библиотеки - не всегда есть контроль за тем, что и как они открывают.

Блокирование главной нити программы для выполнения какой-то операции очень плохая практика. Значит нам нужно создавать отдельную нить для выполнения там system()

Во-первых, system можно сделать неблокирующим средствами шелла (но код завершения потом уже не узнать), а во-вторых system обычно вызывают как раз для того, чтобы сделать всё с ожиданием завершения, примерно как это делается в шелл-скриптах. Ну и о мультитреде в таких программах речь обычно не идёт. Ещё много чего можно написать, лень, суть такова: ты не понял зачем нужен system, он не для тонкого управления потоками выполнения программ, а для скрипто-подобного простого запуска подпрограммы с ожиданием. И эту функцию он выполняет хорошо.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)

Оригинальная статья была опубликована на habr

По кривляниям видно.

ЛОРу остро нужен лимит в 140 или сколько там символов на статью. Тема интересная, крупицы информации в стене текста есть, осталось выпарить/выморозить воду.

t184256 ★★★★★
()
Ответ на: комментарий от hobbit

Оригинальная статья была опубликована на habr и была попячена вместе с аккаунтом по запросу автора.

https://dic.academic.ru/dic.nsf/ushakov/959926 Толковый словарь Ушакова ПОПЯЧЕННЫЙ

попяченный — прил., кол во синонимов: 1 • двинутый (42) Словарь синонимов ASIS. В.Н. Тришин. 2013 …   Словарь синонимов

попяченный — поп яченный; кратк. форма ен, ена …   Русский орфографический словарь

попяченный — прич.; кр.ф. попя/чен, попя/чена, чено, чены …   Орфографический словарь русского языка

попяченный — по/пяч/енн/ый …   Морфемно-орфографический словарь

двинутый — поведенный, не в своем уме, чокнутый, ненормальный, передвинутый, сумасшедший, трахнутый, душевнобольной, сдвинутый, попяченный, психованный, пошевеленный, направленный, помешанный, тронутый, безумный, долбанутый, перемещенный, малахольный,… …   Словарь синонимо

Шутка

ТС с хабра попятился на ЛОР.

Forum0888
()
Последнее исправление: Forum0888 (всего исправлений: 3)
Ответ на: комментарий от t184256

По кривляниям видно.

ЛОРу остро нужен лимит в 140 или сколько там символов на статью. Тема интересная, крупицы информации в стене текста есть, осталось выпарить/выморозить воду.

Зря. Нормальная статья. Видно что люди старались, и откровенных ляпов я не увидел. А было бы написано суше - «молодежи» (а, я так понимаю, это и есть основная целевая аудитория) читать было бы менее интересно.

ПыСы. Вот чего ЛОРу реально не хватает так это кнопарей в шапке «scroll to the first / last comment» - умрёшь на телефоне листать…

bugfixer ★★★★
()
Последнее исправление: bugfixer (всего исправлений: 1)
Ответ на: комментарий от bugfixer

Вот чего ЛОРу реально не хватает так это кнопарей в шапке «scroll to the first / last comment» - умрёшь на телефоне листать…

LOR panel - ещё один аддон для навигации по форуму

В мобильном файрфоксе ставлю вместе с другими дополнениями через «собственный сборник дополнений».

vvn_black ★★★★★
()
Ответ на: комментарий от bugfixer

«молодежи»

Здесь? Есть молодёжь, ищущая знаний? Разбирающаяся в Thread safety?

откровенных ляпов я не увидел.

Нахрена писать простейшие обёртки в таком стиле:

return pthread_sigmask(SIG_SETMASK, &setnew, &g_sig_blocked) == 0;
Если всё что делает функция — это вызов sigprocmask(), возвращающей 0 в случае успешного выполнения, то зачем менять возвращаемое значение на противоположное?

И «вода» это не ляпы, а, например, рассуждения про вложеность обработчиков сигналов. Если читатель вобще первый раз это термин видит, то он ничего не поймёт, а для остальных... повторение мать заикания.

mky ★★★★★
()
Ответ на: комментарий от mky

Здесь? Есть молодёжь, ищущая знаний?

Не могу сказать за «молодёжь» в целом, но людей с минимумом опыта в разработке (а тем более - в промышленной разработке) по моим наблюдениям - предостаточно. А те люди кто не хотят учиться и читать такие длинные «простыни» не будут.

ПыСы. Мне почему-то кажется что Вы не очень высокого мнения о современной молодёжи… Ну, или я слишком оптимистично настроен.

Разбирающаяся в Thread safety?

Дык, оно придёт, со временем. В частности благодаря вот таким вот статьям.

Если всё что делает функция — это вызов sigprocmask(), возвращающей 0 в случае успешного выполнения

Там ещё и маска правильно инициализируется, если я ничего не упустил.

то зачем менять возвращаемое значение на противоположное?

Имхо, нет ничего плохого в «инвертировании» интов если возвращаем булы success/failure.

Если читатель вобще первый раз это термин видит, то он ничего не поймёт, а для остальных… повторение мать заикания.

Я склонен надеяться что может быть хоть кто-то из «новобранцев» задумается «откуда рожки растут», и одно это уже оправдывает усилия автора(ов).

bugfixer ★★★★
()

Да, можно скопировать самый заплюсованный ответ и сказать, что миллионы не могут ошибаться …

  1. Если смотреть целыми днями «Ютуб» вместо чтения книг, то мышление изменяется в сторону повторения готовых решений вместо создания собственных. Иногда даже необратимо.

  2. Похоже на то, что написатель статьи сам не разобрался до конца о чём писал. Тоже самое можно видеть в советских учебниках по полупроводниковой электронике - в них нихрена непонятно, а вот по ламповой - всё ясно сразу. Причина в том, что ламповую электронику в Союзе создавали сами, а полупроводниковую - повторяли с Запада.

Бездарей-повторятелей лучше не читать совсем, чтобы не засорять свой разум. Читайте первооткрывателей, родоначальников - эти люди пишут просто и ясно, потому что сами всё придумали и сделали.

Enthusiast ★★
()
Ответ на: комментарий от bugfixer

@LamerOk, раз уж заминусили - не сочтёте за труд развернуть мысль? Мне правда интересно…

bugfixer ★★★★
()
Ответ на: комментарий от t184256

выморозить воду

Для любителей сухомятки уже есть маны.

Nervous ★★★★★
()

Спасибо!

Пачку кофе, чая и печенья вам (бесплатно).

Forum0888
()
Последнее исправление: Forum0888 (всего исправлений: 3)

Кстати, познакомьтесь, на КДПВ Картошка.

Фраза осталась от публикации на хабре?

greenman ★★★★★
()
Ответ на: комментарий от Enthusiast

Причина в том, что ламповую электронику в Союзе создавали сами, а полупроводниковую - повторяли с Запада.

Нет. Просто в позднем совке поменялся стиль изложения. Начали писать заумно, чтобы выпендриться. Челендж вида «напиши определение с помощью минимального количества слов и с использованием заумных терминов». Понятность изложения вообще перестала волновать авторов. И Россия полностью унаследовала эту тенденцию.

Это не только электроники касается. Я интегралы учил по книжке Лузин Н.Н. Интегральное исчисление. Она старая (издание 1961, а автора замучили на десять лет раньше), но там понятным языком написано. В поздних нихера не понятно. Пока не нашёл эту книгу было туго.

ox55ff ★★★★★
()

вилку в левую руку, нож в правую.
вилкой суппортишь/накидываешь.
*статью не читал

etwrq ★★★★★
()

если даже опустить тот момент, что статья - чистой воды графоманство, продираясь сквозь которое, крайне сложно дойти до сути, «кушать» в данном контексте, стилистически не верно, это нарушение языковых норм.

https://russkiiyazyk.ru/kultura-rechi/kushat-ili-est-kak-pravilno-govorit.html

aol ★★★★★
()
Ответ на: комментарий от bga_

Я че-то не понял, ты мне сеппуку предлагаешь сделать или чистить чужую графоманию от кривляний?

t184256 ★★★★★
()
Ответ на: комментарий от ox55ff

На превью оно было. После опубликования исчезло.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от bga_

Простите, машинный перевод с какого языка? Статья писалась с нуля.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от bugfixer

Вы не очень высокого мнения о современной молодёжи

Чту традиции. Здесь положено всё и всех ненавидить.

Дык, оно придёт, со временем.

Всякие «высокоуровневые» ЯП не просто так придумали, большинство не может в Си, в котором много обязательной низкоуровневой возни да ещё нужно помнить нюансы общения с ОС.

Там ещё и маска правильно инициализируется

Там вобще неправильная логика. g_sig_blocked сугубо локальная переменная, имеющая смысл только вблизи fork(), объявлять её

static __thread sigset_t g_sig_blocked;
ошибочно. Она должна объявлятся там, где вызывается fork() и, если так хочется обёрток, передаваться аргументом в _sigreset_block()/_sigreset_unblock().

задумается «откуда рожки растут»

ИМХО, после такого:

В данном примере мы опустили защиту от одновременного доступа к g_sig_blocked, а ведь если одновременно стартуют два процесса - то произойдёт состояние гонки. Вам необходимо самостоятельно добавить защиту от этого.

Большинство не будут задумываться, а пойдут копипастить со стековерфлоу, где дают «рабочий» код, а не заготовку, куда нужно ещё какую-то защиту добавлять. А те, кто попробует использовать код, столкнутся с непонятным ″i32″ в _sigreset_default(). Ну и добавлю сюда, что в описании возможностей clone() умалчивается про флаг CLONE_CLEAR_SIGHAND, заменяющий _sigreset_default().

И раз уж статья претендует на идеально-правильный код, встаёт отдельный вопрос, хорошо ли блокировать сигналы SIGBUS, SIGFPE, SIGILL, SIGSEGV?

mky ★★★★★
()

Ну так и чего, где финальный «абсолютный пример», на который надо равняться? Автор не осилил…

Shushundr ★★★
()
Ответ на: комментарий от mky

Я до этого банально не дочитал. Говорю же - графомания чистой воды!

aol ★★★★★
()
Ответ на: удаленный комментарий

Откуда дровишки?

Шутка это была. У Чехова есть «Правила для начинающих авторов»:

Всякого только что родившегося младенца следует старательно омыть и, давши ему отдохнуть от первых впечатлений, сильно высечь со словами: «Не пиши! Не пиши! Не будь писателем!»...

Я не считаю, что молодёжь или ещё кого нужно завлекать в Си-кодеры искромётным юмором и прибаутками. Кому скучно читать, пусть читают научную фантастику. Реально, когда в статье нагромождение разнородных фактов, сложно выделить и запомнить важное. А когда ещё перевирают историю возникновения fork() в unix на PDP-7...

По поводу atexit() не знаю, может backtrace()?

mky ★★★★★
()
Ответ на: комментарий от mky

По поводу atexit() не знаю, может backtrace()?

«Уважаемые» люди утверждают что стандартного способа не существует (а жаль). Backtrace() - не вариант: слишком дорого. Нашлось решение проще (хотя тоже - не бесплатное). А я уж было рассматривал варианты с перехватом exit()…

bugfixer ★★★★
()

Спасибо конечно ТС за статью, но привязка к термину «вилка» - профонация (на кого рассчитана такая аналогия не понятно).

Forum0888
()
Ответ на: комментарий от mky

Ну поделитесь решеним.

Оно сугубо специфичное для нашего кода, и из другой оперы немножко. Грубо говоря - информация о том «раскручиваемся» мы или нет перестала быть существенной. Цена - дополнительные телодвижения во время exit() которых можно было бы избежать если бы такой boolean в том или ином виде присутствовал.

ПыСы: глубинная причина почему такой boolean не существует кроется в so-шках которые теоретически могут оффлоадиться on-the-fly.

bugfixer ★★★★
()
Ответ на: комментарий от bugfixer

Грубо говоря - информация о том «раскручиваемся» мы или нет перестала быть существенной.

Так и должно быть. Кто хочет перехватывать exit, ставит туда свой обработчик, а дела тех, кто поставил обработчики после, его не должны особо касаться.

firkax ★★★★★
()
Ответ на: комментарий от firkax

Кто хочет перехватывать exit, ставит туда свой обработчик

Дык, в том то и дело что явно никто себя в atexit() не добавлял. Речь о довольно сложном и «неконтролируемом» наборе global и function-local statics в модели publisher-subscriber. Причём publisher - singleton. Пришлось добавить на него pointer в subscribers (они не обязательно static - могут come and go semi-randomly), и обнулять его пробегаясь по списку subscriber’ов в деструкторе publisher’а (счёт subscribers идёт на десятки) дабы отвязаться от порядка destruction. Хотя, по большому счету, outside of main message processing loop это всё «возня в песочнице» и «waste of CPU cycles»: знать бы что выходим - можно было бы не делать ничего.

bugfixer ★★★★
()
Последнее исправление: bugfixer (всего исправлений: 1)

Думаю, стоит упоминуть особенности использования vfork. Корова коровой, но в принципе он позволяет избежать много проблем, если использовать его правильно, ну и создать если использовать неправильно (exit вместо _exit например)

mittorn ★★★★★
()

Искал в тексте словосочетание «чистить вилкой», не нашёл. Опечалился: это же классика, это знать надо.

Mobutu_Sese_Seko
()
Последнее исправление: Mobutu_Sese_Seko (всего исправлений: 1)
Ответ на: комментарий от mittorn

При вызове vfork вызывающая нить замораживается, покуда дочерний «процесс» не вызовет execve, _exit или не упадет, убитый защитой доступа памяти. Кроме того, для ускорения вызова память родителя не копируется, а используется как есть - вместе с кучей и стеком.

Расскажите ещё, люди в комментарии почитают.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

С чем сталкивался - если по ошибке вместо _exit дёргать exit, то он будет вызывать всякие atexit и разрушит память. А такую ошибку легко допустить, пытаясь заменить fork на vfork

mittorn ★★★★★
()
Ответ на: комментарий от mittorn

В одной из удаленных статей на хабре как раз рассматривал подробно такую проблему. exit вместо _exit и в обычном fork сломает всё. Потому что хендлеры дюпятся и все деструкторы их закроют.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

Тогда зачем в примере в этой статье у вас ″exit(127)″?

Но exit() не всё сломает, только буферы сбросит, и это просто приведёт к дублированию в записываемых файлах. Но часто эти буферы пустые.

А если fork() используется не для exec(), а для порождения вспомогательного процесса и exit() ему нужен, то перед fork() нужно делать fflush() по всем потокам ввода/вывода.

mky ★★★★★
()
Ответ на: комментарий от mky

Тогда зачем в примере в этой статье у вас ″exit(127)″?

Вы правы, это неверно. Исправил, оставив лишь там, где код неверен «по сюжету»

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

Хорошо, но, ИМХО, здесь:

  switch (fork()) {
     case -1: _exit(EXIT_FAILURE);
уже излишне, ″-1″ ведь обрабатывается в родительском процессе, пусть родитель нормально сбросит буферы через ″exit()″.

mky ★★★★★
()
Ответ на: комментарий от mky

и это просто приведёт к дублированию в записываемых файлах.

Вот с этого места - можно поподробнее? О каких буферах речь - в user или kernel space? И откуда дублирование возмётся?

bugfixer ★★★★
()
Ответ на: комментарий от bugfixer

И откуда дублирование возмётся?

Как и сказано в статье, при вызове clone по умолчанию дублируются все открытые файловые дескрипторы родительского процесса. Включая файлы, стримы, мьютексы. Т.е. всё что в unix называется «файл». Следовательно вызов деструктора на копию дескриптора вызовет поломку процесса в материнском процессе.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от bugfixer

В user space, буферы потокового вывода, который fprintf. При выводе через fprintf(), обычно, write() сразу не вызывается, запись идёт в буфер. Буфер сбрасывается (данные передаются ядру) при заполнении буфера, при закрытии потока и при нормальном завершнии процесса, то есть exit() или return из main().

Ну, а остальное уже написано, про копирование памяти и файловых дескрипторов.

Дублирование возмётся, если в момент fork() у родительского процесса будут не пустые буферы, а потомок вызовет exit().

mky ★★★★★
()
Ответ на: комментарий от mky

При выводе через fprintf(), обычно, write() сразу не вызывается, запись идёт в буфер.

Эта часть очевидна.

Буфер сбрасывается (данные передаются ядру) при заполнении буфера, при закрытии потока и при нормальном завершнии процесса

И это тоже понятно. Непонятно почему Вы думаете что write() из parent / child будет по разным offsets?

bugfixer ★★★★
()
Ответ на: комментарий от PPP328

Следовательно вызов деструктора на копию дескриптора вызовет поломку процесса в материнском процессе.

Вопиющее заблуждение. Parent вообще ничего не заметит если child close() на этих fd’s дёрнет. Можете сами проверить. Проблемы могут начаться только если child как-то ещё эти fd’s использовать начнёт - будет каша.

ПыСы. Не помню когда мьютекс в файл превратился, но это мелочи.

bugfixer ★★★★
()
Ответ на: комментарий от bugfixer

Эта часть очевидна.

Вы же не единственный читатель. Подразумевается, что это будут читать и другие, которые «люди с минимумом опыта», поэтому и жую.

Касательно offset всё написано. В современно линуксе man fork, а в других ОС может быть в man lseek.

The child inherits copies of the parent's set of open file descriptors. Each file descriptor in the child refers to the same open file description (see open(2)) as the corresponding file descriptor in the parent. This means that the two file descriptors share open file status flags, file offset, and signal-driven I/O attributes (see the description of F_SETOWN and F_SETSIG in fcntl(2)).

Смещение в файле хранится не в user space, а в ядре, после fork() у процессов общий файловый дескриптор, который в ядре. Запись в файл из потомка смещает offset и для потомка и для родителя.

И dup() делат дескрипторы аналогично fork().

Но даже, если бы это было не так, то поток (файл) мог быть открыт в режиме O_APPEND, или вобще это мог быть STDOUT/printf() в pipe или без перевода строки...

mky ★★★★★
()
Ответ на: комментарий от mky

Но даже, если бы это было не так

Я, может быть, не до конца правильно выразился. Я всегда (подчёркиваю - это важно) признаю свои ошибки, и желаю учиться. Не надо воспринимать что я говорю как истину в последней инстанции. Но, мне не понятно - почему файлы не корраптятся налево и направо?

ПыСы. Я таки поиграюсь завтра на тему delayed flushes after fork().

bugfixer ★★★★
()
Ответ на: комментарий от bugfixer

При чём тут ошибки. Я писал про ситуацию, что даже, если бы пропатчить ядро и сделать, что после fork() потомок получит не дубликат файлового дескриптора, а независимую копию, так что у каждого процесса будет свой offset в файл, то и в этом случае, могут быть проблемы с user-space буферизаций. Если файл исходно открыт в режиме O_APPEND, тогда смещения в файле как-бы нет, запись всегда в конец.

почему файлы не корраптятся налево и направо?

А почему файлы должны портиться налево/направо? fork() ведь начинающие кодеры нечасто используют как fork(), в основном с exec(), причём exec() обычно успешный.

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    FILE *F = fopen("test_fork_iostream.txt", "w");
    fprintf(F, "1234567890");
    switch (fork()) {
        case -1: _exit(EXIT_FAILURE);
        case 0 : {
            exit(EXIT_SUCCESS);
        }
        default:
            wait(NULL);
            fprintf(F, "ABCDEFG");
            fclose(F);
    }
    return EXIT_SUCCESS;
}
mky ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.