Справочник ключевых терминов курса Operating Systems для Junior.
Центральный компонент ОС, работающий с привилегированным доступом к железу. Управляет памятью, процессами, файловыми системами, сетью и устройствами. В Linux всё это монолит в едином адресном пространстве (но с loadable modules для драйверов). У kernel свой стек, своя память, свой набор машинных инструкций (включая privileged: HLT, CLI, LIDT). Userspace не может напрямую вызвать функции kernel -- только через syscall, который физически выполняет переход CPU в kernel mode.
# Какое ядро запущено
uname -r
# 6.6.12-amd64
# Версия и параметры сборки
cat /proc/version
# Linux version 6.6.12 (gcc ...) #1 SMP PREEMPT_DYNAMIC ...
# Параметры, с которыми ядро загружено
cat /proc/cmdline
# BOOT_IMAGE=/vmlinuz-6.6 root=UUID=... ro quiet
# Размер ядра в памяти и виртуальные области
cat /proc/meminfo | grep -i kernelОбласть памяти и привилегий, где запускаются обычные процессы -- ваш bash, Python, nginx, браузер. CPU выполняет код в non-privileged режиме (ring 3 на x86), не может писать в kernel-память, не может напрямую обращаться к железу. Любая операция, требующая привилегий (чтение файла, сетевой запрос, fork процесса) -- это syscall в kernel. Изоляция userspace процессов друг от друга обеспечивается виртуальной памятью: у каждого процесса свой address space.
# Где живёт user-код процесса -- /proc/PID/maps
cat /proc/self/maps | head -3
# 555555554000-555555556000 r--p 00000000 ...
# 555555556000-55555555c000 r-xp 00002000 ...
# Какой UID/привилегии у текущего процесса
cat /proc/self/status | grep -E 'Uid|Cap'
# Сравнить kernel vs user CPU time
time sleep 1
# real 1.001s user 0.001s sys 0.000sГраничный API между userspace и kernel. Физически: специальная CPU-инструкция (`syscall` на x86_64, `svc` на ARM), переключающая CPU в kernel mode и передающая управление по заранее зарегистрированному адресу (entry point). Номер syscall кладётся в регистр (rax на x86_64), аргументы -- в rdi/rsi/rdx/r10/r8/r9. Возвращаемое значение приходит в rax. В Linux ~400 syscalls: read, write, open, close, mmap, fork, execve, brk, getpid и т.д. Стандартная C-библиотека glibc оборачивает syscalls в удобные функции (printf -> write, malloc -> brk/mmap).
# Все syscalls текущей команды
strace -c ls /tmp
# % time seconds usecs/call calls syscall
# ------ ----------- ----------- --------- ----------
# 21.34 0.000064 16 4 mmap
# 18.34 0.000055 11 5 openat
# Конкретный syscall в деталях
strace -e openat ls /etc
# openat(AT_FDCWD, '/etc', O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3Привилегированный уровень исполнения на x86. CPU имеет 4 кольца (Ring 0-3), реально используются только два: Ring 0 для kernel, Ring 3 для userspace. В Ring 0 доступны привилегированные инструкции (управление прерываниями, MMU, I/O-портами), полный доступ ко всей физической памяти. Переход Ring 3 -> Ring 0 происходит только через контролируемые точки входа: syscall, interrupt, exception. Это аппаратная основа изоляции kernel от userspace -- даже если процесс попытается выполнить privileged-инструкцию, CPU выкинет General Protection Fault.
# Linux запускает userspace в Ring 3, kernel в Ring 0.
# На x86_64 виртуализация добавляет Ring -1 (hypervisor).
# В исполняемом коде userspace privileged-инструкции дадут SIGSEGV:
# __asm__('cli'); # Disable interrupts -- невозможно из Ring 3
# Посмотреть текущий privilege-level можно через CS register (последние 2 бита),
# но только через ядро -- /proc/cpuinfo показывает только статически известноеНепривилегированный уровень на x86, где работают userspace-процессы. Запрещены privileged-инструкции, прямой доступ к I/O-портам, изменение IDT/GDT/CR3. Каждый процесс видит только свой виртуальный адресный спейс. Любая попытка обратиться к kernel-памяти или выполнить запрещённую инструкцию -> General Protection Fault, kernel перехватывает и обычно убивает процесс (SIGSEGV). Это аппаратный sandbox: ошибка в userspace не может уронить kernel.
# Простой эксперимент: попытаться записать в kernel-адрес
# из C-программы -- получишь SIGSEGV (segmentation fault).
# В strace это видно как exit с сигналом:
strace ./bad_program
# --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0xffffffff} ---
# +++ killed by SIGSEGV +++Архитектура, где все системные сервисы (файловые системы, сетевой стек, драйверы устройств, планировщик, IPC) живут в едином адресном пространстве kernel. Преимущество: быстро, нет накладных расходов на IPC между подсистемами. Недостаток: баг в драйвере = kernel panic, всё падает. Linux, FreeBSD, классический Unix -- монолитные. Современные монолиты допускают loadable kernel modules (LKM): драйверы и FS можно подгружать/выгружать без перезагрузки.
# Какие модули загружены
lsmod | head
# Module Size Used by
# nvidia_drm 106496 4
# btrfs 1773568 1
# Загрузить модуль
sudo modprobe overlay
# Информация о модуле
modinfo overlayАрхитектура, где в kernel только минимум: scheduling, IPC, базовое управление памятью. Драйверы, FS, networking работают как обычные userspace-сервисы и общаются через message passing. Преимущества: изоляция (падение FS-сервиса не валит систему), легче верифицировать. Недостатки: больше context switches между сервисами => медленнее. Примеры: L4, MINIX, QNX, частично macOS XNU (Mach + BSD). Реальный мейнстрим -- гибридный подход: monolith с возможностью выноса.
# macOS XNU = Mach (microkernel concepts) + BSD subsystem.
# Smartphone модемы часто на QNX или L4.
# Полностью microkernel'овая ОС на десктопе -- редкость.Запущенный экземпляр программы, изолированный в собственном виртуальном адресном пространстве. У процесса есть PID, PPID, UID/GID, набор открытых файловых дескрипторов, address space (text/data/heap/stack), credentials, сигнальная маска. В Linux представлен структурой task_struct в kernel. Процессы образуют дерево: каждый создан родителем через fork, корень -- init (PID 1). Изоляция процессов друг от друга обеспечивается виртуальной памятью и проверкой UID при доступе к ресурсам.
# Список всех процессов с PID, PPID, UID
ps -eo pid,ppid,uid,comm | head
# PID PPID UID COMMAND
# 1 0 0 systemd
# 2 0 0 kthreadd
# Информация о процессе
cat /proc/$$/status | head -10
# Name: bash
# Pid: 12345
# PPid: 12344
# Uid: 1000 1000 1000 1000
# Дерево процессов
pstree -p | headУникальный (на момент жизни) идентификатор процесса в kernel. В Linux 32-битное число, по умолчанию диапазон 1..4194304 (контролируется `/proc/sys/kernel/pid_max`). PID 1 -- init (systemd). Kernel-потоки часто имеют низкие PIDы (2, 3, ...). PID переиспользуется после смерти процесса -- через какое-то время этот же номер достанется другому. Поэтому код, держащий PID долго (например, файл с PID демона), должен дополнительно проверять имя процесса или использовать pidfd.
# PID текущего shell
echo $$
# 12345
# Найти процесс по имени
pgrep nginx
# 2401
# 2402
# Подробно
ps -p 2401 -o pid,ppid,user,cmdPID родительского процесса -- того, который сделал fork и породил данный процесс. Если родитель умирает раньше ребёнка, ребёнок становится orphan и его PPID меняется на 1 (init/systemd принимает осиротевших). Дерево процессов через PPID можно увидеть командой `pstree`. PPID полезен для понимания, кто запустил процесс: например, странный bash с PPID=ssh => кто-то залогинился; bash с PPID=cron => запущено по расписанию.
# PPID текущего shell
ps -o ppid= -p $$
# 12344 (например, login shell или sshd)
# Полное дерево с командой
pstree -ap | grep -A1 systemd | headСистемный вызов, создающий точную копию текущего процесса. Ребёнок получает свой PID, но видит ту же память что и родитель (через copy-on-write -- страницы не копируются физически, пока кто-то не запишет). Те же открытые файлы (fd дублируются, offset общий). Возвращает 0 в ребёнке, PID ребёнка в родителе, -1 на ошибке. Это базовый механизм создания процессов в Unix -- даже shell делает fork+exec на каждую команду. В новых API часто заменяется на posix_spawn или vfork+exec для эффективности.
import os
pid = os.fork()
if pid == 0:
print(f'Child: pid={os.getpid()}, parent={os.getppid()}')
os._exit(0)
else:
print(f'Parent: I forked pid={pid}')
os.wait()Семейство syscalls (execve, execvp, execl и т.д.), полностью заменяющих образ текущего процесса на новую программу. PID остаётся тот же, но память, code, data, stack заменяются на содержимое нового binary. Открытые файловые дескрипторы по умолчанию остаются (если на них не выставлен FD_CLOEXEC). После успешного exec код после exec() не выполняется -- его уже физически нет. Комбинация fork + exec -- классический способ запуска новой программы в Unix.
import os
# В дочернем процессе
os.execvp('ls', ['ls', '-la', '/tmp'])
# Этот print никогда не выполнится -- exec заменил образ
print('never reached')
# В strace это виден один execve
strace -e execve ls /
# execve('/usr/bin/ls', ['ls', '/'], 0x7ffd...) = 0Syscall, через который родитель ждёт завершения ребёнка и забирает его exit code. Без wait завершённый процесс остаётся в системе как zombie -- занимает запись в process table, держит свой PID и exit status. waitpid позволяет ждать конкретный PID или с флагом WNOHANG не блокироваться (вернёт 0 если ребёнок ещё жив). Родитель может игнорировать SIGCHLD -- тогда kernel сам почистит зомби. Если родитель умер раньше -- ребёнка усыновляет init/systemd и он его wait'ит.
import os
pid = os.fork()
if pid == 0:
os._exit(42)
else:
waited_pid, status = os.waitpid(pid, 0)
print(f'Child {waited_pid} exited with {os.waitstatus_to_exitcode(status)}')
# Child 12346 exited with 42Процесс, который завершился (вызвал exit), но его родитель ещё не сделал wait() -- exit code не забран. Зомби не потребляет CPU/RAM, но занимает PID и запись в process table. В `ps` отображается как `Z` в STAT. Множество зомби -- баг родителя, не вызвавшего wait. Убить зомби невозможно (он уже мёртв) -- надо или заставить родителя сделать wait (SIGCHLD ему), или убить родителя -- тогда init/systemd подберёт сирот и почистит их.
# Найти всех зомби
ps -eo pid,ppid,stat,comm | awk '$3 ~ /Z/'
# PID PPID STAT COMMAND
# 4242 4200 Z+ <defunct>
# Чей родитель -- кто баг?
ps -o pid,ppid,comm -p 4200
# Лечение: kill родителя (его дочки уйдут к init)
kill -CHLD 4200 # пусть отреагирует
# или
kill 4200Процесс, чей родитель умер до его завершения. Kernel автоматически меняет PPID такого процесса на 1 (init/systemd) -- init обязан wait()'ить сирот, поэтому orphans не превращаются в zombie. Это разные понятия: orphan -- живой процесс без оригинального родителя; zombie -- мёртвый процесс, на который никто не сделал wait. В daemonization-паттерне специально делают двойной fork, чтобы процесс гарантированно стал orphan и его принял init.
# Симуляция: запустить процесс в фоне и убить shell
bash -c 'sleep 60 &'
# Сразу:
ps -o pid,ppid,comm | grep sleep
# PID PPID COMMAND
# 5050 4040 sleep
# Через секунду (когда shell умер):
ps -o pid,ppid,comm | grep sleep
# PID PPID COMMAND
# 5050 1 sleep <- PPID стал 1, его принял initПроцесс в Linux находится в одном из состояний (поле STAT в ps): R -- running/runnable, готов или исполняется на CPU; S -- interruptible sleep, ждёт события (можно прервать сигналом); D -- uninterruptible sleep, обычно ждёт I/O от железа (не убить даже SIGKILL); T -- остановлен (SIGSTOP, или debugger); Z -- zombie. Переходы: created -> ready (R) -> running (R) -> blocked (S/D) -> ready -> ... -> terminated (Z). Многочисленные D-state процессы указывают на проблемы с диском.
# Состояния всех процессов
ps -eo stat,comm | awk '{print $1}' | sort | uniq -c | sort -rn | head
# 320 S
# 25 I
# 12 R
# 2 D
# Найти D-state (uninterruptible)
ps -eo stat,pid,comm | awk '$1 ~ /D/'Единица планирования внутри процесса. Все потоки одного процесса разделяют адресное пространство (heap, глобальные переменные, открытые файлы), но у каждого свой stack, свои регистры, свой program counter. В Linux реализованы через clone() с флагами CLONE_VM|CLONE_FS|CLONE_FILES -- технически это просто 'lightweight process'. Планировщик не различает процессы и потоки -- видит task_struct и решает, что запустить. Множество потоков нужно для параллелизма на multi-core, фонового I/O, отзывчивого UI.
# Сколько потоков у процесса
ps -o nlwp -p $$
# NLWP
# 1
# Все потоки конкретного процесса
ps -L -p <PID>
# или
ls /proc/<PID>/task/
# Стек и состояние каждого потока
cat /proc/<PID>/task/<TID>/statСтандартизированный POSIX-API для работы с потоками: pthread_create, pthread_join, pthread_mutex_t, pthread_cond_t. В Linux реализован через библиотеку NPTL (Native POSIX Thread Library), под капотом использует clone() syscall. Один pthread = один task_struct в kernel = 1:1 модель threading (в отличие от старого LinuxThreads или зелёных потоков). Большинство языков (Python, Go runtime, Java) либо строятся поверх pthread, либо имеют свой scheduler поверх него.
// C, gcc -pthread
#include <pthread.h>
#include <stdio.h>
void *worker(void *arg) {
printf('thread %ld\n', (long)arg);
return NULL;
}
int main() {
pthread_t t[4];
for (long i = 0; i < 4; i++)
pthread_create(&t[i], NULL, worker, (void*)i);
for (int i = 0; i < 4; i++)
pthread_join(t[i], NULL);
}Баг, при котором результат зависит от порядка выполнения нескольких потоков -- и этот порядок недетерминирован. Классический пример: два потока делают `counter += 1` без синхронизации; операция не атомарна (read-modify-write), поэтому при переплетении инструкций одно из обновлений теряется. Race conditions сложно отлавливать: они проявляются под нагрузкой и могут не воспроизводиться в дебаге. Лечатся блокировками (mutex, atomic), либо архитектурно -- избегая разделяемого мутабельного состояния (actor model, immutable data).
import threading
counter = 0
def worker():
global counter
for _ in range(1_000_000):
counter += 1
threads = [threading.Thread(target=worker) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(counter)
# Ожидали 4_000_000, получили что-то меньше -- race condition
# (в CPython GIL частично спасает, но не от каждого случая)Примитив синхронизации, гарантирующий, что критическую секцию в момент времени исполняет только один поток. Операции: lock (блокирует поток если mutex занят), unlock (отпускает). В Linux реализован через futex syscall: на быстром пути lock'нуться можно в userspace через atomic-операцию, без захода в kernel; только при contention делается futex_wait. У mutex есть владелец -- только захвативший поток может разлочить. Recursive mutex позволяет одному потоку lock'нуться повторно (для рекурсии).
import threading
counter = 0
lock = threading.Lock()
def worker():
global counter
for _ in range(1_000_000):
with lock:
counter += 1
threads = [threading.Thread(target=worker) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(counter) # 4_000_000 -- корректноСчётчик-примитив синхронизации. Поток делает wait (P-операция): если счётчик > 0, декрементирует и продолжает; иначе блокируется. Signal (V) -- инкрементирует и будит ожидающего. В отличие от mutex, у семафора нет владельца -- любой поток может сигнализировать. Binary semaphore (счётчик 0/1) близок к mutex. Counting semaphore используется для ограничения количества одновременных операций (rate limiter, connection pool). В POSIX: sem_init, sem_wait, sem_post.
import threading
# Ограничить количество одновременных загрузок
sem = threading.Semaphore(3)
def download(url):
with sem: # max 3 одновременно
# ... делать запрос ...
passСитуация, когда два или более потоков ждут друг друга, и никто не может продолжить. Классический пример: поток A держит lock1 и ждёт lock2, поток B держит lock2 и ждёт lock1. Кофман сформулировал 4 необходимых условия: mutual exclusion, hold-and-wait, no preemption, circular wait. Лечение архитектурное: всегда брать блокировки в одинаковом порядке (lock ordering), использовать try_lock с откатом, или избегать вложенных блокировок. На диск-уровне случается с fcntl-locks между процессами.
# Linux: можно посмотреть, кто кого держит
cat /proc/<PID>/status | grep State
# State: D (disk sleep) -- застрял в kernel I/O lock
# Для futex-deadlock в userspace:
perf lock report
# или strace -e futex показывает FUTEX_WAIT, который не получает FUTEX_WAKEПодсистема kernel, решающая, какой из готовых (runnable) потоков получит CPU следующим. Главные задачи: справедливость (fairness), отзывчивость (latency), пропускная способность (throughput). В Linux основной планировщик -- CFS (Completely Fair Scheduler) для обычных задач, плюс real-time классы SCHED_FIFO/SCHED_RR/SCHED_DEADLINE для критичного по времени. Решение принимается при каждом tick'е таймера, syscall, прерывании, пробуждении задачи.
# Какие задачи в системе и их класс
chrt -p $$
# pid 12345's current scheduling policy: SCHED_OTHER
# pid 12345's current scheduling priority: 0
# Статистика scheduler'а
cat /proc/<PID>/sched | head
# se.exec_start : ...
# se.vruntime : ...
# nr_switches : 12345Модель, в которой kernel может в любой момент отобрать CPU у текущего потока и переключить на другой. В Linux это делается по тику таймера (~100-1000 Hz), при пробуждении более приоритетной задачи, при возврате из syscall. Противоположность -- cooperative scheduling (как в classic Mac OS или asyncio), где поток сам должен явно отдать управление. Preemption обеспечивает отзывчивость даже если userspace-код зацикливается -- его всё равно прервут.
# Частота тиков таймера kernel'а (HZ)
grep CONFIG_HZ= /boot/config-$(uname -r)
# CONFIG_HZ=1000
# Сколько раз процесс был preempted
cat /proc/<PID>/status | grep -i ctxt
# voluntary_ctxt_switches: 1234
# nonvoluntary_ctxt_switches: 567 <- preemptionОсновной планировщик Linux с 2007 года. Не использует фиксированные time slices -- вместо этого ведёт виртуальное время (vruntime) для каждой задачи и всегда выбирает ту, у которой vruntime минимально. Получается, что задачи получают CPU пропорционально своему weight (выводится из nice value). На практике 'completely fair' = взвешенно справедливо. CFS хорошо работает для общих interactive/batch workload, но для жёстких real-time нужны другие классы (SCHED_DEADLINE).
# vruntime текущей задачи
cat /proc/<PID>/sched | grep vruntime
# se.vruntime : 12345678.123456
# Веса nice <-> weight можно посмотреть в исходниках kernel/sched/core.c
# nice -20 -> weight 88761, nice 19 -> weight 15Userspace-настройка приоритета процесса в Linux. Диапазон -20..+19, по умолчанию 0. Низкое значение = выше приоритет = больше CPU. Имя от 'be nice to others' -- положительный nice делает процесс уступчивым. Только root может ставить отрицательный nice (повышать приоритет). Стартовый nice задаётся при запуске (`nice -n 10 cmd`), потом меняется через `renice`. Влияет только на CPU-scheduling, не на disk I/O (для этого ionice).
# Запустить с пониженным приоритетом
nice -n 19 ./backup.sh
# Изменить nice уже запущенного процесса
renice -n 10 -p 12345
# Текущий nice
ps -o pid,ni,pri,cmd -p $$
# PID NI PRI COMMAND
# 12345 0 19 -bashУтилита для управления приоритетом дискового I/O процесса (в kernel это part of BFQ/mq-deadline I/O scheduler). Классы: 1 (Realtime), 2 (Best-effort, default), 3 (Idle -- ждёт пока никто не использует диск). Внутри классов 0-7 уровней приоритета. Полезно для backup-скриптов, индексаторов, etl-задач -- чтобы не мешать prod-нагрузке. Реальный эффект зависит от I/O scheduler'а блочного устройства: noop/none их игнорирует.
# Запустить процесс в idle-классе I/O
ionice -c3 ./backup.sh
# Изменить у запущенного
ionice -c2 -n7 -p 12345
# Текущий приоритет
ionice -p 12345
# best-effort: prio 4
# I/O scheduler устройства
cat /sys/block/sda/queue/scheduler
# [bfq] mq-deadline noneАбстракция, при которой каждый процесс видит свой 'полный' адресный спейс (на x86_64 -- 128 TiB), не зависящий от физической RAM. Виртуальные адреса транслируются в физические через page tables, которыми управляет kernel + MMU. Преимущества: изоляция между процессами (каждый видит только свою память), отсутствие фрагментации физической памяти (физически память может быть разбросана, виртуально -- последовательна), возможность держать процессы больше RAM (через swap), copy-on-write (fork без копирования).
# Виртуальная память процесса
cat /proc/self/maps | head
# 555555554000-555555556000 r--p ... /usr/bin/cat
# 555555556000-55555555c000 r-xp ... /usr/bin/cat
# 7ffff7d83000-7ffff7da8000 r--p ... /usr/lib/libc.so.6
# Общая статистика
cat /proc/self/status | grep -E 'VmSize|VmRSS|VmData'
# VmSize: 12345 kB <- виртуально
# VmRSS: 4321 kB <- физически в RAM сейчасРазбиение виртуальной и физической памяти на блоки фиксированного размера (страницы, обычно 4 KiB на x86; есть huge pages 2 MiB и 1 GiB). Kernel ведёт page table, мапящую виртуальные страницы на физические frame'ы. При доступе CPU через MMU транслирует адрес. Если страница не в RAM (был swapped или ни разу не trigger'ился) -- page fault, kernel либо подгружает её с диска (major fault), либо аллоцирует свежую (minor fault).
# Размер страницы
getconf PAGESIZE
# 4096
# Поддерживаемые huge pages
cat /proc/meminfo | grep -i huge
# HugePages_Total: 0
# Hugepagesize: 2048 kB
# Page faults процесса
cat /proc/self/stat | awk '{print 'minflt='$10, 'majflt='$12}'Структура данных в kernel-памяти, описывающая, как виртуальные адреса процесса транслируются в физические. На x86_64 используется 4-уровневая иерархия (PGD -> PUD -> PMD -> PTE), что позволяет компактно представлять 128 TiB виртуального пространства, не аллоцируя страницы для неиспользуемых регионов. Каждый процесс имеет свой root page table -- именно его адрес лежит в регистре CR3, и переключение CR3 на context switch -- это переключение address space. Запись в PTE содержит physical frame number плюс флаги (present, writable, user, accessed, dirty).
# Размер page tables процесса
cat /proc/self/status | grep -i pagetable
# VmPTE: 80 kB
# Можно через /proc/self/pagemap прочитать mapping для конкретного адреса
# (требует root)Аппаратный кэш на CPU, хранящий результаты последних трансляций виртуальный -> физический адрес. Без него каждое обращение к памяти требовало бы хождения по page table (4 чтения на x86_64 -- очень дорого). При hit TLB трансляция занимает 0 циклов; при miss MMU делает page walk (~100 циклов на DDR-доступ). При context switch TLB частично сбрасывается (с CONFIG_PCID -- по тегам). Huge pages уменьшают TLB pressure: одна 2 MiB-запись покрывает 512 4 KiB-обращений.
# Прямого способа смотреть TLB-stats в Linux без perf нет.
perf stat -e dTLB-load-misses,iTLB-load-misses ./my_program
# 1,234,567 dTLB-load-misses
# 12,345 iTLB-load-missesАппаратный блок в CPU, выполняющий трансляцию виртуальных адресов в физические по page table. Без MMU работа process isolation была бы невозможна -- надо было бы делать программные проверки на каждое обращение к памяти. MMU также проверяет права доступа (writable, executable, kernel-only) и при нарушении генерирует page fault. На x86_64 MMU интегрирована с CPU; на ARM/RISC-V похожая архитектура. У контроллеров без MMU (embedded) полноценная виртуальная память невозможна -- запускают uClinux или RTOS.
# MMU всегда есть на x86_64/ARM с MMU.
# Проверить, что у процессора (косвенно):
grep -i 'mmu\|pae\|pse\|paging' /proc/cpuinfoДисковая область (раздел или файл), куда kernel выгружает неактивные страницы памяти, когда не хватает RAM. Позволяет процессам в сумме использовать больше памяти, чем физически есть. Цена: доступ к swapped-странице в тысячи раз медленнее, чем к RAM (DDR ~100 ns vs SSD ~100 us). Поведение управляется `/proc/sys/vm/swappiness` (0..200, default 60): выше -- активнее свопить. На серверах с быстрым SSD swap полезен; на DB-серверах часто отключают чтобы избежать I/O-всплесков.
# Состояние swap
swapon --show
free -h
# Кто сейчас активно использует swap
for f in /proc/*/status; do
awk '/VmSwap|Name/{printf $2 " " $3 "\n"}' $f 2>/dev/null
done | paste - - | sort -k2 -h | tail
# Изменить swappiness
sysctl vm.swappiness=10Механизм kernel, активирующийся когда система не может аллоцировать память даже после очистки кэшей и свопа. Kernel выбирает 'жертву' по oom_score (учитывается размер процесса, время жизни, oom_score_adj) и шлёт ей SIGKILL. Цель -- освободить достаточно памяти, чтобы система выжила. Алгоритм агрессивно выбирает крупных потребителей: часто жертва -- сам важный процесс (Postgres, JVM). Защититься можно через oom_score_adj=-1000 (immunity), но это рискованно -- зацикливается nothing-to-kill.
# Кто был убит
dmesg -T | grep -i 'killed process'
# [...] Out of memory: Killed process 12345 (python3) total-vm:8GB, anon-rss:7GB
# Защитить процесс от OOM (root)
echo -1000 > /proc/12345/oom_score_adj
# Текущий счёт
cat /proc/12345/oom_scoreПолитика, при которой kernel выдаёт процессам в сумме больше виртуальной памяти, чем есть физической + swap. Работает потому что большинство аллоцированной памяти процессы реально не трогают. Управляется `/proc/sys/vm/overcommit_memory`: 0 -- эвристика (default), 1 -- всегда соглашаться (даже на безумные malloc), 2 -- никогда не overcommit'ить, лимит = swap + ratio*RAM. Без overcommit fork() большого процесса часто бы падал (хотя copy-on-write делает реальную копию минимальной).
# Текущая политика
sysctl vm.overcommit_memory
# vm.overcommit_memory = 0
# Сколько памяти 'commit'нуто и лимит
cat /proc/meminfo | grep -i commit
# CommitLimit: 8388608 kB
# Committed_AS: 4194304 kB
# Жёсткая дисциплина (для прода с DB)
sysctl vm.overcommit_memory=2
sysctl vm.overcommit_ratio=80Архитектура многосокетных серверов, где у каждого CPU-сокета своя локальная память. Доступ к 'чужой' (remote) памяти медленнее, чем к локальной (1.5-3x latency). Kernel пытается аллоцировать память на том же node, где работает процесс (NUMA affinity), но при memory pressure может уйти на другой node. Для perf-критичных задач (DBMS, ML training) важно явно прибивать процессы и память к одному node через numactl. На однопроцессорных серверах NUMA отсутствует.
# Сколько NUMA-нод
numactl --hardware
# available: 2 nodes (0-1)
# node 0 cpus: 0-15
# node 0 size: 64512 MB
# node 1 cpus: 16-31
# node 1 size: 64512 MB
# Запустить на конкретном node
numactl --cpunodebind=0 --membind=0 ./db_server
# Где сейчас память процесса
numastat -p <PID>Область памяти процесса/потока, растущая в сторону уменьшения адресов на x86 (auto-grow до RLIMIT_STACK, default 8 MiB). Хранит локальные переменные функций, аргументы, адреса возврата, saved registers. У каждого потока свой stack. Переполнение stack (`stack overflow`) -- обычно из-за бесконечной рекурсии или огромного локального массива -- даёт SIGSEGV. Размер настраивается через `ulimit -s` или pthread_attr_setstacksize. В отличие от heap, аллокация/освобождение на стеке -- просто инструкция изменения регистра sp, без участия kernel.
# Stack-границы текущего потока
cat /proc/self/maps | grep stack
# 7ffe...000-7ffe...000 rw-p ... [stack]
# Лимит размера stack
ulimit -s
# 8192 (в KiB, т.е. 8 MiB)
# Увеличить (для текущего shell)
ulimit -s 16384Область памяти процесса для динамической аллокации через malloc/new/realloc. Растёт в сторону увеличения адресов через syscall brk (расширяет program break) или mmap (отдельный регион). Управляется аллокатором (glibc ptmalloc, jemalloc, tcmalloc, mimalloc), который ведёт свободные блоки внутри полученных от kernel страниц -- minimizing syscalls. В отличие от stack, heap общий для всех потоков (отсюда возможность race в malloc, лечится thread-local arenas).
# Область heap процесса
cat /proc/self/maps | grep heap
# 5555...000-5555...000 rw-p ... [heap]
# Какой аллокатор используется (в Python)
import ctypes
libc = ctypes.CDLL('libc.so.6')
# malloc на самом деле звонит в ptmalloc (glibc)
# Активность аллокатора
strace -e brk,mmap python -c 'a = [0]*1_000_000' 2>&1 | headСтандартные C-функции для динамической аллокации памяти. malloc(size) возвращает указатель на size байт неинициализированной памяти. Внутри glibc реализована через ptmalloc2: для маленьких аллокаций (<128 KB обычно) использует heap через brk/sbrk, для больших -- mmap. Многопоточный режим использует множественные arenas (по одной на поток обычно) чтобы избежать contention на одном lock. Возвращаемый указатель выровнен на 16 байт на x86_64.
// C
char *buf = malloc(1024);
if (!buf) { /* OOM */ }
// ... use buf ...
free(buf);
# Какие malloc-syscalls делает программа
strace -e mmap,brk ./my_program 2>&1 | wc -l
# Замена аллокатора через LD_PRELOAD
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 ./my_programОсвобождает память, ранее выделенную malloc. Память возвращается в пул аллокатора, но не обязательно kernel'у -- glibc может держать освобождённое в своих arenas для будущих аллокаций (быстрее, не надо syscall). Этого можно достичь через malloc_trim(0). free на уже освобождённом указателе (double-free) -- UB, обычно сразу падает или corrupts heap. Free на не-malloc'нутом указателе -- тоже UB. Современные аллокаторы детектят простые случаи и аварийно падают с диагностикой.
// C
char *buf = malloc(1024);
free(buf);
buf = NULL; // защита от случайного double-free
// Вернуть память kernel'у (если возможно)
#include <malloc.h>
malloc_trim(0);Syscall, мапящий файл или анонимный регион памяти в виртуальное пространство процесса. Используется в трёх ролях: (1) загрузка исполняемых файлов и .so в память, (2) шаринг памяти между процессами (MAP_SHARED), (3) большие аллокации в malloc (поверх MAP_ANONYMOUS). Mmap'нутый файл читается lazy: страница загружается с диска только при первом обращении (page fault). MAP_SHARED + запись -> изменения видны другим процессам и (при наличии файла) попадают на диск.
import mmap
with open('huge.dat', 'rb') as f:
mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
# Доступ как к массиву, без read()
print(mm[1024:1100])
mm.close()
# Что в адресном пространстве -- из mmap
cat /proc/self/maps | grep -v stack | grep -v heap | headПамять, видимая нескольким процессам -- самый быстрый способ IPC (нет копирования при общении). В POSIX API: shm_open создаёт файл-объект в /dev/shm, дальше его mmap'ят с MAP_SHARED. Старый SysV API (shmget, shmat) реже используется. Координация доступа -- через отдельные примитивы (mutex в shared memory, семафоры). Часто используется для shared cache, ML inference (одна модель, много worker'ов), zero-copy IPC.
import multiprocessing as mp
import numpy as np
# Python: shared memory из multiprocessing
shm = mp.shared_memory.SharedMemory(create=True, size=1024*1024)
arr = np.ndarray((1024,), dtype=np.int64, buffer=shm.buf)
arr[0] = 42
# В другом процессе:
# shm2 = SharedMemory(name=shm.name)
# arr2 = np.ndarray(..., buffer=shm2.buf)
# print(arr2[0]) # 42Механизмы общения между процессами. В Linux основные: pipes (anonymous и named/FIFO), Unix domain sockets, signals, shared memory, message queues (SysV/POSIX), eventfd/signalfd. Выбор зависит от: направление (one-way / bidirectional), родственность (parent-child или unrelated), нагрузка (control plane vs big data), требования к latency. Для большинства задач Unix sockets хорошо балансируют простоту и производительность; для high-throughput -- shared memory + futex.
# Список IPC-объектов в системе
ipcs -a
# ------ Message Queues --------
# ------ Shared Memory Segments --------
# ------ Semaphore Arrays --------
# Открытые pipe/socket конкретного процесса
lsof -p <PID> | grep -E 'PIPE|sock|FIFO'Однонаправленный канал байтового стрима между процессами в Unix. Syscall pipe() возвращает два fd: один для чтения, один для записи. Buffer 64 KiB по умолчанию. Используется shell'ом для `|`: один процесс пишет в write-end, другой читает из read-end. Анонимные pipes доступны только через наследование fd (fork) -- unrelated процессы их не увидят. EOF на read'е приходит когда все писатели закрыли свой fd. Запись блокируется когда буфер полон.
import os
r, w = os.pipe()
pid = os.fork()
if pid == 0:
os.close(r)
os.write(w, b'hello')
os._exit(0)
else:
os.close(w)
print(os.read(r, 100)) # b'hello'
os.waitpid(pid, 0)
# shell: $ ls | grep .txt -- это pipe()Pipe с именем в файловой системе. Создаётся через mkfifo(1) или mkfifo(3). В отличие от анонимного pipe, может использоваться unrelated процессами -- они оба открывают один и тот же путь. Запись/чтение -- через стандартные open/read/write. open блокируется до тех пор, пока кто-то не откроет другой конец (если не O_NONBLOCK). Данные не пишутся на диск -- передаются в kernel-буфере. Полезно для шёлл-скриптов и простой сериализации.
# Терминал 1
mkfifo /tmp/mypipe
echo 'hello' > /tmp/mypipe
# Блокируется пока кто-то не прочитает
# Терминал 2
cat /tmp/mypipe
# helloАсинхронное уведомление, отправленное процессу. Стандартных сигналов 31 + real-time signals (32-64). Процесс может: обработать (handler), игнорировать (SIG_IGN), оставить default. SIGKILL и SIGSTOP нельзя ни поймать, ни проигнорировать. Сигнал доставляется в любой момент -- handler должен быть async-signal-safe (только список разрешённых syscalls). В новых API часто заменяется на signalfd: сигналы как poll'имый файл-дескриптор, что снимает большую часть граблей.
import signal
import os
import time
def handler(signum, frame):
print(f'got signal {signum}')
signal.signal(signal.SIGUSR1, handler)
print(f'my pid is {os.getpid()}, send: kill -USR1 {os.getpid()}')
time.sleep(60)
# В другом терминале
kill -USR1 <PID>Стандартный сигнал 'просьба завершиться'. Процесс может перехватить и сделать graceful shutdown: закрыть соединения, флушнуть буферы, сохранить state. Это сигнал по умолчанию у `kill PID` без флага. systemd при остановке сервиса сначала шлёт SIGTERM, ждёт TimeoutStopSec (default 90 сек), затем SIGKILL. Хорошо написанный сервис при SIGTERM начинает draining: перестаёт принимать новые запросы, завершает текущие, корректно отключается.
# Послать процессу SIGTERM
kill 12345
# или явно
kill -TERM 12345
kill -15 12345
# В Python
import signal
signal.signal(signal.SIGTERM, lambda *a: shutdown_gracefully())Безусловное убийство процесса. Не может быть пойман или проигнорирован -- kernel убивает процесс немедленно. Используется как крайнее средство, когда SIGTERM не сработал. Минус: процесс не успеет закрыть файлы (буферы потеряются), отдать соединения, обновить state. На D-state (uninterruptible) процесса SIGKILL не подействует, пока процесс не выйдет из kernel-кода -- иногда D-процессы остаются 'до перезагрузки'.
# Убить процесс наверняка
kill -9 12345
kill -KILL 12345
# Если процесс в D-state -- ждать или ребут
ps -eo pid,stat,comm | awk '$2 ~ /D/'IPC-механизм через стандартный socket API (socket/bind/connect/send/recv), но без сети -- общение через файл-сокет в FS (`/tmp/app.sock`, `/var/run/docker.sock`) или абстрактное имя (с префиксом 0x00). Быстрее TCP loopback (нет TCP overhead), безопаснее (можно контролировать через файловые права на сокет, plus SO_PEERCRED -- узнать UID собеседника). Поддерживает SOCK_STREAM и SOCK_DGRAM. Используется в Docker, X11, PostgreSQL, systemd.
# Сервер
import socket
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.bind('/tmp/my.sock')
s.listen(5)
conn, _ = s.accept()
print(conn.recv(1024))
# Клиент
import socket
c = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
c.connect('/tmp/my.sock')
c.send(b'hello')
# В FS
ls -la /tmp/my.sock
# srwxr-xr-x 1 user user 0 ... /tmp/my.sockЦелочисленный handle, который kernel выдаёт процессу при открытии файла, сокета, pipe, eventfd и т.д. Уникален в рамках процесса. Стандартные: 0 stdin, 1 stdout, 2 stderr. open/socket/pipe возвращают свежий fd; read/write/close принимают fd. Все 'всё есть файл' в Unix реализовано через единый API над fd. Лимит на количество одновременно открытых fd ограничен: ulimit -n (default 1024). При исчерпании syscalls вернут EMFILE 'Too many open files'.
# Все fd процесса
ls -la /proc/$$/fd/
# lrwx------ 0 -> /dev/pts/0
# lrwx------ 1 -> /dev/pts/0
# lrwx------ 2 -> /dev/pts/0
# Текущий лимит
ulimit -n
# 1024
# Системный максимум
cat /proc/sys/fs/file-maxСтруктура метаданных файла на диске: размер, права, владелец, mtime/ctime/atime, ссылки на блоки данных, счётчик hard-links. Имя файла НЕ в inode -- оно живёт в директории как пара (имя -> номер inode). Поэтому два разных имени могут указывать на один inode (hard link). Количество inode фиксируется при mkfs (`mkfs.ext4 -N`). 'No space left on device' при df с free space -- часто исчерпан пул inode (см. `df -i`).
# Номер inode файла
stat /etc/passwd
# File: /etc/passwd
# Inode: 12345 Links: 1
# То же из ls
ls -i /etc/passwd
# 12345 /etc/passwd
# Inode use на разделе
df -i /
# Filesystem Inodes IUsed IFree IUse% ...Второе (третье и т.д.) имя для существующего файла, указывающее на тот же inode. Все hard links равноправны -- 'оригинала' нет. Файл физически удаляется когда счётчик ссылок (`stat -c %h`) падает до нуля И никто его не держит открытым. Hard links нельзя сделать на директории (избегаем циклов) и через разные mount-точки (inode уникальны только в рамках FS). Удобны для дедупликации (rsync --link-dest, Time Machine).
echo 'hello' > /tmp/orig
ln /tmp/orig /tmp/link
stat /tmp/orig | grep Links
# Links: 2
rm /tmp/orig
cat /tmp/link
# hello -- файл жив, есть второе имяСпециальный файл, содержащий путь к другому файлу. При открытии kernel прозрачно следует по этому пути. Symlink может указывать на несуществующий файл (dangling), пересекать FS-границы, ссылаться на директорию. Удаление symlink не трогает target; удаление target оставляет dangling symlink. В отличие от hard link, видно через `ls -la` (`->`). Создаётся `ln -s`. readlink читает значение без следования.
ln -s /etc/passwd /tmp/users
ls -la /tmp/users
# lrwxr-xr-x 1 ... /tmp/users -> /etc/passwd
readlink /tmp/users
# /etc/passwd
# Не следовать symlink
ls -l --no-dereference /tmp/users
stat -L /tmp/users # с -L следоватьОперация, прикрепляющая файловую систему (раздел диска, NFS-share, tmpfs) к точке в существующем дереве каталогов. Без mount раздел диска -- набор блоков, недоступный через обычное чтение файлов. После mount /dev/sda2 на /mnt/data, все файлы FS видны под /mnt/data/... Один блочный девайс может быть mount'ен в нескольких местах (с MS_BIND). Системные mounts описаны в `/etc/fstab` и автоматически делаются при boot.
# Все смонтированное
mount | head
findmnt -l | head
# Подключить раздел
sudo mount /dev/sdb1 /mnt/data
# Отключить
sudo umount /mnt/data
# Что в fstab
cat /etc/fstabКонфиг автоматических mount'ов при загрузке. Каждая строка: устройство (или UUID), точка монтирования, тип FS, опции, dump-флаг, fsck-order. Используется systemd при boot, а также `mount -a`. Лучше указывать UUID, а не /dev/sdaN -- порядок устройств может меняться. Опции типа `noatime` улучшают perf, `relatime` -- разумный default. Ошибки в fstab могут запретить загрузку -- использовать `mount -a` или `findmnt --verify` перед reboot.
cat /etc/fstab
# /dev/disk/by-uuid/abc123 / ext4 errors=remount-ro 0 1
# UUID=def456 /home ext4 defaults 0 2
# tmpfs /tmp tmpfs nosuid,noexec 0 0
# Проверка перед reboot
sudo findmnt --verifyАбстракция в kernel, дающая единый API для разных файловых систем. open/read/write/close работают одинаково для ext4, xfs, btrfs, tmpfs, NFS, FUSE, /proc, /sys. VFS определяет операции (inode_operations, file_operations, super_operations), а каждая FS их реализует под себя. Благодаря VFS, userspace не зависит от конкретной FS и можно прозрачно использовать сетевые/виртуальные FS. /proc и /sys -- pseudo-FS, не имеют дисковых блоков, операции read/write генерируют данные на лету.
# Все смонтированные FS и их типы
findmnt --types ext4,xfs,btrfs,tmpfs,procfs,sysfs | head
# Поддерживаемые ядром типы FS
cat /proc/filesystems
# nodev sysfs
# nodev tmpfs
# ext4Журналируемая FS, default в большинстве Linux-дистрибутивов. Развитие ext2/ext3. Поддерживает extents (компактное описание непрерывных диапазонов блоков), журнал (восстановление после краха), backwards-compat с ext3. Хорошо балансирует производительность и стабильность для общего use case. Лимиты: до 16 TiB файлов, 1 EiB FS. Минусы: нет snapshot'ов, нет встроенного checksum'а данных, нет copy-on-write.
# Создать ext4
mkfs.ext4 /dev/sdb1
# Проверить и оптимизировать
fsck.ext4 /dev/sdb1
tune2fs -l /dev/sdb1 | head
# Resize онлайн
resize2fs /dev/sdb1Журналируемая FS, изначально из SGI Irix, теперь главная FS для RHEL/Rocky/CentOS. Сильна на больших объёмах: миллионы файлов в директории, файлы до 8 EiB, отличное масштабирование на NVMe/multi-disk. Поддерживает delayed allocation (откладывает запись на диск, объединяя в большие extents). Не умеет shrink -- только grow. Хороший выбор для filer'ов, медиа-серверов, баз с тяжёлыми блобами.
# Создать
mkfs.xfs /dev/sdb1
# Информация
xfs_info /
# Дефрагментация
xfs_fsr -v /
# Снимок-через-LVM (xfs сам snapshot не умеет)Copy-on-write FS с встроенными snapshots, checksums, compression, RAID, send/receive. Полностью реализована в mainline Linux. Default в SUSE, Synology DSM. Преимущества: атомарные snapshot за O(1), incremental backup через send/receive, прозрачное сжатие (zstd/lz4), проверка целостности. Минусы: исторически проблемы с RAID5/6 (write hole), требовательность к свободному месту (нельзя забивать >85%), медленнее ext4 на random-write workload.
# Создать subvolume
btrfs subvolume create /mnt/data/work
# Snapshot
btrfs subvolume snapshot /mnt/data/work /mnt/data/work-2026-05-18
# Send/receive
btrfs send /mnt/data/work-2026-05-18 | ssh backup btrfs receive /backup/
# Использование
btrfs filesystem df /Самая богатая по фичам FS: copy-on-write, snapshots, send/receive, encryption, compression (zstd), deduplication, RAID-Z, end-to-end checksums, ARC-кэш в RAM. Изначально из Sun Solaris, в Linux через OpenZFS (license из-за CDDL ставится out-of-tree). Дорогая по памяти (1 GB RAM на 1 TB рекомендация), но мощь -- основа TrueNAS, многих storage-серверов. Дедупликация требует огромных ресурсов -- обычно отключают.
# Создать pool из двух дисков (mirror)
zpool create tank mirror /dev/sdb /dev/sdc
# Dataset с сжатием
zfs create -o compression=zstd tank/data
# Snapshot
zfs snapshot tank/data@2026-05-18
# Список pool'ов и состояние
zpool statusКэш файловых данных в RAM, через который проходит каждое чтение/запись на диск. При read из файла kernel сначала проверяет cache: если страница в нём -- мгновенно отдаёт, если нет -- читает с диска и кладёт в cache. При write по умолчанию данные попадают в page cache (dirty pages), а на диск сбрасываются позже (writeback). Размер кэша динамический -- всё свободное RAM. Поэтому 'free' показывает мало 'free' и много 'cache/buffers' -- это норма.
# Состояние page cache
free -h
# total used free shared buff/cache available
# Mem: 16G 2G 1G ... 13G 14G
# Очистить cache (root, на свой страх)
sync && echo 3 > /proc/sys/vm/drop_caches
# vmstat показывает активность
vmstat 1 5
# r b swpd free buff cache si so bi bo in cs us sy id wa stSyscall, заставляющий kernel записать все dirty-страницы файла на физический диск и дождаться подтверждения. Без fsync write только обновил page cache -- при выключении питания данные потеряются. fdatasync аналогичен, но не флушит метаданные (mtime, ctime) если не было их изменения -- быстрее. Критично для баз данных (после commit), почтовых серверов, любых durability-требовательных приложений. На SSD относительно быстро (1-10 ms), на HDD дорого (10-50 ms).
import os
with open('/data/important.dat', 'wb') as f:
f.write(b'critical data')
f.flush() # из buffer Python в kernel
os.fsync(f.fileno()) # из kernel на диск
# Замерить cost fsync на устройстве
# pip install pg-test-fsync или fio
fio --name=fsync_test --filename=/data/test --rw=write --size=1G --fsync=1Флаг open(), сообщающий kernel'у не использовать page cache -- данные читаются и пишутся напрямую между userspace-buffer'ом и диском. Требует выравнивания buffer'а и размера I/O по размеру блока (обычно 4 KiB). Используется базами данных (PostgreSQL, MySQL InnoDB), которые ведут собственный buffer pool и не хотят double-buffering. Минус: каждое I/O -- syscall + физический disk-доступ, нет кэширования; ускоряет только если cache_hit_ratio был низкий.
// C: открыть для direct I/O
int fd = open('/dev/sdb1', O_RDONLY | O_DIRECT);
// buf должен быть выровнен на блок
void *buf;
posix_memalign(&buf, 4096, 4096);
read(fd, buf, 4096);
# PostgreSQL пишет с O_DIRECT при wal_sync_method = open_datasync (в Linux)Числовой идентификатор пользователя в Linux. 0 -- root, 1-999 обычно системные (daemon-аккаунты), 1000+ -- обычные пользователи. Хранится в /etc/passwd (login name <-> UID), процесс наследует UID от запустившего его. У процесса есть реальный (real), эффективный (effective) и saved UID -- разные для setuid-программ. Доступ к ресурсам (файлы, сокеты) проверяется по effective UID. UID 0 пропускает все проверки доступа (всё под root).
# Текущий UID
id -u
# 1000
id
# uid=1000(lev) gid=1000(lev) groups=1000(lev),27(sudo)
# UID процесса
cat /proc/<PID>/status | grep Uid
# Uid: 1000 1000 1000 1000 (real, eff, saved, fs)Числовой идентификатор группы. Пользователь имеет primary group (в /etc/passwd) и supplementary groups (в /etc/group). При доступе к файлу проверяется effective GID и все supplementary. Группы используются для шаринга доступа: добавьте пользователей в `docker` группу -- они смогут юзать docker без sudo. Изменения в группах применяются при следующем login (не для текущей сессии -- надо relogin или `newgrp`).
# Все группы пользователя
groups
# lev sudo docker
id -G
# 1000 27 999
# Добавить в группу
sudo usermod -aG docker lev
# /etc/group
grep docker /etc/group
# docker:x:999:lev,aliceБазовая модель прав файлов в Unix: для owner, group, others -- читать (r), писать (w), executable (x). Отображается ls -l: `-rwxr-xr--`. В octal: r=4, w=2, x=1, owner+group+other => 754. На директорию: r -- ls; w -- создавать/удалять файлы; x -- cd внутрь. Без x на директории нельзя обращаться к её содержимому даже зная имя файла. chmod меняет права (требует ownership или root).
ls -l /etc/passwd
# -rw-r--r-- 1 root root 2048 ... /etc/passwd
# u=rw, g=r, o=r (644)
chmod 600 secret.txt # только owner может читать
chmod u+x script.sh # добавить executable для owner
chmod -R g+w /shared # рекурсивно дать write группеУтилита для выполнения команды от имени другого пользователя (по умолчанию root). Решение принимается на основе /etc/sudoers -- кто, какие команды, на каких хостах, с паролем или без. Преимущества над su: гранулярность (можно дать только nginx restart, не весь root), audit (логирует кто что делал в /var/log/auth.log или journalctl), отсутствие необходимости знать root-пароль. Best practice -- не давать NOPASSWD ALL, ограничивать конкретными командами.
# Запустить от root
sudo apt update
# От другого пользователя
sudo -u postgres psql
# Список разрешённых команд
sudo -l
# Правка sudoers (всегда через visudo для валидации!)
sudo visudo
# %dev ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginxСпециальный бит прав, при выставлении которого исполняемый файл при запуске получает effective UID = UID владельца файла, а не запустившего. Классический пример -- /usr/bin/passwd: владелец root, SUID, обычный пользователь запускает и меняет /etc/shadow (доступный только root). В ls отображается как `s` вместо `x`: `-rwsr-xr-x`. Огромный security risk: ошибка в SUID-программе = privilege escalation. Современный Linux рекомендует заменять на capabilities.
ls -l /usr/bin/passwd
# -rwsr-xr-x 1 root root ... /usr/bin/passwd
# Поставить SUID
chmod u+s /usr/local/bin/myutil
# или octal
chmod 4755 /usr/local/bin/myutil
# Найти все SUID-binary в системе
find / -perm -4000 -type f 2>/dev/nullГранулярное разбиение root-привилегий на ~40 отдельных capability'и: CAP_NET_RAW (raw socket), CAP_NET_BIND_SERVICE (привязка к порту < 1024), CAP_SYS_ADMIN, CAP_KILL (kill чужих процессов), CAP_CHOWN и т.д. Можно дать программе только нужные capability'и, не делая её SUID root. Видны в getcap (на binary) и в /proc/PID/status (на запущенном). Контейнеры (Docker, podman) активно используют для drop'а лишних cap'ов.
# Capabilities binary'я
getcap /usr/bin/ping
# /usr/bin/ping cap_net_raw=ep
# Дать программе CAP_NET_BIND_SERVICE
sudo setcap cap_net_bind_service=+ep /usr/local/bin/myserver
# Cap'ы процесса
grep Cap /proc/self/status
# CapInh: 00...
# CapPrm: 00...
# CapEff: 00...Механизм изоляции: процессы в namespace видят свой собственный 'instance' какого-то системного ресурса. Типы: pid (свои PIDs), net (свои интерфейсы, маршруты), mnt (свои mount points), uts (свой hostname), ipc (свои SysV IPC), user (свои UID/GID mappings), cgroup, time. Основа контейнеризации: Docker/podman/kubernetes создают комбинацию namespaces для каждого контейнера. Создаются через clone() с CLONE_NEW* флагами или unshare(2).
# Свои namespaces у процесса
ls -la /proc/$$/ns/
# lrwxrwxrwx 1 ... ipc -> 'ipc:[4026531839]'
# lrwxrwxrwx 1 ... mnt -> 'mnt:[4026531840]'
# lrwxrwxrwx 1 ... net -> 'net:[4026531992]'
# ...
# Создать процесс в новом PID namespace
sudo unshare --pid --fork --mount-proc bash
# Теперь ps показывает только процессы внутриМеханизм ограничения и учёта ресурсов (CPU, RAM, disk I/O, network) для группы процессов. Иерархическая структура контроллеров: memory, cpu, blkio, pids и т.д. В cgroup v2 (mainline с Linux 4.5) -- единая иерархия в /sys/fs/cgroup. systemd использует cgroups для каждого unit'а (services, scopes, slices). Контейнеры через cgroups ограничивают сколько RAM/CPU может съесть workload. memory.max -- hard limit (превышение -> OOM kill внутри cgroup).
# cgroups текущего процесса
cat /proc/self/cgroup
# 0::/user.slice/user-1000.slice/session-3.scope
# Лимиты systemd service
systemctl show docker.service -p MemoryHigh,CPUQuota
# Сколько RAM использует cgroup
cat /sys/fs/cgroup/user.slice/user-1000.slice/memory.currentPseudo-FS, дающая userspace интерфейс к kernel state. Не имеет дисковых блоков -- данные генерируются ядром по запросу. /proc/<PID>/ -- информация о процессе (status, maps, fd/, stat, sched). /proc/cpuinfo, /proc/meminfo, /proc/loadavg -- системная информация. /proc/sys/... -- настройки kernel (можно изменять через echo). Самый удобный способ диагностики Linux без специальных утилит. Сами утилиты ps, top, free, vmstat читают /proc.
# CPU info
cat /proc/cpuinfo | grep -E 'model name|cpu cores' | head -4
# Memory
cat /proc/meminfo | head -5
# Загрузка
cat /proc/loadavg
# 0.45 0.34 0.28 1/512 12345 <- 1/5/15min loadavg, runnable/total, last PID
# Open fds процесса
ls /proc/<PID>/fd/Pseudo-FS, экспортирующая kernel device model: устройства, драйверы, kernel modules, шины. /sys/class/ -- по классам (net, block, usb), /sys/devices/ -- по железной топологии, /sys/module/ -- загруженные модули. Можно читать (свойства устройства, статистика) и иногда писать (включить/выключить функцию, изменить настройку). udev использует sysfs для генерации /dev. Хорошо подходит для скриптов мониторинга: например, /sys/class/thermal/thermal_zone*/temp для CPU-температур.
# Сетевые интерфейсы
ls /sys/class/net/
# eth0 lo wlan0
# Скорость интерфейса
cat /sys/class/net/eth0/speed
# 1000
# Температуры CPU
for z in /sys/class/thermal/thermal_zone*/temp; do
echo $((`cat $z` / 1000))C
doneУтилита для трассировки syscalls процесса. Использует ptrace API kernel'а -- attach'ится к процессу и перехватывает каждый syscall, показывая аргументы и возвращаемое значение. Незаменимо для диагностики: 'почему программа упала?', 'к каким файлам обращается?', 'где зависла?'. Минусы: серьёзный slowdown (5-50x на syscall-heavy workload), требует CAP_SYS_PTRACE. Для production-критичных задач лучше eBPF/bpftrace -- меньше overhead.
# Все syscalls команды
strace ls /tmp 2>&1 | head
# Только определённые
strace -e openat,read,write ./app
# Прицепиться к запущенному процессу
strace -p <PID>
# Статистика
strace -c ./app
# % time seconds usecs/call calls syscall
# 50.0 0.002 1 2000 readАналог strace для библиотечных вызовов (dynamic linker hooks). Показывает вызовы malloc, free, strcpy, fopen и т.д. -- то что вызывается ВНУТРИ программы из glibc и других .so. В отличие от strace, не работает со статически слинкованными binaries и менее популярен (eBPF/uprobes для production удобнее). Полезно для отладки library-bug'ов в legacy-софте.
ltrace ls /tmp 2>&1 | head
# __libc_start_main(...) <unfinished ...>
# getenv('LS_COLORS') = NULL
# malloc(...) = 0x55...
# strcpy(0x55..., '/tmp') = 0x55...
# Прицепиться
ltrace -p <PID>Универсальный профайлер Linux, использующий perf_events kernel API. Считает hardware events (CPU cycles, cache-misses, branch-misses) через PMU процессора + software events (context switches, page faults). Делает statistical sampling: периодически записывает stack trace, по которому потом строится flame graph. Минимальный overhead, подходит для prod-профилирования. Главные команды: perf record (собрать), perf report (просмотр), perf top (live), perf stat (счётчики).
# Статистика выполнения
perf stat ./my_program
# 1,234,567 cycles
# 123,456 instructions # 0.10 insn per cycle
# 12,345 cache-misses
# Профиль (с stack traces)
sudo perf record -g ./my_program
perf report
# Live top по функциям
sudo perf topУтилита для мониторинга системных ресурсов: процессы (runnable/blocked), память (free, buffers, cache, swap), I/O (block in/out), system (interrupts, context switches), CPU (user/sys/idle/iowait/steal). Запускается с интервалом: vmstat 1 -- раз в секунду. Главные колонки для диагностики: r -- runnable queue (>cores значит CPU contention), wa -- iowait (медленный диск), cs -- context switches (если десятки тысяч -- много потоков или сигналов), si/so -- swap-in/out (если >0 -- активный swap, плохой знак).
vmstat 1 5
# procs ----memory---- ---swap-- ---io--- -system- -----cpu-----
# r b swpd free buff cache si so bi bo in cs us sy id wa st
# 1 0 0 1.2G 200M 13G 0 0 4 100 80 200 5 1 90 4 0
# 2 0 0 1.1G 200M 13G 0 0 0 0 500 1000 80 5 15 0 0Утилита для мониторинга block-устройств: throughput (kB/s read и write), IOPS (r/s, w/s), latency (await), utilization (%util). Из пакета sysstat. Главные метрики: await -- средняя latency запроса в ms (для SSD <1, для HDD <10 нормально); %util -- процент времени, когда устройство было занято (100% не всегда значит насыщение -- современные NVMe могут параллелить); rkB/s, wkB/s -- пропускная способность.
iostat -xz 1 5
# Device rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
# sda 0.00 2.00 1.0 50.0 32 800 16.3 0.5 10.0 5.0 10.1 0.5 25.5
# Просто общая статистика
iostatКлассическая интерактивная утилита: процессы отсортированы по CPU/MEM. Показывает: uptime, loadavg, общую статистику по процессам и CPU/RAM/swap, потом список процессов. Главные клавиши: P -- сорт по CPU, M -- по MEM, k -- kill процесс, c -- полная command line, 1 -- per-CPU stats. Колонки: %CPU (последний интервал), %MEM (RSS / total), VIRT (virtual size), RES (resident), SHR (shared).
top -b -n 1 | head -15
# top -- 14:30:00 up 5 days, 2:15, 2 users, load average: 0.50, 0.45, 0.30
# Tasks: 312 total, 1 running, 311 sleeping, 0 stopped, 0 zombie
# %Cpu(s): 5.0 us, 1.0 sy, 0.0 ni, 93.5 id, 0.5 wa, 0.0 hi, 0.0 si, 0.0 st
# MiB Mem: 16384 total, 1024 free, 8192 used, 7168 buff/cache
#
# PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMANDУлучшенная версия top: цветной интерфейс, scrollable список процессов, удобная навигация мышью, древовидный режим (F5), фильтр (F4), tree of children. Не требует sudo для базовых операций. Показывает per-CPU bars наглядно. Можно настраивать колонки (F2), сортировку (F6). Стандартная утилита для интерактивного мониторинга, но не подходит для скриптов -- для них ps и top -b.
htop
# Tab -- сворачивать tree-ветки
# F4 -- filter (по substring команды)
# F5 -- tree view
# F6 -- sort
# F9 -- kill
# / -- search
# F2 (Setup) -- настроить колонки: добавить IO_RATE, CGROUP, и др.Утилита просмотра kernel ring buffer: сообщения от драйверов, kernel-подсистем, hardware events. Сюда попадают: USB-устройства plug/unplug, OOM killer-сообщения, segfault'ы, ошибки файловых систем, BUG/WARN из kernel. На systemd-системах те же сообщения дублируются в journalctl -k. Полезно при диагностике железа и kernel-проблем. Без root читается если /proc/sys/kernel/dmesg_restrict=0.
# Последние сообщения с timestamps
dmesg -T | tail -20
# Только OOM-killer
dmesg -T | grep -i 'killed'
# Только конкретного типа
dmesg --level=err
# Follow (как tail -f)
dmesg -wTUserspace-демон, обрабатывающий device events от kernel (uevents). Динамически создаёт ноды в /dev при подключении устройства, выставляет права, запускает скрипты по правилам. Правила в /etc/udev/rules.d и /usr/lib/udev/rules.d -- по device attributes можно дать стабильное имя (например, флэшке -- одинаковый mount point независимо от того, какой /dev/sdX ей выдан). В systemd объединён с systemd-udevd.
# Текущие udev-events live
sudo udevadm monitor
# Атрибуты устройства
udevadm info -a -n /dev/sda1
# Reload правил
sudo udevadm control --reload-rules
sudo udevadm trigger
# /etc/udev/rules.d/99-myusb.rules
# ACTION=='add', SUBSYSTEM=='usb', ATTR{idVendor}=='1234', SYMLINK+='myusb'Прошивка материнской платы, исполняющаяся при включении (legacy boot). Делает POST (Power On Self Test), инициализирует базовое железо, ищет загрузочный device по списку boot order, читает MBR (Master Boot Record) -- первые 512 байт диска, передаёт ему управление. Ограничения: real mode 16-bit, диски до 2.2 TB (MBR), без secure boot. На современных машинах заменён UEFI; даже если есть 'legacy mode' -- через эмуляцию.
# Проверить, как загрузилась система
[ -d /sys/firmware/efi ] && echo UEFI || echo BIOS
# Boot order и сообщения
dmidecode -t bios | head
# BIOS Information
# Vendor: American Megatrends
# Version: 1.20
# Release Date: 03/2024Современная замена BIOS. 32/64-bit, поддерживает GPT-диски (до зеттабайт), secure boot (подписанные bootloader/kernel), graphical interface, network boot из коробки. Boot из EFI System Partition (ESP, FAT32) по пути типа /EFI/BOOT/BOOTX64.EFI или зарегистрированным в NVRAM entries. На Linux -- работа через efibootmgr и /sys/firmware/efi/. Secure Boot требует подписанного shim/grub/kernel, иначе придётся отключать или подписывать модули самому (MOK).
# Boot entries в NVRAM
sudo efibootmgr
# BootCurrent: 0001
# BootOrder: 0001,0000
# Boot0001* Linux Boot Manager
# Содержимое EFI partition
ls /boot/efi/EFI/
# BOOT systemd ubuntuСамый распространённый bootloader на Linux. Загружается прошивкой (BIOS из MBR или UEFI из ESP), показывает меню выбора kernel/OS, загружает выбранное ядро в память и передаёт ему управление с параметрами. Конфиг -- /etc/default/grub (high-level) + /etc/grub.d/ (scripts). После изменений: grub-mkconfig -o /boot/grub/grub.cfg. Из GRUB-меню можно редактировать параметры одноразово (e), что спасает при сломанной системе (single mode, init=/bin/bash).
# Реконфигурация
sudo grub-mkconfig -o /boot/grub/grub.cfg
# В режиме боли (boot не идёт):
# 1. Нажать e на нужной записи в меню GRUB
# 2. Добавить init=/bin/bash в линию linux
# 3. F10 -- boot
# Получаете root-shell с rw rootfsПервый процесс, запущенный kernel'ом после загрузки -- PID 1. Его задача -- запустить остальные сервисы, обработать сигналы, усыновлять сирот и wait'ить их. Исторически были разные init-системы: SysV init (через /etc/rc.d, последовательно), Upstart (Canonical, event-based), OpenRC. С 2015 года мейнстрим Linux -- systemd. PID 1 не может быть убит -- если упадёт, kernel panic.
# Что является init на системе
ls -la /sbin/init
# /sbin/init -> /lib/systemd/systemd
# PID 1
ps -p 1 -o pid,comm
# PID COMMAND
# 1 systemdСовременная init-система и system/service manager для Linux. Запускает и управляет сервисами (units): service, socket, timer, mount, path, scope, slice. Параллельный запуск с dependency tracking -- сильно быстрее SysV. Использует cgroups для группировки и ограничения процессов сервиса. Журналирует через journald (бинарные логи в /var/log/journal). Управляющие команды: systemctl (start/stop/status/enable), journalctl (логи), systemd-analyze (boot performance).
# Состояние сервиса
systemctl status nginx
# Включить автозагрузку и запустить
sudo systemctl enable --now nginx
# Логи сервиса
journalctl -u nginx -f
# Что тормозит загрузку
systemd-analyze blame | headУтилита для чтения systemd-журнала (бинарного лога от journald). Хранится в /var/log/journal/ или /run/log/journal/ (если persistent storage не настроен). Преимущества над plain text: индексированный поиск по unit/PID/UID/priority, structured fields, автоматическая ротация. Все системные логи (включая dmesg/kernel) в одном месте. Опции: -u <unit>, -p <priority>, -f (follow), --since/--until, -k (kernel-only), -b (current boot).
# Логи сервиса с момента старта
journalctl -u postgresql.service -b
# Реалтайм follow
sudo journalctl -f
# Только ошибки
sudo journalctl -p err
# Конкретный период
sudo journalctl --since '2026-05-18 10:00' --until '2026-05-18 12:00'