PID (идентификатор процесса)
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, систему, где работа с процессами стала неприлично простой:
fork()— «раздвоиться». Идея заимствована из статьи Мелвина Конвея 1963 года «A Multiprocessor System Design» (того самого Конвея, чей закон про организации и архитектуру). Томпсон реализовал её в первой версии Unix.exec()— «стать другой программой, оставшись собой».kill(pid, сигнал)— «послать вот этому номеру сообщение».wait()— «дождаться, пока ребёнок закончит, и забрать его код возврата».
В First Edition Unix (1971) эти вызовы уже описаны в man-страницах, вместе с утилитой ps. Конструкция оказалась настолько удачной, что за пятьдесят пять лет её не переделали — только достроили.
Три вехи после:
- POSIX.1 (IEEE Std 1003.1, 1988) — интерфейс работы с процессами стандартизировали. Тип
pid_t, гарантии уникальности, поведение сигналов. С этого момента код, работающий с PID, стал переносимым между Unix-подобными системами. - PID namespaces в Linux 2.6.24 (январь 2008) — один и тот же процесс теперь может иметь разные PID в разных «пространствах имён». Внутри контейнера он PID 1, снаружи — какой-нибудь 24817. Без этой возможности не было бы ни Docker (2013), ни Kubernetes в нынешнем виде.
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 — нормальные числа для машины, которая давно работает и уже успела запустить и закрыть десятки тысяч процессов.
Полезно развести несколько похожих вещей:
- Программа vs процесс. Программа — файл на диске. Процесс — её запущенный экземпляр. Открыл два окна браузера — одна программа, много процессов, у каждого свой PID.
- PID vs PPID. PPID (parent PID) — номер того, кто тебя породил. Процессы в Unix образуют дерево: у каждого ровно один родитель, а корень — PID 1.
- PID vs TID. Внутри процесса может быть много потоков (threads). У них свои идентификаторы, TID. В Linux это разные пространства номеров, хоть и живущие в одной таблице — источник вечной путаницы.
- PID vs PGID и SID. Процессы объединяются в группы (process group) и сессии (session) — чтобы Ctrl+C в терминале убивал не одну команду, а весь конвейер
cat file | grep x | sort. У группы свой номер, обычно равный PID лидера. - PID 0 и PID 1. Ноль — служебный, за ним прячется само ядро (планировщик, исторически «swapper»). Единица — первый пользовательский процесс,
init: на современном Linux это чаще всего systemd (Леннарт Поттеринг, 2010), на macOS — launchd (Apple, 2005). Убить PID 1 нельзя обычными средствами: без него система не живёт.
Ключевое свойство, которое и делает 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 — посылая сигналы (короткие пронумерованные уведомления):
SIGTERM(15) — «заверши работу по-человечески, прибери за собой». Процесс может его перехватить.SIGKILL(9) — «умри немедленно». Перехватить нельзя, обрабатывает само ядро.SIGHUP(1) — исторически «связь с терминалом оборвалась», сегодня чаще «перечитай конфиг». Так nginx перезагружает настройки без остановки сайта.
Команда 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), а не по голому номеру.
Где встречается в обычной жизни
- «Программа не отвечает» → Завершить принудительно. И в Диспетчере задач Windows, и в «Принудительном завершении» macOS под кнопкой скрывается ровно одно: найти PID и послать по нему сигнал. В Диспетчере задач колонку PID можно включить прямо в настройках вкладки «Подробности».
- «Порт 8080 уже занят». Стандартный сценарий:
lsof -i :8080показывает PID занявшего,kill <PID>его освобождает. Половина «мистических» проблем при запуске локальных серверов лечится этими двумя командами. - Телефон закрыл приложение в фоне. Android при нехватке памяти запускает механизм, который выбирает жертв по приоритету и убивает их — по PID. Поэтому карта, свёрнутая на полчаса, при возврате перезагружается с нуля.
- Игра зависла на весь экран. Ctrl+Alt+Del или три пальца — это способ добраться до списка PID, когда обычный интерфейс перестал отвечать.
- Полоска «приложение использует микрофон». Система знает, какой именно процесс держит устройство — потому что доступ выдан не «программе Zoom», а конкретному PID.
Где встречается в IT и бизнесе
- Мониторинг долгих задач. Любой ночной сбор данных, миграция базы, обучение модели — вопрос «оно ещё идёт или уже сдохло?» решается либо через PID, либо через сторожевой процесс, который PID проверяет.
- Менеджеры служб. systemd, launchd, supervisor, PM2 делают одно и то же: запускают, запоминают PID, следят, что процесс жив, перезапускают при падении. Вся «автоматическая надёжность» сервера сводится к этому циклу.
- Перезагрузка конфигурации без простоя. nginx кладёт свой номер в
/var/run/nginx.pid, и командаkill -HUP $(cat /var/run/nginx.pid)заставляет его перечитать настройки, не разрывая текущие соединения. Ноль секунд даунтайма — на одном сигнале. - Защита от двойного запуска. PID-файл + блокировка не дают cron-задаче запуститься второй раз, пока первая ещё работает. Без этого две копии одного сборщика начинают писать в один файл и портить данные.
- Контейнеры. В Docker процесс внутри контейнера видит себя как PID 1 — и наследует обязанность «усыновлять» сирот. Приложения, не рассчитанные на роль init, этого не делают, и контейнер постепенно зарастает зомби. Отсюда флаг
docker run --init, подставляющий крошечный корректный PID 1.
Кто пользуется
Все 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.
Когда НЕ стоит использовать
- Как идентификатор в базе данных, логах или отчётах. PID не уникален во времени: за неделю один и тот же номер будет у сотни разных программ. Строка «ошибка в процессе 61307» через месяц не значит ничего. Логируй имя задачи, время запуска и собственный уникальный идентификатор — а PID клади рядом как справочную деталь.
- Как проверку живости через большой промежуток времени. Между «запомнил номер» и «проверил номер» процесс может умереть, а номер — достаться другому. Чем дольше пауза и чем загруженнее машина, тем выше шанс поверить в ложную «жизнь». Проверяй по имени и командной строке, либо по свежести лог-файла, либо и так и так.
- Как элемент безопасности. PID предсказуем (счётчик увеличивается на единицу) и виден всем пользователям машины через
ps. Строить на нём проверку «мне пишет именно доверенный процесс» нельзя — для этого есть права доступа, сокеты с проверкой владельца иSO_PEERCRED.
Связанные понятия
- PPID (parent process ID) — номер родителя; из PPID складывается дерево процессов, видимое командой
pstree. - Зомби-процесс — завершившийся процесс, чью запись родитель ещё не забрал вызовом
wait(); занимает номер, но не память. - Процесс-сирота — процесс, чей родитель умер раньше него; немедленно усыновляется PID 1.
- Сигнал — короткое пронумерованное уведомление процессу (SIGTERM, SIGKILL, SIGHUP); единственный штатный способ вмешаться в чужую программу извне.
- PID namespace — изоляция пространства номеров, благодаря которой процесс в контейнере видит себя как PID 1; основа контейнеризации.
- Сторожевой процесс (watchdog) — отдельная программа, которая периодически проверяет живость основной и перезапускает её при падении; ровно этим занимался вчерашний
guard.sh.
Литература и источники
- Maurice J. Bach, «The Design of the UNIX Operating System», 1986 (en; есть русский перевод «Архитектура операционной системы UNIX»). Классика: устройство таблицы процессов разобрано до структур данных. Книга старая, но в этой части ничего не устарело.
- W. Richard Stevens, Stephen A. Rago, «Advanced Programming in the UNIX Environment», 3-е издание, 2013 (en). Главы 8–10 — про управление процессами и сигналы. Практический стандарт для тех, кто пишет такой код руками.
- Remzi H. и Andrea C. Arpaci-Dusseau, «Operating Systems: Three Easy Pieces» (en). Бесплатный университетский учебник, выложен целиком авторами:
pages.cs.wisc.edu/~remzi/OSTEP/. Раздел про виртуализацию CPU объясняет процессы с нуля и очень человеческим языком. - Linux man-pages:
man 2 fork,man 2 kill,man 7 signal,man 5 proc. Первоисточник, читается онлайн наman7.org. Именно там описано, что означает каждое поле в/proc/<PID>/. - Wikipedia, «Process identifier»:
https://en.wikipedia.org/wiki/Process_identifier. Короткая обзорная статья с разбором отличий между системами. В русской Wikipedia соответствующая статья называется «Идентификатор процесса». - Melvin E. Conway, «A Multiprocessor System Design», 1963. Та самая работа, где предложена идея
fork. Искать по названию вместе с «Conway 1963 fork» — текст лежит в открытом доступе в архивах ACM.
Где встретилось у меня
Вчера почти весь день шла длинная фоновая задача на отдельной машине в локальной сети: скрипт по кругу обходил список объектов и складывал результаты, а рядом крутился второй, сторожевой, скрипт. Каждый час я просил дешёвого агента сходить по SSH и ответить на один вопрос — живы ли оба. Ответ каждый раз выглядел одинаково: два номера, отметка «жив», номер текущего круга и время последней записи в лог. Проверка делалась не по запомненному номеру, а поиском по имени команды — и это ровно тот случай, когда разница между двумя способами перестаёт быть теорией.
Краткое резюме
- PID — временный номер запущенной программы в таблице процессов ядра. Не имя, не адрес, а номерок в гардеробе.
- Он уникален только сейчас. После смерти процесса номер возвращается в оборот и может достаться кому угодно — на этом ломается половина самодельных проверок «жив ли сервис».
- Всё общение с чужой программой снаружи идёт через PID и сигналы:
SIGTERM— вежливо,SIGKILL— насмерть,SIGHUP— перечитай конфиг. - Дерево процессов держится на паре PID/PPID: у каждого один родитель, корень — PID 1 (systemd или launchd), он же усыновляет сирот и убирает зомби.
- Практическое правило: для мониторинга проверяй по имени команды (
pgrep -f) и по свежести лога, а PID пиши в отчёт как справку, а не как источник истины.