1. Основы многопроцессорности: анатомия процесса, системные вызовы fork, exec и waitpid в Linux
Основы многопроцессорности: анатомия процесса, системные вызовы fork, exec и waitpid в Linux
Приветствую тебя, студент! Добро пожаловать на курс, который превратит тебя из простого написателя скриптов в настоящего системного инженера. Мы начинаем погружение в мир, где программы не просто выполняют код строчка за строчкой, а живут, размножаются, общаются и иногда даже умирают (и превращаются в зомби, я серьезно).
Сегодня мы не будем говорить о потоках (threads), которые живут внутри одного процесса. Мы начнем с базы — с самих процессов. Ты узнаешь, как операционная система Linux управляет программами и как ты, используя C++, можешь взять это управление в свои руки.
Модуль 1: Анатомия процесса в Linux
Прежде чем создавать процессы, давай разберемся, что это вообще такое. Когда ты запускаешь программу (например, свой скрипт на Python или скомпилированный бинарник C++), операционная система создает для неё процесс.
Процесс — это не просто код. Это контейнер, которому ОС выделяет ресурсы. У каждого процесса есть свой уникальный идентификатор — PID (Process ID). Также у каждого процесса есть родитель — PPID (Parent Process ID), тот процесс, который его запустил.
В памяти Linux процесс выглядит как слоеный пирог. Давай посмотрим на его устройство.
!Структура виртуальной памяти процесса: от кода программы до стека
Разберем слои этого пирога:
new в C++ или malloc, память берется отсюда. Куча растет снизу вверх.Важный нюанс: В Linux каждый процесс считает, что у него есть вся память компьютера. Это иллюзия, которую создает Виртуальная память. Процесс А не может просто так залезть в память Процесса Б. Это обеспечивает безопасность и стабильность.
Модуль 2: Системный вызов fork() — Искусство клонирования
В Python ты, возможно, привык запускать скрипты и не думать, откуда они берутся. В C++ на Linux мы используем системные вызовы (system calls или syscalls). Это прямые обращения к ядру операционной системы с просьбой сделать что-то важное.
Самый главный вызов для создания нового процесса — fork().
Когда процесс вызывает fork(), происходит магия: ядро создает точную копию текущего процесса. Клонируется всё: код, переменные, состояние памяти, открытые файлы. С этого момента в системе работают два почти идентичных процесса.
Как же их различить? По возвращаемому значению функции fork().
* Родителю (тому, кто вызвал) функция возвращает PID ребенка.
* Ребенку (новому процессу) функция возвращает 0.
Давай посмотрим на код. Для работы нам понадобятся заголовки <unistd.h> (для fork, pid_t) и <sys/types.h>.
Что здесь происходит?
Строка pid_t pid = fork(); выполняется один раз, но возвращает управление дважды: один раз в родительском процессе, второй раз — в дочернем. После этой строчки код раздваивается.
> "Fork — это как если бы вы подошли к ксероксу, положили себя на стекло, нажали кнопку, и из лотка вылезла ваша точная копия, которая сразу же начала жить своей жизнью."
Модуль 3: Семейство exec() — Трансплантация мозга
Клонирование — это весело, но бесполезно, если клон делает то же самое, что и оригинал. Обычно мы хотим, чтобы новый процесс запустил другую программу. Например, так работает терминал: когда ты пишешь ls, терминал делает fork, а затем ребенок превращается в утилиту ls.
Для этого используется семейство функций exec (от слова execute — выполнять). Самые популярные: execl, execv, execvp.
Что делает exec? Он полностью заменяет текущий образ процесса (код, данные, стек) новой программой с диска. PID остается тем же, но "начинка" меняется полностью.
Важный момент: Если exec сработал успешно, управление никогда не вернется в следующую строчку кода. Старая программа стерта из памяти.
Пример использования execl:
Обычно fork и exec используют в связке:
fork().if (pid == 0) вызывает exec(), чтобы стать новой программой.Модуль 4: waitpid() и атака зомби
В мире Linux родители обязаны следить за своими детьми. Когда дочерний процесс завершается (делает return или exit()), он не исчезает бесследно. Он превращается в зомби (zombie process).
Зомби — это процесс, который уже умер (память освобождена), но запись о нем все еще висит в таблице процессов ядра. Он ждет, пока родитель прочитает его "предсмертную записку" — код возврата.
Если родитель не прочитает код возврата, зомби будут висеть и засорять таблицу процессов. Чтобы этого избежать, родитель должен использовать системный вызов wait() или waitpid().
Эти функции делают две вещи:
Пример правильного воспитания процессов:
!Жизненный цикл: создание через fork, работа и ожидание через waitpid
Разбор макросов:
*WIFEXITED(status): Проверяет, завершился ли процесс нормально (сам, а не был убит сигналом).
* WEXITSTATUS(status): Извлекает тот самый код возврата (в нашем примере 42).Итоги
Сегодня мы разобрали фундамент многозадачности в Linux:
0 — ребенку, PID — родителю.Это низкоуровневая база. В C++ есть высокоуровневые обертки, но понимание того, как это работает "под капотом" через системные вызовы, делает тебя профессионалом, понимающим цену каждому созданному ресурсу. В следующей статье мы поговорим о том, как эти процессы могут общаться друг с другом (IPC).