Анатомия процесса — PID, address space, fd-таблица
«Процесс» — одно из самых важных понятий в ОС. Когда вы запускаете программу, ядро создаёт процесс. Когда программа завершается, процесс уничтожается. Между этими моментами процесс — сложная структура внутри ядра с десятками полей: PID, владелец, состояние, виртуальная память, открытые файлы, сигналы, переменные окружения.
В этом уроке мы вскроем процесс и посмотрим, что внутри. Через /proc/[pid]/ — это окно в ядро Linux, которое показывает буквально каждое поле любого процесса в системе. После урока вы прочитаете ps -ef, top и любой вывод cat /proc/... как открытую книгу.
Программа vs процесс
Сначала разделим два понятия, которые путают начинающие:
- Программа — это файл на диске. ELF-бинарь, например
/usr/bin/python3. Статический набор инструкций и данных. - Процесс — это выполняющийся экземпляр программы. Динамическое существо в памяти с состоянием.
Один и тот же бинарь может породить много процессов. Запустите три раза python3 -i — получите три отдельных процесса с разными PID, разными виртуальными пространствами, разными состояниями. Они не знают друг о друге (пока вы их явно не соедините через IPC).
Программа загружается с диска в виртуальную память каждого процесса. Но физически .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 использует короткие буквы:
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, здесь — основы.
В адресном пространстве есть сегменты:
Можно увидеть это распределение для процесса:
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 или несколько версий)?».
Резюме: что в процессе
Каждое поле имеет своё отражение в /proc/[pid]/. Это интерфейс, через который вся observability (top, ps, htop, lsof) получает данные. Зная этот интерфейс, вы можете дебажить системы, не запуская специальных тулзов — просто cat и ls -la.
Реальный пример: разбор 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