Learning Platform
Глоссарий Troubleshooting
Урок 04.01 · 22 мин
Начальный
ProcessPIDAddress Space/procfd

Анатомия процесса — PID, address space, fd-таблица

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

В этом уроке мы вскроем процесс и посмотрим, что внутри. Через /proc/[pid]/ — это окно в ядро Linux, которое показывает буквально каждое поле любого процесса в системе. После урока вы прочитаете ps -ef, top и любой вывод cat /proc/... как открытую книгу.


Программа vs процесс

Сначала разделим два понятия, которые путают начинающие:

  • Программа — это файл на диске. ELF-бинарь, например /usr/bin/python3. Статический набор инструкций и данных.
  • Процесс — это выполняющийся экземпляр программы. Динамическое существо в памяти с состоянием.

Один и тот же бинарь может породить много процессов. Запустите три раза python3 -i — получите три отдельных процесса с разными PID, разными виртуальными пространствами, разными состояниями. Они не знают друг о друге (пока вы их явно не соедините через IPC).

Программа vs процесс
/usr/bin/python3Программа: ELF-файл на диске. Статика. ~50 MB разбитых на секции (.text -- код, .data -- инициализированные данные, .rodata, etc)
процесс 1 (PID 100)Первый запуск: процесс с PID 100. Свой address space (4 GB виртуальной памяти). Своё состояние. Не знает про другие
процесс 2 (PID 200)Второй запуск той же программы: новый процесс с PID 200. Свой address space, отдельный от PID 100. .text страницы могут быть shared в физической памяти (copy-on-write)
процесс 3 (PID 300)Третий процесс. Каждый имеет свои переменные, свой stdin/stdout, свою heap-память

Программа загружается с диска в виртуальную память каждого процесса. Но физически .text страницы (read-only код) могут быть shared — в RAM одна копия, mapped в адресные пространства всех процессов. Это экономит память: если у вас 50 экземпляров Apache, в RAM один кусок httpd, а не 50.


Содержимое процесса: главные поля

Внутри ядра процесс представлен структурой task_struct (если читать исходники Linux). Она огромна — сотни полей. Для понимания основных нужно знать примерно 10. Их можно увидеть в /proc/[pid]/status:

cat /proc/self/status | head -30

Вывод (с пояснениями):

Name:    bash                  # имя процесса
Umask:   0022                  # umask
State:   S (sleeping)          # состояние процесса
Tgid:    12345                 # Thread Group ID (= PID для одного потока)
Ngid:    0                     # NUMA group ID
Pid:     12345                 # Process ID
PPid:    9000                  # Parent PID
TracerPid: 0                   # PID отладчика, если процесс трассируется
Uid:     1000  1000  1000  1000  # real / effective / saved / fsuid
Gid:     1000  1000  1000  1000  # то же для GID
FDSize:  256                   # размер fd table
Groups:  1000 27 130           # supplementary groups
NStgid:  12345                 # PID в namespaces
VmPeak:   24340 kB             # пиковый VSZ
VmSize:   24340 kB             # текущий VSZ
VmLck:        0 kB             # locked в RAM (mlock)
VmPin:        0 kB             # pinned
VmHWM:     4824 kB             # peak RSS
VmRSS:     4824 kB             # текущий RSS
RssAnon:   2400 kB             # anonymous pages (heap, stack)
RssFile:   2424 kB             # file-backed pages (code, mmap'ed)
RssShmem:     0 kB             # shared memory pages
VmData:    3496 kB             # private data + heap
VmStk:      132 kB             # stack
VmExe:      876 kB             # .text (executable)
VmLib:     2444 kB             # mapped libraries
VmPTE:       72 kB             # размер page tables
VmSwap:       0 kB             # в swap
Threads:    1                  # количество потоков
SigQ:       0/63468            # очередь сигналов
SigPnd:     0000000000000000   # pending signals
ShdPnd:     0000000000000000   # shared pending
SigBlk:     0000000000010000   # заблокированные сигналы
SigIgn:     0000000000384004   # игнорируемые
SigCgt:     000000004b813efb   # перехватываемые
CapInh:     0000000000000000   # inheritable capabilities
CapPrm:     0000000000000000   # permitted
CapEff:     0000000000000000   # effective
CapBnd:     0000003fffffffff   # bounding
...

Каждое поле — это срез состояния процесса. Разберём важные.

PID и PPID

PID (Process ID) — уникальный номер процесса в системе. Когда ядро создаёт процесс, оно даёт ему PID (минимально 1, максимум обычно 4194304, можно глянуть в /proc/sys/kernel/pid_max). PID-ы переиспользуются: когда процесс умирает, ядро может позднее выдать тот же PID новому процессу.

PPID (Parent PID) — PID процесса-родителя. Каждый процесс создан другим процессом (через fork). Исключение — init (PID 1), у него PPID 0. Когда родитель умирает раньше ребёнка, ребёнок наследует PPID 1 (или 1 ближайший subreaper) — это разбираем в уроке 03.

# PID вашего shell:
echo $$

# PPID -- кто запустил ваш shell:
ps -o ppid= -p $$

# Иерархия процессов:
pstree -p $$

UID и GID

Каждый процесс работает от имени какого-то пользователя. Это UID (User ID). В Linux есть 4 разных UID:

  • Real UID (RUID) — кто фактически запустил процесс.
  • Effective UID (EUID) — от чьего имени проверяются права. Обычно == RUID, но может быть выше при setuid-бинарях (например, sudo устанавливает EUID=0).
  • Saved UID — сохранённое старое значение, чтобы программа могла временно понизить и потом восстановить.
  • Filesystem UID — для проверки прав на файлы. Обычно == EUID.

Аналогично GID. В большинстве процессов все 4 числа одинаковые. Setuid-бинари (/usr/bin/sudo, /usr/bin/passwd) — интересные исключения.

# UID вашего процесса:
cat /proc/self/status | grep ^Uid

# UID и GID для всех процессов:
ps -eo pid,user,group,cmd | head

Состояние процесса

State — текущее состояние. Linux использует короткие буквы:

Состояния процесса в Linux
RRunning или Runnable. Процесс выполняется на CPU или ждёт в очереди готовых. Готов выполняться
SInterruptible Sleep. Спит, ждёт события (I/O, сигнал, futex). Может быть разбужен сигналом. Самое частое состояние
DUninterruptible Sleep. Спит, но не реагирует на сигналы. Обычно ждёт I/O от диска или другого устройства. kill не убивает! Только перезагрузка системы
TStopped. Остановлен сигналом SIGSTOP/SIGTSTP (ctrl-Z). Не выполняется до SIGCONT. Используется отладчиками и шеллом
tTracing stop. Остановлен под трассировкой (gdb, strace). Похоже на T, но другая семантика
ZZombie. Процесс завершился, но родитель ещё не сделал wait(). Структура в ядре сохраняется, чтобы родитель мог узнать exit code. Память освобождена
XDead. Процесс полностью удалён из системы. В ps вы никогда не увидите X -- к моменту опроса процесса уже нет
IIdle. Только для kernel threads, которые не делают работу. В user-space процессов не бывает

Lifecycle подробнее разбираем в уроке 03. Главное запомнить: D — знак того, что процесс ждёт что-то от железа и kill -9 не поможет (надо ждать, пока I/O завершится).

# Все состояния прямо сейчас:
ps -eo stat | sort | uniq -c | sort -rn
# Что-то вроде:
# 178 S
#  12 R
#   3 I
#   1 Z

Address space процесса

Каждый процесс имеет своё виртуальное адресное пространство — иллюзию своего компьютера с 128 TB памяти (на x86_64). Это разбираем подробно в M05, здесь — основы.

В адресном пространстве есть сегменты:

Адресное пространство процесса Linux
Stack (top)Стек: локальные переменные функций, return-адреса. Растёт сверху вниз (от высоких адресов к низким). Размер по умолчанию 8 MB (см. ulimit -s)
Mmap areaРегион для mmap() -- mapping файлов и shared libraries. Сюда mapping libc.so, libpython.so и т.д.
HeapКуча: динамически выделенная память (malloc/free). Растёт вверх от brk(). Питон-объекты, std::vector и пр. -- всё в heap
.bssBSS: неинициализированные глобальные переменные. Занимает место в виртуальной памяти, но в файле программы только размер -- страницы создаются по требованию заполненными нулями
.dataData: инициализированные глобальные переменные. Загружается из файла программы. Можно читать и писать
.rodataRead-only data: константы, string literals. Только чтение, попытка записи -> SIGSEGV. Может быть shared между процессами
.text (bottom)Код программы: машинные инструкции. Только чтение и исполнение, нельзя писать. Shared между всеми экземплярами программы

Можно увидеть это распределение для процесса:

cat /proc/self/maps

Вывод:

55c1a3d00000-55c1a3d04000 r--p 00000000 fd:00 1234567 /usr/bin/cat
55c1a3d04000-55c1a3d08000 r-xp 00004000 fd:00 1234567 /usr/bin/cat
55c1a3d08000-55c1a3d0b000 r--p 00008000 fd:00 1234567 /usr/bin/cat
55c1a3d0b000-55c1a3d0c000 r--p 0000a000 fd:00 1234567 /usr/bin/cat
55c1a3d0c000-55c1a3d0d000 rw-p 0000b000 fd:00 1234567 /usr/bin/cat
55c1a3e58000-55c1a3e79000 rw-p 00000000 00:00 0       [heap]
7fa1b8000000-7fa1b8024000 r--p 00000000 fd:00 7654321 /lib/x86_64-linux-gnu/libc.so.6
7fa1b8024000-7fa1b81b9000 r-xp 00024000 fd:00 7654321 /lib/x86_64-linux-gnu/libc.so.6
7fa1b81b9000-7fa1b820e000 r--p 001b9000 fd:00 7654321 /lib/x86_64-linux-gnu/libc.so.6
7fa1b820e000-7fa1b8212000 r--p 0020d000 fd:00 7654321 /lib/x86_64-linux-gnu/libc.so.6
7fa1b8212000-7fa1b8214000 rw-p 00211000 fd:00 7654321 /lib/x86_64-linux-gnu/libc.so.6
7fa1b8214000-7fa1b8221000 rw-p 00000000 00:00 0
7fa1b834b000-7fa1b834d000 rw-p 00000000 00:00 0
7fa1b836c000-7fa1b836d000 r--p 00000000 00:00 0       [vvar]
7fa1b836d000-7fa1b836f000 r-xp 00000000 00:00 0       [vdso]
7ffd8f3e3000-7ffd8f404000 rw-p 00000000 00:00 0       [stack]

Каждая строка — один сегмент памяти (VMA — Virtual Memory Area). Формат: адрес-адрес права смещение dev:inode путь.

Права: r (read), w (write), x (execute), p (private) / s (shared).

В этом выводе видно:

  • /usr/bin/cat — сам исполняемый файл, mmap’ed в адресное пространство. Видим 5 сегментов: один r—p (.rodata), один r-xp (.text), один r—p (relro), один r—p (другая часть relro), один rw-p (.data + .bss).
  • [heap] — куча. Анонимная (00:00 0 = не файл).
  • libc.so.6 — mapped libc.
  • [vvar], [vdso] — специальные регионы для vDSO (см. M11).
  • [stack] — стек, в самом верху.

fd-таблица: открытые файлы

Каждый процесс имеет таблицу файловых дескрипторов (file descriptor table). Это массив, где каждый индекс — открытый «файл» (файл, сокет, pipe, что угодно). Индексы начинаются с 0.

По умолчанию у каждого процесса открыты:

  • fd 0 — stdin (стандартный ввод).
  • fd 1 — stdout (стандартный вывод).
  • fd 2 — stderr (стандартный поток ошибок).

Когда вы делаете open(), socket(), pipe() — ядро выделяет следующий свободный fd (обычно 3, потом 4, и т.д.).

ls -la /proc/self/fd/

Вывод (для shell):

lrwx------ 1 user user 64 May 18 14:00 0 -> /dev/pts/0
lrwx------ 1 user user 64 May 18 14:00 1 -> /dev/pts/0
lrwx------ 1 user user 64 May 18 14:00 2 -> /dev/pts/0
lrwx------ 1 user user 64 May 18 14:00 255 -> /dev/pts/0

Что-то более интересное — для процесса Nginx или PostgreSQL:

# Найти PID nginx:
pgrep nginx | head -1
# 4321

# Открытые файлы:
sudo ls -la /proc/4321/fd/ | head
# lrwx------ 1 root root 64 May 18 14:00 0 -> /dev/null
# lrwx------ 1 root root 64 May 18 14:00 1 -> /dev/null
# lrwx------ 1 root root 64 May 18 14:00 2 -> /var/log/nginx/error.log
# lr-x------ 1 root root 64 May 18 14:00 3 -> /etc/nginx/nginx.conf
# lrwx------ 1 root root 64 May 18 14:00 4 -> 'socket:[12345]'
# lrwx------ 1 root root 64 May 18 14:00 5 -> 'socket:[12346]'
# ...

Видно: stdout/stderr идут в /dev/null или logfile, есть прочитанный конфиг, кучка сокетов (TCP-соединения с клиентами).


Окружение и аргументы

Каждый процесс получает при запуске:

  • Аргументы (argv) — список строк, как python3 script.py arg1 arg2.
  • Окружение (envp) — переменные окружения как PATH=/usr/bin:..., HOME=/home/user, ....

Они хранятся в стеке процесса и доступны через /proc/[pid]/cmdline (argv) и /proc/[pid]/environ (envp):

# Аргументы (NULL-разделённые):
cat /proc/self/cmdline | tr '\0' '\n'

# Окружение (тоже NULL-разделённое):
cat /proc/self/environ | tr '\0' '\n' | head -10

# Аналогично можно для любого PID:
sudo cat /proc/<PID>/cmdline | tr '\0' ' '

Внимание: переменные окружения это снимок на момент запуска процесса. Если вы делаете export FOO=bar в shell, это новое значение увидят только дочерние процессы, запущенные после. Уже работающий процесс не изменится.


Working directory и executable

Процесс «знает», где он запущен и какой бинарь его представляет. Это через симлинки:

# Текущая директория процесса:
ls -la /proc/self/cwd
# lrwxrwxrwx 1 user user 0 May 18 14:00 /proc/self/cwd -> /home/user

# Исполняемый файл:
ls -la /proc/self/exe
# lrwxrwxrwx 1 user user 0 May 18 14:00 /proc/self/exe -> /usr/bin/cat

Полезно для дебага: «откуда запущен этот процесс?», «какой именно бинарь работает (вдруг symlink или несколько версий)?».


Резюме: что в процессе

Полная карта процесса
IdentificationPID, PPID, TID, namespace IDs, exit status. Кто это и в каких отношениях с другими
OwnershipUID/GID/groups, capabilities. От кого и с какими правами
StateR/S/D/T/Z, priority (PR), nice (NI), CPU usage, threads count, signal state
Address spaceКарта виртуальной памяти: text, data, heap, stack, mmap. См. /proc/[pid]/maps
fd tableОткрытые файлы, сокеты, pipes. /proc/[pid]/fd. У серверных процессов их может быть тысячи
Args + envАргументы запуска и окружение. /proc/[pid]/cmdline и /proc/[pid]/environ
Working dir + exeТекущая директория, бинарь. /proc/[pid]/cwd и /proc/[pid]/exe
SignalsPending signals, blocked, handlers. /proc/[pid]/status (Sig*) и через прочие syscalls
Resource usageCPU time, memory peak, I/O bytes, page faults. /proc/[pid]/stat, /proc/[pid]/io

Каждое поле имеет своё отражение в /proc/[pid]/. Это интерфейс, через который вся observability (top, ps, htop, lsof) получает данные. Зная этот интерфейс, вы можете дебажить системы, не запуская специальных тулзов — просто cat и ls -la.

top, htop, btop: живой мониторинг процессов docker stats и top

Реальный пример: разбор ps -ef

Знакомый вывод ps -ef:

UID    PID  PPID  C STIME TTY    TIME     CMD
root     1     0  0 May14 ?      00:00:34 /sbin/init splash
root     2     0  0 May14 ?      00:00:00 [kthreadd]
root   123     2  0 May14 ?      00:00:00 [migration/0]
levo  9000  4567  0 May18 pts/0  00:00:01 -bash
levo  9876  9000  0 14:30 pts/0  00:00:00 ps -ef

Расшифровка:

  • UID — имя пользователя (или числовой UID, если имени нет в /etc/passwd).
  • PID — ID процесса.
  • PPID — ID родителя. Init — 1, его родитель 0 (специальный псевдо-родитель).
  • C — CPU usage percent (исторически усреднение, в современных ядрах часто 0).
  • STIME — время запуска (если давно — дата, недавно — время).
  • TTY — терминал процесса (? значит без терминала, типа daemon).
  • TIME — CPU time с момента запуска (HH:MM:SS).
  • CMD — команда. В квадратных скобках — kernel threads (например, [kthreadd], [migration/0]).

Теперь, когда вы знаете, что внутри процесса — каждое поле имеет смысл.

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

Попробуй сам

# 1. Посмотреть всё про ваш shell:
cat /proc/self/status | head -25

# 2. Иерархия родства:
pstree -p $$

# 3. Карта памяти вашего shell:
cat /proc/self/maps | head -10

# 4. Открытые файлы вашего shell:
ls -la /proc/self/fd/

# 5. Окружение вашего shell:
cat /proc/self/environ | tr '\0' '\n' | head

# 6. Что показывает ps для вашего shell с полными полями:
ps -o pid,ppid,uid,gid,user,stat,pri,ni,vsz,rss,sz,wchan,cmd -p $$

# 7. Найти все процессы с состоянием D (uninterruptible):
ps -eo stat,pid,cmd | awk '$1 ~ /^D/ {print}'

# 8. Посмотреть процессы с самым большим RSS:
ps -eo pid,user,rss,vsz,cmd --sort=-rss | head -10

# 9. Сколько threads у процесса (например, firefox):
pgrep firefox | head -1 | xargs -I{} cat /proc/{}/status | grep Threads

Проверка знанийKnowledge check
Junior смотрит на 'ps -ef' и видит, что один из процессов имеет PPID=1 (init), хотя он явно был запущен НЕ из init. Почему?
ОтветAnswer
Это очень важный сценарий. PPID=1 означает, что родитель процесса умер раньше ребёнка, и init (или ближайший subreaper) усыновил ребёнка. Такие процессы называются 'orphaned' (осиротевшие). Пример: вы из shell запускаете 'long_running_script.py &' (в фоне). Потом закрываете shell. Что с процессом? Shell умер, его PID 12345 свободен. Но скрипт продолжает работать. Его PPID был 12345, но 12345 теперь нет (или взят новым процессом). Linux передаёт сиротский процесс на воспитание PID 1 (init/systemd). Поэтому в ps теперь PPID=1. Это 'feature', не баг. Каждый процесс должен иметь родителя, чтобы кто-то мог сделать wait() и забрать его exit code. Если родителя нет -- система рискует получить zombie. Init умеет автоматически принимать сирот и делать wait() на них. Демоны (daemon-процессы) часто специально становятся сиротами: процесс делает fork(), родитель выходит, ребёнок продолжает работать с PPID=1. Это техника называется 'daemonization' -- так делают традиционные Unix-демоны, чтобы отвязаться от запустившего их shell. В современном Linux есть концепция 'subreaper' -- процесс может объявить себя через prctl(PR_SET_CHILD_SUBREAPER), и тогда сироты от его детей будут переусыновлены ему, а не init. Это используется в supervision-системах (systemd, supervisord, runit) -- они хотят быть 'init' для своих сервисов. Если вы видите PPID=1 у процесса и НЕ ожидали -- скорее всего, родитель упал. Проверьте логи, может это симптом более крупной проблемы.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Чем процесс отличается от программы?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4