Жизнь и смерть процесса в Linux

Вольный перевод с источника

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

Рождение
Каждый раз, когда создается процесс, на самом деле другой процесс разделяет себя с помощью системного вызова fork или clone. После форкинга процессы обычно выполняют системный вызов execve, чтобы поменять текущий исполняемый двоичный файл на другой. Например, когда вы запускаете ls из оболочки bash, сначала bash разделяет себя на два процесса bash с помощью fork, а затем дочерняя оболочка bash использует exec для замены себя на ls. Дочерняя оболочка умирает, когда ls завершает выполнение, оставляя только исходный процесс bash.

На практике программы почти никогда не вызывают syscalls напрямую - вместо них используются обертки libc или libc-функции, подобные system, которая под капотом использует fork и execve (или один из нескольких вариантов execve).

Смерть
Как умирают процессы? Они почти всегда вызывают системные вызовы exit или exit_group. Если программист не вызывает exit явно, а вместо этого возвращается из main, то exit вызывается в любом случае, потому что компилятор обернул main в libc main, который вызывает exit за вас. Если программа была скомпилирована без libc и программист не вызвал exit явно, то возврат из main вызовет segfault или другой критический сигнал, потому что return попытается извлечь из стека недопустимый адрес возврата. Это подводит нас к предпоследнему способу, которым процессы могут умереть, а именно через сигналы типа SIGTERM или SIGKILL. И, наконец, последний способ для процесса умереть - это выдернуть вилку из розетки вашего компьютера.

Идентичность и зомбирование
Каждый процесс имеет уникальный pid - или, по крайней мере, он уникален до тех пор, пока ядро не переработает pid через некоторое время после смерти процесса. Прежде чем pid процесса может быть утилизирован, родительский процесс должен вызвать wait, waitpid или waitid для дочернего процесса. (В примере, представленном ранее, bash должен вызвать wait для ls.) Если родительский процесс не ждет дочерний, то дочерний процесс становится зомби, что означает, что он висит в таблице процессов ядра, тратя ресурсы. Если сам родитель умирает, то процессу переназначается родитель с pid 1 - то есть pid уникального процесса init, который автоматически вызывает wait и освобождает процесс.

Нити - Малые процессы
Что такое поток? В Linux потоки - это более или менее независимые процессы, которые имеют общую память и некоторые другие ресурсы. Они создаются вызовом clone с соответствующим флагом (флагами). В терминологии ядра, каждый поток имеет свой уникальный pid, а все потоки в одном процессе имеют один и тот же tgid (идентификатор группы потоков), который равен pid первого потока. Таким образом, с точки зрения ядра, pid идентифицирует потоки, tgid - процессы, а pid равен tgid для однопоточных процессов. Запутанным это делает то, что в usermode терминология отличается, как видно из следующей таблицы:

Имя в ядре (в task_struct) Имя в юзерспейсе Возвращаемое системным вызовом
tgid (групповой id потока) pid (id процесса) getpid
pid (id процесса) tid (id потока) gettid, clone, fork