Какие системные вызовы Linux используются для создания процесса?

Ответ

В Linux для создания нового процесса традиционно используется комбинация двух системных вызовов: fork() и exec().

  1. fork(): Создает почти точную копию (дочерний процесс) вызывающего процесса (родительского). Дочерний процесс получает копию памяти, файловых дескрипторов и других атрибутов. Для оптимизации используется механизм Copy-On-Write (COW): память физически копируется только при попытке записи.
  2. exec() семейство (например, execve): Заменяет текущий образ процесса (код, данные, стек) новым исполняемым файлом, загружаемым с диска. Этот вызов обычно выполняется в дочернем процессе после fork(), чтобы запустить другую программу.

Типичная последовательность в оболочке (shell) или демоне:

#include <unistd.h>
#include <sys/wait.h>

pid_t pid = fork(); // Создаем копию процесса
if (pid == 0) {
    // Код выполняется в дочернем процессе
    char *args[] = {"/bin/ls", "-l", NULL};
    execve(args[0], args, NULL); // Заменяем образ на /bin/ls
    // Если execve вернул управление - это ошибка
    _exit(EXIT_FAILURE);
} else if (pid > 0) {
    // Код выполняется в родительском процессе
    int status;
    waitpid(pid, &status, 0); // Ожидаем завершения дочернего процесса
}
  1. clone(): Более гибкий низкоуровневый вызов, на котором основаны fork() и механизмы создания потоков (threads). Позволяет точно контролировать, какие ресурсы (память, таблица файлов, пространство имен) будут разделяться между родителем и потомком. Именно clone() с определенными флагами используется для создания контейнеров (namespaces, cgroups).

С точки зрения DevOps, понимание этих вызовов важно для отладки проблем с порождением процессов в контейнерах, написания своих демонов или скриптов мониторинга.

Ответ 18+ 🔞

Слушай, а ведь это реально интересная тема, как в Линуксе новые процессы появляются на свет. Не просто так, по щучьему велению, а по чёткому, проверенному временем алгоритму. Прямо как в старом добром конструкторе, только вместо болтиков — системные вызовы.

Вот смотри, вся эта магия крутится вокруг двух главных действующих лиц: fork() и exec(). Представь себе, что ты — процесс. Ты сидишь, работаешь, память свою используешь, файлы открываёшь. И тут тебе надо запустить, например, ls. Что делать? Самому в ls превращаться? Не, это пиздец какой-то.

Вот тут и приходит на помощь fork(). Этот вызов — просто ёперный театр. Он берёт тебя, родительский процесс, и создаёт твою почти полную копию. Дочерний процесс. У него будет своя копия твоей памяти, твоих открытых файлов — всего. Ключевое слово — «копия». Раньше это было дорого, а сейчас умные ядроёбы придумали Copy-On-Write (COW). Гениальная штука, ей-богу. Память-то физически не копируется сразу. Пока оба процесса (и родитель, и дитё) только читают — они смотрят на одни и те же страницы памяти. Но как только кто-то один попытается в эту память записать — вот тут ядро и делает настоящую копию только для этого засранца. Экономия — овердохуища.

Ну ок, копия есть. Но это же всё ещё ты, только младший. А нам надо запустить ls, а не свою копию. Вот тут на сцену выходит второе действующее лицо — семейство exec() (самый популярный — execve). Это уже не копирование, а полная замена. Представь: дочерний процесс, который пока что твоя копия, вызывает execve("/bin/ls"). И происходит волшебство: весь его старый код, данные, стек — всё это выкидывается на свалку истории. А на его место с диска загружается образ новой программы — того самого /bin/ls. Старый процесс умер, да здравствует новый! Если execve вернул управление — значит, случилась ошибка, и загрузка не удалась. В нормальной ситуации возврата нет.

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

#include <unistd.h>
#include <sys/wait.h>

pid_t pid = fork(); // Раздвоение личности. Сам от себя охуел.
if (pid == 0) {
    // А вот тут мы уже в ребёнке.
    char *args[] = {"/bin/ls", "-l", NULL};
    execve(args[0], args, NULL); // Щёлкаем пальцами и превращаемся в ls
    // Сюда попадём только если execve обосрался.
    _exit(EXIT_FAILURE); // Тихий уход в ночь.
} else if (pid > 0) {
    // А это уже родитель, он продолжает жить своей жизнью.
    int status;
    waitpid(pid, &status, 0); // Сидит, ждёт, пока дитя не нагуляется.
}

Всё, казалось бы, просто. Но есть ещё один монстр, который сидит в самом низу и всем заправляет — clone(). Это не просто вызов, это швейцарский нож для создания всего на свете. И fork(), и создание потоков (pthreads) — всё это внутри использует clone(). В чём прикол? clone() позволяет с хитрой жопой указать, что именно ты хочешь разделить со своим потомком. Общую память? Легко. Общую таблицу открытых файлов? Без проблем. А можешь вообще создать процесс в новом пространстве имён (namespace) — и вот ты уже на полпути к контейнеру. Именно такую магию с clone() и используют всякие Docker'ы, чтобы изолировать процессы. Так что для девопса понимание этой кухни — не просто академический интерес. Когда у тебя в контейнере что-то не запускается или демон дохнет странно, копать надо часто как раз сюда, в эти низкоуровневые механизмы порождения жизни. Иначе можно сидеть и гадать, а что за хуй с винтом происходит.