htop изнутри: процессы, память и ядро Linux

htop на Ubuntu Server 16.04 x64

Вот скриншот htop, который я собираюсь разобрать по частям.

Скриншот утилиты htop

Uptime (время работы системы)

Uptime показывает, как долго система работает без перезагрузки.

Ту же информацию можно получить, выполнив команду uptime:

$ uptime
12:17:58 up 111 days, 31 min, 1 user, load average: 0.00, 0.01, 0.05

Откуда программа uptime берёт эти данные? Она читает файл /proc/uptime.

9592411.58 9566042.33

Первое число — суммарное количество секунд, прошедших с момента запуска системы. Второе — сколько из этого времени машина провела в режиме простоя (idle). На многоядерных системах второе значение может превышать первое, поскольку складывается по всем ядрам.

Как это проверить? Можно посмотреть, какие файлы открывает программа uptime при запуске, с помощью утилиты strace:

strace uptime

Вывод будет объёмным. Можно отфильтровать системный вызов open через grep. Но тут есть тонкость: strace пишет всё в стандартный поток ошибок (stderr), поэтому нужно перенаправить его в стандартный вывод (stdout) с помощью 2>&1.

Результат будет примерно таким:

$ strace uptime 2>&1 | grep open
...
open("/proc/uptime", O_RDONLY) = 3
open("/var/run/utmp", O_RDONLY|O_CLOEXEC) = 4
open("/proc/loadavg", O_RDONLY) = 4

Здесь видно тот самый файл /proc/uptime. Кстати, можно обойтись и без grep — достаточно запустить strace -e open uptime.

Зачем вообще нужна программа uptime, если можно просто прочитать файл? Дело в том, что uptime выводит данные в удобочитаемом виде, тогда как сырое количество секунд удобнее использовать в собственных программах или скриптах.

Load average (средняя нагрузка)

Помимо времени работы, команда uptime выводит три числа — среднюю нагрузку:

$ uptime
12:59:09 up 32 min, 1 user, load average: 0.00, 0.01, 0.03

Эти данные берутся из файла /proc/loadavg — если снова посмотреть на вывод strace, он тоже открывался.

$ cat /proc/loadavg
0.00 0.01 0.03 1/120 1500

Первые три столбца — средняя нагрузка за последние 1, 5 и 15 минут. Четвёртый показывает число процессов, которые сейчас выполняются, и общее число процессов в системе. Последний столбец — ID последнего использованного процесса.

Начнём с последнего числа.

При каждом запуске нового процесса ему присваивается идентификатор (PID). PID-ы обычно растут по порядку — если только не исчерпались и не начали повторяться. Процессу /sbin/init, запускаемому при загрузке, всегда присваивается ID 1.

Посмотрим на содержимое /proc/loadavg и запустим команду sleep в фоновом режиме — при запуске в фоне её PID будет выведен на экран:

$ cat /proc/loadavg
0.00 0.01 0.03 1/123 1566
$ sleep 10 &
[1] 1567

Запись 1/123 означает, что прямо сейчас запущен или готов к запуску один процесс, а всего в системе 123 процесса.

Когда вы открываете htop и видите один выполняющийся процесс — это и есть сам htop.

Если запустить sleep 30 и снова открыть htop, счётчик выполняющихся процессов останется равным 1. Всё потому, что sleep не выполняется — он спит, то есть ожидает наступления события. Выполняющийся процесс (running process) — это тот, который в данный момент работает на физическом CPU или стоит в очереди на выполнение.

Запустим cat /dev/urandom > /dev/null — эта команда непрерывно генерирует случайные байты и пишет их в специальный файл, который никто не читает:

$ cat /dev/urandom > /dev/null &
[1] 1639
$ cat /proc/loadavg
1.00 0.69 0.35 2/124 1679

Теперь выполняется два процесса (генерация случайных чисел и cat, читающий /proc/loadavg), и средняя нагрузка заметно выросла.

Средняя нагрузка (load average) отражает среднее состояние нагрузки на систему за период времени.

Значение нагрузки вычисляется как количество процессов в состоянии «выполняется или готов к выполнению» плюс процессы в непрерываемом ожидании (uninterruptible sleep — ожидание диска или сети). Проще говоря, это просто число процессов.

Казалось бы, средняя нагрузка — это просто среднее число таких процессов за 1, 5 и 15 минут. Но всё не так просто.

На самом деле load average — это экспоненциально взвешенное скользящее среднее (exponentially damped moving average) значения нагрузки. Из Википедии:

С математической точки зрения все три значения усредняют нагрузку с момента старта системы. Все они затухают экспоненциально, но с разной скоростью. Поэтому минутное среднее складывается из 63% нагрузки за последнюю минуту и 37% нагрузки за всё остальное время. Технически некорректно говорить, что 1-минутное среднее отражает только последние 60 секунд: в нём остаётся 37% из более ранних периодов, хотя именно последняя минута вносит основной вклад.

Неожиданно, правда?

Вернёмся к нашей генерации случайных чисел:

$ cat /proc/loadavg
1.00 0.69 0.35 2/124 1679

Вот как я упрощаю для себя понимание средней нагрузки, пусть это и не вполне строго.

В данном случае процесс генерации случайных чисел полностью загружает CPU, поэтому средняя нагрузка за последнюю минуту равна 1.00 — в среднем один выполняющийся процесс.

Поскольку в моей системе один CPU, загрузка составляет 100%: процессор может выполнять только один процесс за раз.

На двухъядерной машине загрузка CPU составила бы 50%, потому что компьютер может одновременно выполнять два процесса. Load average 100%-но загруженного двухъядерного компьютера равнялся бы 2.00.

Число ядер или CPU можно увидеть в верхнем левом углу htop или выполнив команду nproc.

Поскольку в число нагрузки входят и процессы в непрерываемом ожидании, практически не влияющие на использование CPU, напрямую судить о загрузке процессора по load average некорректно. Этим и объясняется, почему бывает высокий load average при малой нагрузке на CPU.

Для измерения мгновенной загрузки CPU есть инструмент mpstat:

$ sudo apt install sysstat -y
$ mpstat 1
Linux 4.4.0-47-generic (hostname) 12/03/2016 _x86_64_ (1 CPU)
10:16:20 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
10:16:21 PM all 0.00 0.00 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
10:16:22 PM all 0.00 0.00 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
10:16:23 PM all 0.00 0.00 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
# ...
# kill cat /dev/urandom
# ...
10:17:00 PM all 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
10:17:01 PM all 1.00 0.00 0.00 2.00 0.00 0.00 0.00 0.00 0.00 97.00
10:17:02 PM all 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00

Зачем вообще нужны средние нагрузки? Посмотрим, что говорит исходный код ядра:

$ curl -s https://raw.githubusercontent.com/torvalds/linux/v4.8/kernel/sched/loadavg.c | head -n 7
/*
* kernel/sched/loadavg.c
*
* This file contains the magic bits required to compute the global loadavg
* figure. Its a silly number but people think its important. We go through
* great pains to make it work on big machines and tickless kernels.
*/

Процессы

В правом верхнем углу htop показывает общее число процессов и сколько из них выполняется. Но написано не «Processes», а Tasks («задачи»). Почему?

Задача (task) — это ещё одно название процесса. Ядро Linux внутренне называет процессы задачами. htop использует слово Tasks вместо Processes, вероятно, потому что оно короче и экономит место на экране.

В htop можно также видеть потоки (threads). Их отображение переключается нажатием Shift+H. Если вы видите Tasks: 23, 10 thr — значит, потоки отображаются.

Потоки ядра (kernel threads) можно отобразить через Shift+K. В этом случае появится надпись Tasks: 23, 40 kthr.

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

При каждом запуске нового процесса ему присваивается уникальный идентификационный номер — идентификатор процесса, или PID (process ID).

Если запустить программу в фоне (&) из bash, в квадратных скобках появится номер задания, а следом — PID:

$ sleep 1000 &
[1] 12503

Если пропустили — можно воспользоваться переменной $! в bash, которая раскрывается в PID последнего запущенного в фоне процесса:

$ echo $!
12503

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

procfs — это псевдофайловая система, позволяющая пользовательским программам получать информацию от ядра через чтение файлов. Она обычно монтируется в /proc/ и выглядит как обычный каталог, по которому можно ходить с помощью ls и cd.

Вся информация о процессе хранится в /proc/<pid>/:

$ ls /proc/12503
attr coredump_filter fdinfo maps ns personality smaps task
auxv cpuset gid_map mem numa_maps projid_map stack uid_map
cgroup cwd io mountinfo oom_adj root stat wchan
clear_refs environ limits mounts oom_score schedstat statm
cmdline exe loginuid mountstats oom_score_adj sessionid status
comm fd map_files net pagemap setgroups syscall

Например, /proc/<pid>/cmdline содержит команду, которой был запущен процесс:

$ cat /proc/12503/cmdline
sleep1000$

Выглядит неопрятно. Дело в том, что аргументы команды разделены байтом \0:

$ od -c /proc/12503/cmdline
0000000 s l e e p \0 1 0 0 0 \0
0000013

Его можно заменить пробелом или переносом строки:

$ tr '\0' '\n' < /proc/12503/cmdline
sleep
1000
$ strings /proc/12503/cmdline
sleep
1000

Каталог процесса может содержать символические ссылки. Например, cwd указывает на текущий рабочий каталог, а exe — на запущенный исполняемый файл:

$ ls -l /proc/12503/{cwd,exe}
lrwxrwxrwx 1 ubuntu ubuntu 0 Jul 6 10:10 /proc/12503/cwd -> /home/ubuntu
lrwxrwxrwx 1 ubuntu ubuntu 0 Jul 6 10:10 /proc/12503/exe -> /bin/sleep

Именно так htop, top, ps и другие диагностические утилиты получают информацию о процессах: они читают её из /proc/<pid>/<file>.

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

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

В htop иерархию процессов можно увидеть, нажав F5.

Также можно использовать флаг f команды ps:

$ ps f
PID TTY STAT TIME COMMAND
12472 pts/0 Ss 0:00 -bash
12684 pts/0 R+ 0:00 \_ ps f

или утилиту pstree:

$ pstree -a
init
├─atd
├─cron
├─sshd -D
│ └─sshd
│ └─sshd
│ └─bash
│ └─pstree -a
...

Если вы когда-нибудь задавались вопросом, почему bash или sshd так часто оказываются родителями других процессов — вот ответ.

Вот что происходит, когда вы запускаете, например, date из оболочки bash:

  • bash создаёт новый процесс — копию самого себя (системный вызов fork);

  • затем загружает программу из исполняемого файла /bin/date в память (системный вызов exec);

  • bash как родительский процесс ожидает завершения дочернего.

Таким образом, /sbin/init с ID 1 запускается при загрузке системы и порождает SSH-демон sshd. При подключении к серверу sshd создаёт процесс для сессии, который в свою очередь запускает оболочку bash.

Древовидный режим в htop удобен, когда хочется одновременно видеть все потоки процессов.

Владелец процесса

Каждый процесс принадлежит определённому пользователю. Пользователи представлены числовыми идентификаторами:

$ sleep 1000 &
[1] 2045
$ grep Uid /proc/2045/status
Uid: 1000 1000 1000 1000

Узнать имя пользователя по его ID можно командой id:

$ id 1000
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm)

Оказывается, id берёт эту информацию из файлов /etc/passwd и /etc/group:

$ strace -e open id 1000
...
open("/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 3
open("/lib/x86_64-linux-gnu/libnss_compat.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/lib/x86_64-linux-gnu/libnss_files.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/etc/passwd", O_RDONLY|O_CLOEXEC) = 3
open("/etc/group", O_RDONLY|O_CLOEXEC) = 3
...

Это определяется файлом конфигурации Name Service Switch (NSS) — /etc/nsswitch.conf:

$ head -n 9 /etc/nsswitch.conf
# ...
passwd: compat
group: compat
shadow: compat

Значение compat (режим совместимости) работает как files, но допускает некоторые дополнительные записи. files означает, что база данных хранится в файле (загружается через libnss_files.so). Пользователей можно также хранить в других базах данных или использовать, например, протокол LDAP (Lightweight Directory Access Protocol).

/etc/passwd и /etc/group — обычные текстовые файлы, связывающие числовые ID с читаемыми именами:

$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
$ cat /etc/group
root:x:0:
adm:x:4:syslog,ubuntu
ubuntu:x:1000:

passwd? А где же пароли?

Они хранятся в /etc/shadow:

$ sudo cat /etc/shadow
root:$6$mS9o0QBw$P1ojPSTexV2PQ.Z./rqzYex.k7TJE2nVeIVL0dql/:17126:0:99999:7:::
daemon:*:17109:0:99999:7:::
ubuntu:$6$GIfdqlb/$ms9ZoxfrUq455K6UbmHyOfz7DVf7TWaveyHcp.:17126:0:99999:7:::

Что это за набор символов?

  • $6$ — алгоритм хеширования пароля, в данном случае sha512;

  • за ним следует случайно сгенерированная соль (salt), защищающая от атак с радужными таблицами;

  • и наконец — хеш пароля вместе с солью.

Когда вы запускаете программу, она выполняется от вашего имени — даже если исполняемый файл принадлежит другому пользователю.

Чтобы запустить программу от имени root или другого пользователя, используется sudo:

$ id
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm)
$ sudo id
uid=0(root) gid=0(root) groups=0(root)
$ sudo -u ubuntu id
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm)
$ sudo -u daemon id
uid=1(daemon) gid=1(daemon) groups=1(daemon)

Если нужно зайти под другим пользователем и выполнить несколько команд, используйте sudo bash или sudo -u user bash — вы получите оболочку от имени этого пользователя.

Чтобы не вводить пароль root при каждом вызове sudo, можно отключить его запрос, добавив своего пользователя в файл /etc/sudoers.

Попробуем:

$ echo "$USER ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
-bash: /etc/sudoers: Permission denied

Логично — только root может это сделать.

$ sudo echo "$USER ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
-bash: /etc/sudoers: Permission denied

Что за ерунда?

Дело в том, что команда echo выполняется от имени root, но перенаправление вывода в файл /etc/sudoers всё равно происходит от имени вашего пользователя.

Обойти это можно двумя способами:

  • echo "$USER ALL=(ALL) NOPASSWD: ALL" | sudo tee -a /etc/sudoers

  • sudo bash -c "echo '$USER ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers"

В первом случае tee -a дописывает стандартный ввод в файл, и вся эта команда выполняется от root.

Во втором случае мы запускаем bash от root и просим его выполнить команду (-c), и тогда весь фрагмент выполняется от root. Обратите внимание на хитрое сочетание " и ', которое определяет, когда раскрывается переменная $USER.

Если открыть файл /etc/sudoers, в начале будет:

$ sudo head -n 3 /etc/sudoers
#
# This file MUST be edited with the 'visudo' command as root.
#

Вот это да.

Это предупреждение напоминает, что редактировать файл нужно через sudo visudo. Эта команда проверит содержимое перед сохранением и не даст допустить ошибку. Если отредактировать файл вручную и что-то сломать, можно потерять доступ к sudo — и исправить ошибку будет уже не так просто.

Допустим, нужно сменить пароль. Это делается командой passwd, которая, как мы уже видели, сохраняет пароль в файл /etc/shadow.

Этот файл чувствителен к безопасности и доступен на запись только root:

$ ls -l /etc/shadow
-rw-r----- 1 root shadow 1122 Nov 27 18:52 /etc/shadow

Как же программа passwd, запущенная обычным пользователем, может писать в защищённый файл?

Выше я говорил, что процесс принадлежит пользователю, который его запустил, — даже если исполняемый файл принадлежит кому-то другому. Но это поведение можно изменить через права доступа к файлу:

$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 54256 Mar 29 2016 /usr/bin/passwd

Обратите внимание на букву s. Она устанавливается командой sudo chmod u+s /usr/bin/passwd и означает, что исполняемый файл будет запускаться от имени его владельца — в данном случае root.

Найти так называемые setuid-исполняемые файлы можно командой find /bin -user root -perm -u+s.

То же самое можно сделать и для группы (g+s).

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

Теперь рассмотрим столбец состояния процесса в htop, обозначенный буквой S.

Возможные значения:

R  running or runnable (on run queue) — выполняется или готов к выполнению (в очереди)
S  interruptible sleep — прерываемое ожидание (ждёт наступления события)
D  uninterruptible sleep — непрерываемое ожидание (обычно I/O)
Z  defunct ("zombie") — завершился, но не прибран родителем
T  stopped by job control signal — остановлен сигналом управления заданиями
t  stopped by debugger during tracing — остановлен отладчиком при трассировке
X  dead — мёртв (на практике не встречается)

Перечислено в порядке частоты встречаемости.

Обратите внимание, что ps показывает составные состояния: Ss, R+, Ss+ и т.д.

$ ps x
PID TTY STAT TIME COMMAND
1688 ? Ss 0:00 /lib/systemd/systemd --user
1689 ? S 0:00 (sd-pam)
1724 ? S 0:01 sshd: vagrant@pts/0
1725 pts/0 Ss 0:00 -bash
2628 pts/0 R+ 0:00 ps x

R — выполняется или готов к выполнению (в очереди)

В этом состоянии процесс либо выполняется прямо сейчас, либо стоит в очереди на выполнение.

Что значит «выполняется»? Когда вы компилируете написанную программу, получается машинный код — набор инструкций для CPU. Он сохраняется в исполняемый файл. При запуске программа загружается в память, и процессор начинает выполнять эти инструкции. Иными словами, CPU физически обрабатывает команды.

S — прерываемое ожидание (ждёт наступления события)

В этом состоянии инструкции процесса не выполняются на CPU. Вместо этого процесс ждёт какого-то события или условия. Когда событие наступает, ядро переводит процесс в состояние «выполняется».

Пример — утилита sleep из пакета coreutils: она ждёт указанное число секунд.

$ sleep 1000 &
[1] 10089
$ ps f
PID TTY STAT TIME COMMAND
3514 pts/1 Ss 0:00 -bash
10089 pts/1 S 0:00 \_ sleep 1000
10094 pts/1 R+ 0:00 \_ ps f

Это прерываемое ожидание. Как его прервать? Послав сигнал.

В htop можно отправить сигнал, нажав F9 и выбрав нужный сигнал в меню слева.

Отправка сигнала также называется kill — потому что kill является системным вызовом, позволяющим отправить сигнал процессу. Программа /bin/kill делает этот вызов из пользовательского пространства, и по умолчанию отправляет сигнал TERM, прося процесс завершить работу.

Сигнал — это просто число. Чтобы их было проще запоминать, им дают имена. Имена сигналов обычно пишутся заглавными буквами и могут начинаться с префикса SIG.

Наиболее используемые сигналы: INT, KILL, STOP, CONT, HUP.

Прервём процесс sleep, отправив сигнал INT (он же SIGINT, он же 2, он же «прерывание с терминала»):

$ kill -INT 10089
[1]+ Interrupt sleep 1000

То же самое происходит, когда вы нажимаете CTRL+C: bash отправляет текущему процессу сигнал SIGINT — точно так же, как мы только что сделали вручную.

Кстати, в bash команда kill встроена в оболочку, хотя на большинстве систем есть и /bin/kill. Зачем? Чтобы можно было завершать процессы, даже когда исчерпан лимит на количество создаваемых процессов.

Все три команды делают одно и то же:

  • kill -INT 10089

  • kill -2 10089

  • /bin/kill -2 10089

Ещё один полезный сигнал — SIGKILL (он же 9). Наверняка вы им пользовались, когда процесс не реагировал на отчаянные нажатия CTRL+C.

При написании программы можно устанавливать обработчики сигналов — функции, которые вызываются при получении процессом сигнала. Иными словами, сигнал можно перехватить и выполнить какое-то действие, например, корректно завершить работу. Поэтому отправка SIGINT («пользователь хочет прервать процесс») или SIGTERM («пользователь хочет завершить процесс») вовсе не гарантирует, что процесс действительно завершится.

Возможно, вы видели такое исключение при выполнении Python-скриптов:

$ python -c 'import sys; sys.stdin.read()'
^C
Traceback (most recent call last):
  File "<string>", line 1, in <module>
KeyboardInterrupt

Чтобы принудительно завершить процесс, не давая ему шансов обработать сигнал, нужно отправить KILL:

$ sleep 1000 &
[1] 2658
$ kill -9 2658
[1]+ Killed sleep 1000

D — непрерываемое ожидание (обычно I/O)

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

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

Процессы в непрерываемом ожидании, как правило, ждут I/O после ошибки страницы (page fault). В этом состоянии процесс не может быть прерван, поскольку не способен обработать ни один сигнал: если бы он мог, произошла бы ещё одна ошибка страницы, и всё вернулось бы к тому же.

На практике это может происходить при использовании сетевой файловой системы NFS (Network File System), если чтение или запись занимают слишком много времени.

По личному опыту могу добавить: это состояние бывает и при активном свопинге — когда свободной памяти катастрофически не хватает.

Попробуем воспроизвести непрерываемое ожидание принудительно.

8.8.8.8 — публичный DNS-сервер Google. Открытого NFS-сервера там нет, но это нас не остановит.

$ sudo mount 8.8.8.8:/tmp /tmp &
[1] 12646
$ sudo ps x | grep mount.nfs
12648 pts/1 D 0:00 /sbin/mount.nfs 8.8.8.8:/tmp /tmp -o rw

Как понять, что происходит? Поможет strace!

Запустим strace для команды из вывода ps:

$ sudo strace /sbin/mount.nfs 8.8.8.8:/tmp /tmp -o rw
...
mount("8.8.8.8:/tmp", "/tmp", "nfs", 0, ...

Системный вызов mount блокирует процесс.

Если интересно: можно запустить mount с опцией intr, чтобы он работал в прерываемом режиме: sudo mount 8.8.8.8:/tmp /tmp -o intr.

Z — зомби (завершился, но не прибран родителем)

Когда процесс завершается через exit и при этом у него остаются дочерние процессы, эти дочерние процессы превращаются в зомби.

  • Зомби-процессы, существующие короткое время, — это норма.

  • Зомби, живущие долго, могут свидетельствовать об ошибке в программе.

  • Зомби не потребляют памяти — только идентификатор процесса.

  • Завершить зомби-процесс через kill нельзя.

  • Можно попросить родительский процесс «прибрать» зомби, отправив ему сигнал SIGCHLD.

  • Можно завершить родителя зомби — тогда исчезнут и он, и его зомби.

Проиллюстрирую это на примере кода на C.

Вот наша программа:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
    printf("Running\n");
    int pid = fork();
    if (pid == 0) {
        printf("I am the child process\n");
        printf("The child process is exiting now\n");
        exit(0);
    } else {
        printf("I am the parent process\n");
        printf("The parent process is sleeping now\n");
        sleep(20);
        printf("The parent process is finished\n");
    }
    return 0;
}

Установим компилятор GCC (GNU C Compiler):

sudo apt install -y gcc

Скомпилируем и запустим:

gcc zombie.c -o zombie
./zombie

Посмотрим на дерево процессов:

$ ps f
PID TTY STAT TIME COMMAND
3514 pts/1 Ss 0:00 -bash
7911 pts/1 S+ 0:00 \_ ./zombie
7912 pts/1 Z+ 0:00 \_ [zombie] <defunct>
1317 pts/0 Ss 0:00 -bash
7913 pts/0 R+ 0:00 \_ ps f

Зомби получен!

Когда родительский процесс завершается, зомби исчезает:

$ ps f
PID TTY STAT TIME COMMAND
3514 pts/1 Ss+ 0:00 -bash
1317 pts/0 Ss 0:00 -bash
7914 pts/0 R+ 0:00 \_ ps f

Если заменить sleep(20) на while (true) ;, зомби исчезнет сразу же.

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

Зачем же вообще хранить зомби-процессы? Дело в том, что у родительского процесса есть возможность узнать код завершения дочернего (в обработчике сигнала) с помощью системного вызова wait. Если процесс спит, ему нужно дождаться момента, когда он проснётся.

Почему нельзя просто принудительно разбудить его и уничтожить? По той же причине, по которой вы не выбрасываете ребёнка в мусор, когда устали. Последствия могут быть непредсказуемыми.

T — остановлен сигналом управления заданиями

Откроем два терминала и посмотрим на процессы пользователя через ps u:

$ ps u
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
ubuntu 1317 0.0 0.9 21420 4992 pts/0 Ss+ Jun07 0:00 -bash
ubuntu 3514 1.5 1.0 21420 5196 pts/1 Ss 07:28 0:00 -bash
ubuntu 3528 0.0 0.6 36084 3316 pts/1 R+ 07:28 0:00 ps u

Процессы -bash и ps u из дальнейшего вывода опускаю.

Запустим cat /dev/urandom > /dev/null в одном из терминалов. Состояние процесса — R+, то есть выполняется:

$ ps u
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
ubuntu 3540 103 0.1 6168 688 pts/1 R+ 07:29 0:04 cat /dev/urandom

Нажмём CTRL+Z для остановки процесса:

$ # CTRL+Z
[1]+ Stopped cat /dev/urandom > /dev/null
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
ubuntu 3540 86.8 0.1 6168 688 pts/1 T 07:29 0:15 cat /dev/urandom

Состояние изменилось на T.

Чтобы возобновить выполнение, запустите fg в том же терминале.

Аналогичного результата можно добиться, отправив процессу сигнал STOP через kill. Для продолжения выполнения используется сигнал CONT.

t — остановлен отладчиком при трассировке

Сначала установим отладчик GDB (GNU Debugger):

sudo apt install -y gdb

Запустим программу, которая будет ожидать входящих соединений на порту 1234:

$ nc -l 1234 &
[1] 3905

Процесс спит — ждёт данных из сети:

$ ps u
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
ubuntu 3905 0.0 0.1 9184 896 pts/0 S 07:41 0:00 nc -l 1234

Запустим отладчик и подключимся к процессу с ID 3905:

sudo gdb -p 3905

Теперь состояние процесса изменится на t — процесс трассируется отладчиком:

$ ps u
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
ubuntu 3905 0.0 0.1 9184 896 pts/0 t 07:41 0:00 nc -l 1234

Время процесса

Linux — многозадачная операционная система: даже с одним CPU можно одновременно выполнять несколько процессов. Можно подключиться к серверу по SSH и смотреть вывод htop, пока веб-сервер отдаёт страницы блога читателям.

Как это возможно, если один CPU выполняет только одну инструкцию за раз?

Ответ — разделение времени (time sharing).

Каждый процесс работает короткий промежуток времени, после чего приостанавливается, уступая место другим. Этот промежуток называется квантом времени (time slice).

Квант обычно составляет несколько миллисекунд, поэтому при низкой нагрузке переключение почти незаметно. (Было бы интересно узнать, какова типичная длина кванта в Linux.)

Теперь понятно, почему средняя нагрузка — это среднее число выполняющихся процессов. При одном ядре и load average 1.0 CPU загружен на 100%. Если load average выше 1.0 — процессов, желающих выполняться, больше, чем CPU успевает обработать, и могут возникать замедления. Если ниже 1.0 — CPU иногда простаивает.

Это также объясняет, почему процесс, работающий 10 секунд, может показывать время выполнения больше или меньше ровно 10 секунд.

Приоритет и «вежливость» процесса

Когда задач больше, чем ядер CPU, нужно решить, какие из них запустить следующими, а какие оставить ждать. Этим занимается планировщик задач (task scheduler).

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

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

«Вежливость» (NI, niceness) — приоритет пользовательского пространства (user-space priority). Значения варьируются от -20 (наивысший приоритет) до 19 (наименьший). Это немного сбивает с толку, но можно думать о ней так: чем «вежливее» процесс, тем больше он уступает другим.

По информации, собранной с StackOverflow и других ресурсов, увеличение niceness на 1 даёт другим процессам примерно на 10% больше процессорного времени.

Приоритет (PRI) — приоритет пространства ядра, который использует сам Linux. Значения от 0 до 139, из которых 0–99 — диапазон реального времени, 100–139 — для пользовательских процессов.

Изменить niceness можно, а PRI — нет.

Связь между niceness и приоритетом:

PR = 20 + NI

То есть при NI от -20 до +19 получаем PR от 0 до 39, что соответствует диапазону 100–139.

Задать niceness перед запуском программы:

nice -n niceness program

Изменить niceness уже запущенного процесса командой renice:

renice -n niceness -p PID

Что означают цвета при отображении CPU:

  • Синий: потоки с низким приоритетом (nice > 0)

  • Зелёный: потоки с обычным приоритетом

  • Красный: потоки ядра

Использование памяти — VIRT/RES/SHR/MEM

У каждого процесса есть иллюзия, что он единственный в памяти. Это достигается с помощью виртуальной памяти (virtual memory).

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

Суть в том, что однозначно определить, сколько памяти занимает процесс, непросто. Учитывать ли разделяемые библиотеки или отображённую на диск память? Тем не менее ядро предоставляет, а htop отображает ряд показателей, помогающих оценить расход памяти.

Что означают цвета при отображении памяти:

  • Зелёный: используемая память

  • Синий: буферы

  • Оранжевый: кэш

VIRT/VSZ — образ виртуальной памяти

Суммарный объём виртуальной памяти, используемой задачей. Включает весь код, данные, разделяемые библиотеки, страницы, выгруженные в своп, и страницы, отображённые, но не использованные.

VIRT — объём виртуальной памяти. Учитывается всё, включая файлы, отображённые в память (memory-mapped files).

Если приложение запросило 1 ГБ памяти, но использует только 1 МБ, VIRT покажет 1 ГБ. Если через mmap отобразить файл размером 1 ГБ и никогда к нему не обращаться — VIRT тоже покажет 1 ГБ.

В большинстве случаев этот показатель мало о чём говорит.

RES/RSS — резидентный размер

Объём физической памяти (не выгруженной в своп), фактически используемой задачей.

RES — резидентная память, то есть то, что прямо сейчас находится в физической RAM.

Несмотря на то что RES точнее характеризует реальное использование памяти, чем VIRT, нужно учитывать следующее:

  • выгруженная в своп память сюда не включается;

  • часть памяти может быть разделена с другими процессами.

Если процесс занимает 1 ГБ памяти и вызывает fork(), оба процесса будут показывать RES по 1 ГБ, но реально будет занят лишь 1 ГБ — благодаря механизму копирования при записи (copy-on-write).

SHR — размер разделяемой памяти

Объём разделяемой памяти, используемой задачей. Отражает память, которая потенциально может быть разделена с другими процессами.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
    printf("Started\n");
    sleep(10);
    size_t memory = 10 * 1024 * 1024; // 10 MB
    char* buffer = malloc(memory);
    printf("Allocated 10M\n");
    sleep(10);
    for (size_t i = 0; i < memory/2; i++)
        buffer[i] = 42;
    printf("Used 5M\n");
    sleep(10);
    int pid = fork();
    printf("Forked\n");
    sleep(10);
    if (pid != 0) {
        for (size_t i = memory/2; i < memory/2 + memory/5; i++)
            buffer[i] = 42;
        printf("Child used extra 2M\n");
    }
    sleep(10);
    return 0;
}
fallocate -l 10G
gcc -std=c99 mem.c -o mem
./mem
Process  Message             VIRT   RES   SHR
main     Started             4200   680   604
main     Allocated 10M      14444   680   604
main     Used 5M            14444  6168  1116
main     Forked             14444  6168  1116
child    Forked             14444  5216     0
main     Child used extra 2M       8252  1116
child    Child used extra 2M       5216     0

TODO: Этот раздел ещё не закончен.

MEM% — процент использования памяти

Доля доступной физической памяти, используемая задачей в данный момент.

Это RES, делённый на общий объём оперативной памяти.

Если RES равен 400M, а в системе 8 ГБ RAM, то MEM% = 400/8192*100 = 4.88%.

Процессы при запуске системы

Рассмотрим список процессов на скриншоте htop.

Нужны ли они вам на самом деле?

Ниже — мои заметки о процессах, запускаемых при старте чистого дроплета Digital Ocean с Ubuntu Server 16.04.1 LTS x64.

До

Список процессов htop при старте системы

/sbin/init

Программа /sbin/init (также называемая init) координирует оставшуюся часть загрузки и настраивает окружение для пользователя. При запуске команда init становится родителем или прародителем всех процессов, запускаемых автоматически при старте системы.

Является ли это systemd?

$ dpkg -S /sbin/init
systemd-sysv: /sbin/init

Да, это systemd.

Что произойдёт, если его завершить? Ничего.

/lib/systemd/systemd-journald

systemd-journald — это системный сервис, собирающий и хранящий данные журналов. Он создаёт и ведёт структурированные индексированные журналы на основе информации, поступающей из различных источников.

Иными словами:

Одним из главных изменений в journald стал переход от простых текстовых журналов к специальному формату файлов, оптимизированному для хранения сообщений. Этот формат позволяет администраторам эффективнее находить нужные записи и привносит часть возможностей централизованного ведения журналов в отдельные системы.

Для просмотра журналов используется команда journalctl:

  • journalctl _COMM=sshd — журналы sshd

  • journalctl _COMM=sshd -o json-pretty — журналы sshd в формате JSON

  • journalctl --since "2015-01-10" --until "2015-01-11 03:00"

  • journalctl --since 09:00 --until "1 hour ago"

  • journalctl --since yesterday

  • journalctl -b — журналы с момента загрузки

  • journalctl -f — следить за журналом в реальном времени

  • journalctl --disk-usage

  • journalctl --vacuum-size=1G

Весьма удобно.

Судя по всему, этот сервис нельзя ни удалить, ни отключить — можно лишь отключить запись журналов.

/sbin/lvmetad -f

Демон lvmetad кэширует метаданные LVM, позволяя командам LVM читать метаданные без сканирования дисков. Кэширование метаданных выгодно, поскольку сканирование дисков занимает время и может мешать нормальной работе системы.

Что же такое LVM (Logical Volume Management — управление логическими томами)?

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

Если вы используете LVM — оставьте этот сервис.

$ lvscan
$ sudo apt remove lvm2 -y --purge

/lib/systemd/udevd

systemd-udevd прослушивает события ядра (uevents). Для каждого события systemd-udevd выполняет соответствующие инструкции, заданные в правилах udev. udev — менеджер устройств ядра Linux, управляющий узлами устройств в каталоге /dev.

Этот сервис управляет каталогом /dev.

Неясно, нужен ли он на виртуальном сервере.

/lib/systemd/timesyncd

systemd-timesyncd — системный сервис для синхронизации локальных системных часов с удалённым сервером NTP (Network Time Protocol).

Этот сервис заменяет ntpd.

$ timedatectl status
      Local time: Fri 2016-08-26 11:38:21 UTC
  Universal time: Fri 2016-08-26 11:38:21 UTC
        RTC time: Fri 2016-08-26 11:38:20
       Time zone: Etc/UTC (UTC, +0000)
 Network time on: yes
NTP synchronized: yes
 RTC in local TZ: no

Посмотрим на открытые порты сервера:

$ sudo netstat -nlput
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      2178/sshd
tcp6       0      0 :::22                   :::*                    LISTEN      2178/sshd

Отлично!

На Ubuntu 14.04 с пакетом ntp картина была куда менее приятной:

$ sudo apt-get install ntp -y
$ sudo netstat -nlput
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
© 2026 meganuke