PID (идентификатор процесса)

7 сентября 2026 · ~15 мин чтения

операционные-системы unix процессы linux основы

PID (идентификатор процесса)

PID (process identifier, идентификатор процесса) — небольшое целое число, которым операционная система помечает каждую запущенную программу. Это не имя программы и не её адрес, а номерок: единственный способ сказать ядру «вот про этот конкретный экземпляр я говорю».

История

Сама идея «процесса» как отдельной сущности, за которой ОС следит и которой распоряжается, родилась в первой половине 1960-х. Тогда компьютер был один на институт, а желающих посчитать — много, и появилась задача разделения времени (time-sharing): пусть машина по очереди уделяет внимание нескольким программам, а пользователям кажется, что каждый работает один. Термин «процесс» в нынешнем смысле закрепился в проекте Multics (с 1965 года, совместно MIT, Bell Labs и General Electric). Приписывают его обычно Джеку Деннису из MIT и коллегам по Multics — но, честно говоря, тут больше коллективного авторства, чем одного изобретателя.

Многозадачность потребовала таблицы: ядро должно где-то хранить список всех живых программ и их состояние. А раз есть таблица — нужен ключ. Так и появился PID: индекс (или почти индекс) в таблице процессов.

Дальше решающий шаг сделал Unix. В 1969–1970 годах Кен Томпсон и Деннис Ритчи в Bell Labs написали на списанной PDP-7, а потом на PDP-11, систему, где работа с процессами стала неприлично простой:

В First Edition Unix (1971) эти вызовы уже описаны в man-страницах, вместе с утилитой ps. Конструкция оказалась настолько удачной, что за пятьдесят пять лет её не переделали — только достроили.

Три вехи после:

  1. POSIX.1 (IEEE Std 1003.1, 1988) — интерфейс работы с процессами стандартизировали. Тип pid_t, гарантии уникальности, поведение сигналов. С этого момента код, работающий с PID, стал переносимым между Unix-подобными системами.
  2. PID namespaces в Linux 2.6.24 (январь 2008) — один и тот же процесс теперь может иметь разные PID в разных «пространствах имён». Внутри контейнера он PID 1, снаружи — какой-нибудь 24817. Без этой возможности не было бы ни Docker (2013), ни Kubernetes в нынешнем виде.
  3. pidfd в Linux 5.1–5.3 (2019) — файловый дескриптор, ссылающийся на конкретный процесс. Работа во многом Кристиана Браунера. Зачем — расскажу ниже, в разделе про грабли; коротко: чтобы починить дыру, которая жила в Unix полвека.

Что это такое

Когда ты запускаешь программу — например, скрипт сбора данных — ядро создаёт процесс: структуру со своей памятью, своими открытыми файлами, своим текущим каталогом, своим пользователем-владельцем. И присваивает ей номер, PID.

Номер выдаётся последовательно: 61306, 61307, 61308. Когда счётчик упирается в потолок, он оборачивается на начало и ищет первый свободный. Потолок разный: на Linux по умолчанию часто 32768 (значение видно в /proc/sys/kernel/pid_max, на 64-битных системах его можно поднять до 4 194 304), на macOS PID доходят примерно до 99999 и заворачиваются. Именно поэтому вчерашние PID 61307 и PID 61505 — нормальные числа для машины, которая давно работает и уже успела запустить и закрыть десятки тысяч процессов.

Полезно развести несколько похожих вещей:

Ключевое свойство, которое и делает PID полезным, и одновременно опасным: номер уникален только прямо сейчас. Пока процесс жив, никто другой этот номер не получит. Как только он умер и родитель забрал результат — номер вернулся в оборот.

Аналогии из жизни

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

Где ломается: номерок ты держишь в кармане, и пока он у тебя — ячейка твоя. С PID не так: ты можешь держать «номерок» в переменной, а процесс уже умер, номер выдали другому, и ты придёшь за своим пальто, а получишь чужое. Это и есть главная беда PID — переиспользование.

Номер палаты в больнице. Врач говорит «пациент из 314-й», а не «Иванов Иван». Номер палаты — то, чем оперирует система, а не то, кем человек является. Дежурная медсестра проверяет обход по номерам, а не по фамилиям.

Где ломается: палата за пациентом закреплена на неделю, и пока он не выписан, туда никого не положат. У PID нет такой инерции — освободившийся номер может быть выдан через секунду. И ещё: в больнице палата — место, а пациент существует независимо от неё. Процесса вне таблицы ядра не существует вовсе.

Номер заказа в кофейне. Тебе дают чек «заказ 47», бариста через пять минут кричит «сорок седьмой!». Пока заказ готовится, номер занят. Готово — номер свободен и завтра достанется кому-то ещё.

Где ломается: в кофейне между «заказ готов» и «клиент забрал» проходит время, и номер всё это время висит в системе. В Unix у этого промежутка есть точное имя — зомби-процесс: программа уже завершилась, но запись в таблице ещё держится, потому что родитель не забрал код возврата вызовом wait(). Если родитель никогда не спросит — «заказ» так и будет висеть на стойке, занимая номер. В кофейне такого не бывает, а в системе — сколько угодно, и именно так утекают PID.

Как это работает

Разберём жизненный цикл на примере того, что делает командная оболочка, когда ты набираешь ./collect.sh.

Шаг 1. Раздвоение. Оболочка вызывает fork(). Ядро делает копию процесса-оболочки — со всей памятью, открытыми файлами, переменными окружения. Теперь их два, они почти идентичны. Отличаются одним: возвращаемым значением.

fork() — функция, которая возвращает дважды

Ты вызываешь её один раз, а результат получают два процесса. Родителю она возвращает PID новорождённого ребёнка, а самому ребёнку — ноль. Это единственный момент, когда родитель узнаёт номер потомка; больше ниоткуда его получить нельзя. Ребёнок же свой номер узнаёт отдельным вызовом getpid(), а номер родителя — через getppid().

Отсюда классическая идиома в любом Unix-коде:

pid = fork()
если pid == 0:      # мы внутри ребёнка
    exec("collect.sh")   # стать скриптом сбора
если pid > 0:       # мы внутри родителя
    записать pid в файл collect.pid
    wait(pid)            # дождаться и забрать код возврата
если pid < 0:       # ядро отказало: кончились PID или память
    ошибка

Шаг 2. Превращение. Ребёнок вызывает exec() — и содержимое процесса подменяется: вместо копии оболочки в памяти оказывается интерпретатор скрипта. Но PID при этом не меняется. Это важная деталь: номер принадлежит процессу как «месту», а не выполняемому коду. Из-за этого ps может показать PID 61307 с именем collect_витки.sh, хотя минуту назад тот же 61307 был оболочкой.

Шаг 3. Жизнь. Пока процесс работает, с ним можно общаться только через PID — посылая сигналы (короткие пронумерованные уведомления):

Команда kill 61307 посылает SIGTERM, kill -9 61307 — SIGKILL. Слово «kill» вводит в заблуждение: вызов на самом деле означает «отправь сигнал», просто по умолчанию сигнал смертельный.

Шаг 4. Смерть и уборка. Процесс завершается и переходит в состояние зомби: тела нет, запись есть. Родитель вызывает wait(), получает код возврата (0 — успех, ненулевой — ошибка), и только тогда запись удаляется, а PID возвращается в пул. Если родитель умер раньше ребёнка, ребёнок становится сиротой и его немедленно усыновляет PID 1, который добросовестно вызывает wait() за всех.

А как проверить, жив ли процесс? Двумя способами, и разница между ними существенная:

kill -0 61307              # спросить у ядра: есть ли такой PID?
                           # сигнал 0 ничего не делает, только проверяет

pgrep -fl "collect.sh"     # найти процессы, чья командная строка
                           # содержит "collect.sh", и показать их PID

Первый способ быстрее, но отвечает на вопрос «занят ли этот номер», а не «жив ли мой процесс». Второй ищет по имени и командной строке — медленнее, зато отвечает именно на нужный вопрос. Флаг -f заставляет искать по всей командной строке, а не только по имени бинарника; -l — печатать найденное.

Гонка переиспользования PID

Классическая ошибка: скрипт запомнил PID=61307, ждёт минуту, делает kill -0 61307 — ядро отвечает «жив», скрипт рапортует «всё в порядке». А на самом деле твой сборщик упал полчаса назад, номер вернулся в пул, и 61307 сейчас — чей-то grep. На загруженной машине с потолком 32768 полный оборот счётчика занимает минуты. Именно из-за этой дыры в 2019 году в Linux и появился pidfd: файловый дескриптор держит ссылку на конкретный процесс, и после его смерти дескриптор становится невалидным, а не начинает указывать на постороннего. Пока pidfd не везде — проверяй по имени и командной строке (pgrep -f), а не по голому номеру.

Где встречается в обычной жизни

Где встречается в IT и бизнесе

Кто пользуется

Все Unix-подобные системы без исключения: Linux, macOS, все BSD, Android (это Linux), iOS (это BSD-родня). Это не «одна из технологий», а фундамент, на котором стоит всё остальное.

Порядки величин: Linux работает на 100% машин из списка TOP500 суперкомпьютеров (сплошным составом — с ноября 2017 года). Android — по разным оценкам порядка 3 миллиардов активных устройств. На каждом из них прямо сейчас существует таблица процессов, и в ней десятки-сотни записей с номерами.

Windows устроена иначе — там PID тоже есть, но главный способ сослаться на процесс не номер, а HANDLE (дескриптор). Это в определённом смысле честнее: дескриптор держит процесс «на учёте», и пока он открыт, номер не переиспользуется.

Отдельный случай — Erlang/BEAM (Ericsson, 1986). Там PID означает не процесс ОС, а собственный лёгкий процесс виртуальной машины: их можно держать миллионы на одной машине, и всё общение между ними — по этим идентификаторам. На этой модели построен WhatsApp, который на пике обслуживал сотни миллионов пользователей относительно небольшим парком серверов.

Альтернативы и конкуренты

pidfd (Linux 5.3+)
Плюсы: полностью снимает гонку переиспользования, сигнал уходит только тому процессу, на который дескриптор был открыт; интегрируется с poll/epoll — можно ждать смерти процесса как обычного события.
Минусы: только Linux и только достаточно свежий; требует переписывания кода; в шелл-скриптах напрямую недоступен.

cgroups (control groups, Linux)
Плюсы: оперирует группой процессов целиком — можно убить всё дерево разом, ограничить память и CPU, надёжно посчитать потребление; не разваливается, когда процесс порождает потомков.
Минусы: сложнее в настройке, требует прав; отвечает на вопрос «сколько ресурсов ест эта группа», а не «жив ли конкретный экземпляр».

Имена вместо номеров (Kubernetes, systemd-юниты)
Плюсы: systemctl status collect.service или имя пода в Kubernetes стабильно и человекочитаемо, переживает перезапуски, годится для логов и алертов.
Минусы: нужна дополнительная инфраструктура; между именем и реальным процессом стоит слой, который сам может врать или отставать.

Windows HANDLE
Плюсы: пока дескриптор открыт, номер не переиспользуется — гонки нет по конструкции.
Минусы: дескрипторы надо явно закрывать, иначе течёт уже другой ресурс; модель не переносима на Unix.

Когда НЕ стоит использовать

Связанные понятия

Литература и источники

Где встретилось у меня

Вчера почти весь день шла длинная фоновая задача на отдельной машине в локальной сети: скрипт по кругу обходил список объектов и складывал результаты, а рядом крутился второй, сторожевой, скрипт. Каждый час я просил дешёвого агента сходить по SSH и ответить на один вопрос — живы ли оба. Ответ каждый раз выглядел одинаково: два номера, отметка «жив», номер текущего круга и время последней записи в лог. Проверка делалась не по запомненному номеру, а поиском по имени команды — и это ровно тот случай, когда разница между двумя способами перестаёт быть теорией.

Краткое резюме