Операционные системы

Теория: Что такое процесс

Когда программа запускается в Linux, операционная система не выполняет её «с диска» напрямую. Вместо этого ядро создаёт процесс — изолированную рабочую среду, где программа получает всё необходимое для работы: участок оперативной памяти, собственное адресное пространство, идентификаторы (PID, UID, GID), дескрипторы файлов, сетевые соединения и права доступа.

Именно через процессы ядро управляет всей жизнью системы. Каждая служба (демон), контейнер, CI-агент (Continuous Integration Agent — программа, выполняющая автоматические задачи в системе непрерывной интеграции), или пользовательское приложение — это процесс, запущенный из исполняемого файла, но живущий по своим правилам: со своим адресным пространством, приоритетами и ресурсами.

Даже сама система инициализации — systemd — это тоже процесс, первый в иерархии, с идентификатором PID 1. От него происходят все остальные: оболочки пользователей, сетевые службы, серверы приложений, фоновые демоны. Можно сказать, что PID 1 — это корень «дерева жизни» Linux, а процессы — его ветви, через которые система дышит, работает и взаимодействует с миром.

Когда пользователь вызывает программу, например bash, ядро создаёт новый процесс, копируя часть контекста родителя (через системный вызов fork()) и подменяя его кодом новой программы (execve()). Так в дереве процессов появляется новая ветвь, а ядро начинает отслеживать её состояние — пока процесс не завершится и не освободит ресурсы.

Вся активность в Linux - от открытия терминала до запуска контейнера или любого пользовательского приложения - существует именно в форме процессов.

Создание процессов

Когда вводится команда в терминале, bash делает два шага. Сначала он создаёт копию себя системным вызовом fork(). После этого в системе работают два процесса: родитель (bash) и дочерний. Дочерний наследует окружение, но имеет собственный PID. Затем дочерний процесс вызывает exec(), заменяя себя исполняемым файлом команды — например, /bin/ls. Таким образом, каждое выполнение команды порождает новый процесс, который живёт до тех пор, пока не завершит работу.

Проверка PID в практике

Переменная $$ в bash содержит PID текущего процесса. Скобки () создают создают дочерний процесс - новый процесс. Пример:

echo "Родительский PID: $$"
( echo "Дочерний PID: $$" )

Вывод будет разным:

Родительский PID: 2459
Дочерний PID: 2471

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

Фоновые процессы

Команда, запущенная с символом &, выполняется в фоне — в отдельном процессе. Bash не ждёт её завершения, а продолжает работу. Пример:

sleep 10 &
echo "PID фонового процесса: $!"

Флаг & означает «запустить в фоне», а переменная $! содержит PID последнего фонового процесса. Проверить активные фоновые задачи можно командой:

jobs -l

Она показывает список заданий и их PID. Фоновая команда завершается сама, после чего оболочка получает от ядра сигнал SIGCHLD — уведомление о завершении процесса.

Подстановки и конвейеры

В Linux не каждая команда создаёт новый процесс. Команды, встроенные в оболочку (например, cd, export, echo, alias), выполняются прямо внутри процесса самой оболочки и не порождают дочерних процессов. А вот внешние команды — это отдельные исполняемые файлы (например, ls, grep, python, systemctl). Когда оболочка встречает такую команду, она создаёт новый процесс через системный вызов fork(), затем заменяет его содержимое кодом этой программы с помощью exec(). Так работает почти всё, что мы запускаем из терминала: каждая внешняя команда получает свой PID и взаимодействует с другими через ядро.

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

Подстановка $(...) используется, чтобы выполнить одну команду и подставить её результат в другую. В момент подстановки оболочка запускает дополнительную подшелл — временный процесс, который выполняет выражение внутри скобок. Всё, что команда выведет в стандартный поток, возвращается как текст.

Пример:

echo "Количество файлов: $(ls | wc -l)"

На первый взгляд кажется, что это одна строка, но на самом деле оболочка делает несколько шагов. Сначала создаётся подпроцесс для команды ls, который выводит список файлов. Затем — ещё один процесс для wc -l, который подсчитывает количество строк. Между ними ядро создаёт канал (pipe), чтобы перенаправить вывод ls во вход wc. После завершения обеих команд оболочка подставляет полученное число в основную строку и выполняет echo, формируя итоговый вывод.

Таким образом, даже простая конструкция в одну строку на самом деле включает целую цепочку процессов, связанных конвейером и работающих параллельно. Каждый из них изолирован, но взаимодействует через буфер ядра, куда передаётся поток данных. Конвейер | — это не просто “соединение” команд, а реальный IPC-механизм, обеспечивающий передачу информации между процессами без использования временных файлов.

Чтобы увидеть, как оболочка выстраивает эти процессы, можно использовать команду:

ps -f --forest

Опция --forest строит дерево, отображая родительские и дочерние процессы. Вверху обычно находится оболочка (bash или zsh), а под ней — созданные ею команды. В дереве видно, как одна строка с конвейером на самом деле порождает несколько параллельных ветвей, связанных между собой каналами.

Этот механизм — основа всего UNIX-подхода: команды делают одно, но делают это хорошо, а объединяя их конвейером, можно собирать сложные цепочки обработки данных. Оболочка управляет этой оркестровкой процессов, создавая подшеллы, настраивая каналы и синхронизируя вывод так, чтобы всё выглядело как единая команда.

Пример с несколькими потомками

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

Пример скрипта:

#!/bin/bash
for i in {1..3}; do
    ( echo "Потомок $i: PID=$$"; sleep $((RANDOM % 3 + 1)) ) &
done
wait
echo "Все процессы завершены"

Этот сценарий создаёт три подпроцесса, каждый из которых выводит свой PID и "засыпает" на случайное время от одной до трёх секунд. Символ & после скобок запускает команду в фоне, не блокируя выполнение цикла. Команда wait приостанавливает родительский процесс (в данном случае — сам bash), пока все дочерние процессы не завершат работу.

Вывод при запуске будет примерно таким:

Потомок 1: PID=3182
Потомок 2: PID=3183
Потомок 3: PID=3184
Все процессы завершены

Здесь видно, что у каждого подпроцесса свой PID — уникальный идентификатор, присвоенный ядром. Родитель дожидается завершения всех троих, после чего печатает итоговое сообщение.

Что реально происходит внутри

Сам bash не умеет создавать процессы сам по себе. Он обращается к ядру, используя стандартные системные вызовы. Последовательность действий при запуске команды в Linux всегда одна и та же:

  1. fork() — создаёт копию текущего процесса. Новый процесс наследует память, переменные окружения и файловые дескрипторы, но получает собственный PID. На этом этапе родитель и потомок почти идентичны.
  2. exec() — заменяет содержимое дочернего процесса новым исполняемым файлом. То есть bash вызывает execve("/bin/echo", ["echo", "Hello"], envp) и загружает в адресное пространство программы код echo.
  3. wait() — родитель приостанавливает выполнение, пока потомок не завершится. После окончания работы дочерний процесс посылает родителю сигнал SIGCHLD, который сообщает, что можно забирать статус завершения.

Проверка через strace

Чтобы увидеть эту цепочку «вживую», можно использовать инструмент strace, который перехватывает системные вызовы процесса.

Пример:

strace -f bash -c "echo Hello > /dev/null"

Флаг -f заставляет strace следить не только за основным процессом, но и за всеми его потомками. Команда bash -c "echo Hello" создаёт подпроцесс для echo, и в выводе strace можно заметить ключевые моменты:

fork() = 2124
[pid 2124] execve("/bin/echo", ["echo", "Hello"], ...
wait4(2124, ...

Эти строки отражают фактическую работу ядра:

  • fork() создаёт новый процесс с PID 2124;
  • execve() загружает исполняемый файл /bin/echo;
  • wait4() заставляет родителя дождаться завершения потомка.

Именно эта последовательность лежит в основе всего, что происходит в системе: от запуска утилиты ls до работы веб-сервера или контейнера.

Главное

В Linux создание процессов происходит постоянно. Каждая команда, конвейер, скобки или фоновое задание запускают новые процессы. Механизм универсален и основан на трёх шагах, которые выполняет ядро:

  1. fork() — создание копии текущего процесса;
  2. exec() — замена содержимого новой программой;
  3. wait() — ожидание завершения потомков.

Эти шаги скрыты от пользователя, но видны через команды ps, jobs, wait и strace. Благодаря этой модели Linux может одновременно выполнять сотни задач, изолируя их друг от друга и управляя ими с точностью ядра.

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845