Раздел 04 — Процессы и потоки

Что такое процесс, как он рождается, живёт и завершается — и как администратору их найти и остановить

Прогресс курса Раздел 4 из 12

Что вы освоите в этом разделе

6 академических часов: 2 теории, 4 практики. Практика — в ваших ВМ Windows 11 и Debian 13. Номера процессов (PID) у вас будут другими: подставляйте свои.
01

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

Программа — файл на диске: notepad.exe, /usr/bin/ls. Она ничего не делает, пока её не запустили. Процесс — запущенная программа вместе со всем, что ОС ей выделила: память, открытые файлы, права, время процессора. Одну программу можно запустить несколько раз — получится несколько независимых процессов.

О каждом процессе ядро хранит запись — блок управления процессом (PCB, process control block). В Windows это структура EPROCESS, в Linux — task_struct. В ней:

ЧтоЗачем
Идентификатор (PID) и родитель (PPID)Найти процесс, построить дерево
СостояниеВыполняется, готов, ждёт
Регистры процессора и счётчик командПродолжить с того же места после паузы
Описание памятиКакие страницы принадлежат процессу (раздел 07)
Открытые файлы и устройстваДескрипторы, с которыми работают read и write
Владелец и праваЧто процессу разрешено (раздел 10)
Приоритет и учёт времениДля планировщика (раздел 05)

Часть PCB Linux показывает в файле /proc/PID/status. Запустим процесс sleep в фоне (символ &) и посмотрим на него:

$ sleep 300 &
[1] 2684809
$ ps -o pid,ppid,stat,etime,comm -p 2684809
    PID    PPID STAT     ELAPSED COMMAND
2684809 2684790 S          00:00 sleep
$ grep -E '^(Name|State|PPid|Threads)' /proc/2684809/status
Name:	sleep
State:	S (sleeping)
PPid:	2684790
Threads:	1

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

$ ps -e --no-headers | wc -l
137
$ ps -eL --no-headers | wc -l
395
PS> (Get-Process).Count
147
PS> (Get-Process | ForEach-Object Threads).Count
1441

Потоков в несколько раз больше, чем процессов: почти каждый процесс многопоточный.

02

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

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

СостояниеЧто значитКуда переходит
НовыйПроцесс создаётся: ядро заводит PCB, выделяет память→ Готов
ГотовМожет работать, ждёт свободное ядро→ Выполняется, когда планировщик его выбрал
ВыполняетсяЕго команды исполняет процессор→ Готов (вытеснен, кончился квант); → Ожидает (запросил ввод-вывод); → Завершён
Ожидает (блокирован)Ждёт события: данных с диска, нажатия клавиши, сетевого пакета, таймера→ Готов, когда событие произошло
ЗавершёнОтработал; ядро освобождает ресурсы—

Из «Ожидает» процесс не может сразу перейти в «Выполняется»: сначала событие делает его готовым, а потом планировщик решает, когда дать ему процессор.

В Linux состояние — первая буква столбца STAT в ps: R — выполняется или готов, S — ожидает (прерываемый сон), D — ожидает ввода-вывода, прервать нельзя, T — остановлен, Z — зомби, I — простаивающий поток ядра. Посчитаем:

$ ps -eo stat --no-headers | cut -c1 | sort | uniq -c
     46 I
      1 R
     92 S

В Windows планируются не процессы, а потоки, и состояние у каждого потока своё: Running — выполняется, Ready — готов, Standby — выбран следующим на ядро, Wait — ожидает, Initialized — создаётся, Terminated — завершён:

PS> Get-Process | ForEach-Object Threads | Group-Object ThreadState | Select-Object Count, Name
Count Name
----- ----
 1446 Wait
    3 Running
    2 Standby
    2 Initialized

В обеих системах картина одна: выполняется единица процентов, остальные ждут. Это нормально — большую часть времени программы ждут пользователя, диск или сеть. Поэтому процессор свободного компьютера загружен на 0–5 %.

Много процессов в состоянии D в Linux — признак проблемы с диском или сетевой папкой: процессы застряли в ожидании ввода-вывода, и даже kill -9 их не завершит, пока устройство не ответит.
03

Как создаётся процесс

Новый процесс всегда создаёт другой процесс — родитель. Проводник создаёт процесс Блокнота, когда вы щёлкаете по ярлыку; оболочка — процесс ls, когда вы вводите команду.

Посмотрим, как bash выполняет команду ls:

$ strace -f -o trace.txt -e trace=clone,execve,wait4,exit_group bash -c 'ls > /dev/null; echo готово'
готово
$ cat trace.txt
2684828 execve("/usr/bin/bash", ["bash", "-c", "ls > /dev/null; echo \320\263\320\276\321\202\320\276\320\262\320"...], 0x7ffcf94f6ab8 /* 18 vars */) = 0
2684828 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fbcfc5a7e50) = 2684829
2684828 wait4(-1,  <unfinished ...>
2684829 execve("/usr/bin/ls", ["ls"], 0x558e1e2eee70 /* 18 vars */) = 0
2684829 exit_group(0)                   = ?
2684829 +++ exited with 0 +++
2684828 <... wait4 resumed>[{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 2684829
2684828 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2684829, si_uid=1000, si_status=0, si_utime=0, si_stime=1 /* 0.01 s */} ---
2684828 wait4(-1, 0x7ffc1db98ad0, WNOHANG, NULL) = -1 ECHILD (Нет дочерних процессов)
2684828 exit_group(0)                   = ?
2684828 +++ exited with 0 +++

Номера в начале строк — PID процесса, сделавшего вызов. bash (номер в первых строках) сделал clone и получил потомка; потомок выполнил execve("/usr/bin/ls") и стал программой ls; родитель тем временем ждал в wait4. Потомок завершился (exit_group(0)), ядро послало родителю сигнал SIGCHLD, и wait4 вернул код завершения 0 — «успешно». Затем bash сам вывел «готово» и завершился.

Зачем два шага? Между fork и exec потомок может настроить себе окружение: перенаправить вывод в файл, сменить каталог, понизить права. Так оболочка выполняет ls > файл.

Зомби. Завершившийся процесс не исчезает сразу: ядро хранит его код завершения, пока родитель не заберёт его вызовом wait. Процесс в таком состоянии называют зомби (Z). Покажем это программой на Python: потомок завершается сразу, а родитель забирает его код только через секунду:

$ cat zombie.py
import os, time
pid = os.fork()
if pid == 0:
    print("потомок", os.getpid(), "завершается")
    os._exit(0)
time.sleep(1)
os.system(f"ps -o pid,ppid,stat,comm -p {pid}")
os.waitpid(pid, 0)
print("родитель забрал код возврата потомка")
os.system(f"ps -o pid,ppid,stat,comm -p {pid}")
$ python3 zombie.py
    PID    PPID STAT COMMAND
2684854 2684853 Z    python3
    PID    PPID STAT COMMAND
родитель забрал код возврата потомка

Пока родитель не вызвал waitpid, потомок был зомби; после — исчез совсем. Зомби не занимает памяти и процессора, только строку в таблице процессов. Но если программа с ошибкой порождает зомби тысячами, таблица заканчивается и новые процессы не создаются. Лечат это перезапуском родителя: его потомков-сирот «усыновляет» процесс номер 1, а он забирает коды сразу.

04

Дерево процессов

У каждого процесса есть родитель, поэтому процессы образуют дерево. В Linux его рисует pstree. Ключ -s показывает цепочку предков — от кого произошла оболочка, в которой мы работаем:

$ pstree -A -s 2684790
systemd---sshd---sshd-session---sshd-session---bash---bash-+-bash---pstree
                                                           |-sed
                                                           `-sleep
$ pstree -A -p 2535644
postgres(2535644)-+-postgres(2535645)
                  |-postgres(2535646)
                  |-postgres(2535648)
                  |-postgres(2535649)
                  `-postgres(2535650)

Цепочка читается слева направо: systemd (PID 1) запустил службу SSH, она — сеанс подключения, сеанс — оболочку bash, а оболочка — pstree. Сервер СУБД PostgreSQL — главный процесс и его рабочие процессы-потомки.

В Windows родитель хранится в свойстве ParentProcessId. Функция ниже поднимается от процесса к родителям, пока они есть:

PS> function Parents($id) { while ($id) { $q = Get-CimInstance Win32_Process -Filter "ProcessId=$id"; if (-not $q) { break }; "{0,6}  {1}" -f $q.ProcessId, $q.Name; $id = $q.ParentProcessId } }
PS> Parents 968
   968  svchost.exe
   836  services.exe
   700  wininit.exe

Это та цепочка загрузки из раздела 02: wininit.exe запустил диспетчер служб services.exe, а он — svchost.exe, в котором работают службы. Выше wininit подняться нельзя: его создал smss.exe, который завершился после загрузки.

Разница с Linux: в Windows связь «родитель — потомок» почти ничего не значит после запуска. Завершение родителя не влияет на потомков, а номер умершего родителя может достаться новому процессу. Поэтому ParentProcessId — подсказка, а не гарантия.

05

Потоки

Поток — последовательность команд, которую планировщик ставит на ядро процессора. В каждом процессе есть хотя бы один поток; потоки одного процесса делят его память и открытые файлы, а свои у каждого — только регистры и стек.

ПроцессПоток
ПамятьСвоя, другие процессы её не видятОбщая с потоками своего процесса
Создание и переключениеДороже: новое адресное пространствоДешевле
ОшибкаПадает только этот процессПадает весь процесс со всеми потоками
Обмен даннымиЧерез ОС: файлы, каналы, сокетыНапрямую через общую память — быстро, но нужна синхронизация (раздел 06)

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

Даже у консольной программы ping в Windows несколько потоков — служебные потоки создаёт сама система. Почти все они ждут:

PS> $p = Start-Process ping -ArgumentList "-n 60 127.0.0.1" -WindowStyle Hidden -PassThru
PS> $p.Id
3932
PS> Get-Process -Id 3932 | Select-Object Id, ProcessName, StartTime, @{n='Потоков'; e={$_.Threads.Count}}
  Id ProcessName StartTime           Потоков
  -- ----------- ---------           -------
3932 PING        31.10.2026 10:21:35       5
PS> Get-CimInstance Win32_Process -Filter 'ProcessId=3932' | Select-Object ProcessId, ParentProcessId, CommandLine
ProcessId ParentProcessId CommandLine
--------- --------------- -----------
     3932            1168 "C:\WINDOWS\system32\PING.EXE" -n 60 127.0.0.1
PS> (Get-Process -Id 3932).Threads | Select-Object Id, ThreadState, WaitReason
  Id ThreadState     WaitReason
  -- -----------     ----------
5312        Wait ExecutionDelay
8124        Wait   EventPairLow
5272        Wait   EventPairLow
9384        Wait   EventPairLow
9872        Wait   EventPairLow

В Linux поток — тоже задача ядра со своим номером; потоки процесса перечислены в /proc/PID/task, а их число — столбец NLWP:

$ ps -o pid,nlwp,comm -C dockerd,containerd,cron
    PID NLWP COMMAND
    579    1 cron
1556917   12 containerd
1601490   10 dockerd
$ ls /proc/$(pgrep -o dockerd)/task | head -4
1601490
1601491
1601493
1601494

Номер первого потока совпадает с PID процесса — это его главный поток.

06

Инструменты: найти процесс

Диспетчер задач (Ctrl+Shift+Esc): вкладка «Процессы» группирует по программам, вкладка «Подробности» показывает каждый процесс с PID, состоянием и пользователем. Щелчок правой кнопкой по заголовку таблицы → «Выбрать столбцы» добавляет «Потоки», «Дескрипторы», «Командная строка». Более подробный инструмент — Монитор ресурсов (resmon): какие файлы и сетевые соединения у процесса.

В PowerShell — Get-Process. Шесть процессов, занимающих больше всего памяти:

PS> Get-Process | Sort-Object WorkingSet64 -Descending | Select-Object -First 6 ProcessName, Id, @{n="Память, МБ"; e={[math]::Round($_.WorkingSet64/1MB)}}, @{n="Потоков"; e={$_.Threads.Count}}
ProcessName               Id Память, МБ Потоков
-----------               -- ---------- -------
explorer                3196        266      65
MsMpEng                 3144        198      33
powershell              8784        162      11
StartMenuExperienceHost 2464        146      16
msedge                  2908        124      69
msedgewebview2          1068        121      52

В Windows много процессов svchost.exe — в каждом работает одна или несколько служб. Какие именно, показывает tasklist /svc; без него не понять, какую службу «держит» нагруженный svchost:

PS> tasklist /fi "imagename eq svchost.exe" /svc | Select-Object -First 8
Имя образа                     PID Службы
========================= ======== ============================================
svchost.exe                    968 BrokerInfrastructure, DcomLaunch, PlugPlay,
                                   Power, SystemEventsBroker
svchost.exe                    564 RpcEptMapper, RpcSs
svchost.exe                    880 LSM
svchost.exe                   1152 BDESVC

В Linux то же показывает top: в шапке — время работы, нагрузка, число процессов по состояниям, память; ниже — процессы, отсортированные по загрузке процессора. В режиме диалога top обновляется каждые 3 секунды; q — выход, M — сортировка по памяти, k — завершить процесс.

$ top -b -n 1 | head -12
top - 10:21:38 up 66 days, 44 min, 19 users,  load average: 0,00, 0,02, 0,00
Tasks: 137 total,   1 running, 136 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0,0 us,  0,0 sy,  0,0 ni,100,0 id,  0,0 wa,  0,0 hi,  0,0 si,  0,0 st 
MiB Mem :   3921,8 total,    561,4 free,    691,6 used,   2995,5 buff/cache     
MiB Swap:   1722,0 total,    868,8 free,    853,2 used.   3230,2 avail Mem 

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
      1 root      20   0   24504   9432   5948 S   0,0   0,2   4:38.58 systemd
      2 root      20   0       0      0      0 S   0,0   0,0   0:02.79 kthreadd
      3 root      20   0       0      0      0 S   0,0   0,0   0:00.00 pool_wo+
      4 root       0 -20       0      0      0 I   0,0   0,0   0:00.00 kworker+
      5 root       0 -20       0      0      0 I   0,0   0,0   0:00.00 kworker+
ЗадачаWindowsLinux
Список процессовGet-Process, tasklistps -ef, ps aux
Нагрузка в реальном времениДиспетчер задач, resmontop, htop
Найти по имениGet-Process -Name explorerpgrep -a sshd
Родитель и деревоGet-CimInstance Win32_Processpstree, ps -o ppid
Командная строка процессастолбец CommandLinecat /proc/PID/cmdline, ps -o args
07

Завершение процессов и сигналы

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

В Windows taskkill /pid N без ключей посылает окнам программы сообщение «закрыться» — как щелчок по крестику. У ping, запущенного без окна, закрывать нечего, поэтому вежливо не получится; ключ /f завершает принудительно:

PS> taskkill /pid 3932
Ошибка: Не удается завершить процесс с идентификатором 3932.
Причина: Данный процесс может быть прерван только насильственно ( с  параметром /f ).
PS> taskkill /pid 3932 /f
Успешно: Процесс, с идентификатором 3932, успешно завершен.
PS> Get-Process -Id 3932
Get-Process : Не удается найти процесс с идентификатором процесса 3932.

В PowerShell то же делает Stop-Process -Id N; он сразу завершает принудительно и для чужих процессов спрашивает подтверждения.

В Linux процессу посылают сигнал — короткое уведомление от ядра. Программа может перехватить сигнал и обработать его по-своему, кроме SIGKILL и SIGSTOP:

СигналНомерЧто делаетКак послать
SIGTERM15Вежливо просит завершиться — по умолчанию у killkill PID
SIGINT2Прерывание с клавиатурыCtrl+C
SIGKILL9Уничтожить немедленно, перехватить нельзяkill -9 PID
SIGSTOP / SIGCONT19 / 18Приостановить и продолжитьkill -STOP, Ctrl+Z (близкий SIGTSTP)
SIGHUP1Терминал закрыт; службы по нему перечитывают настройкиkill -HUP PID
$ kill -STOP 2684809; ps -o pid,stat,comm -p 2684809
    PID STAT COMMAND
2684809 T    sleep
$ kill -CONT 2684809; ps -o pid,stat,comm -p 2684809
    PID STAT COMMAND
2684809 S    sleep
$ kill 2684809; sleep 0.2; ps -o pid,stat,comm -p 2684809
    PID STAT COMMAND
$ kill -l | head -4
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP

После SIGSTOP состояние стало T, после SIGCONT — снова S, после SIGTERM процесса больше нет: ps вывел только заголовок.

Принудительное завершение (taskkill /f, kill -9) не даёт программе сохранить данные и удалить временные файлы — база данных после этого может восстанавливаться долго. Сначала вежливо, принудительно — только если процесс не отвечает. Системные процессы Windows (csrss.exe, wininit.exe) завершать нельзя: система аварийно остановится с синим экраном.

Ключевые выводы раздела

Запомните главное

  • Процесс — запущенная программа с памятью, файлами, правами; ядро описывает его блоком управления (PCB)
  • Состояния: новый → готов ⇄ выполняется → ожидает → готов; завершён. Почти все процессы почти всё время ждут
  • Windows создаёт процесс одним CreateProcess, Linux — fork и exec; зомби — завершённый процесс, код которого не забрал родитель
  • Поток — то, что ставится на ядро; потоки процесса делят память, поэтому дешевле процессов, но падают вместе
  • Завершать сначала вежливо (taskkill, kill), принудительно (/f, -9) — только зависшие процессы
Практические задания — на учебном портале. Задания, критерии оценивания, сдача работ и оценки преподавателя — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 03: Установка ОС и виртуальные машины Все разделы Раздел 05: Планирование процессора