Системное программирование Linux
Концепции
User Space Kernel Space
───────────────────── ─────────────────────
Приложения (процессы) Ядро Linux
↓ системный вызов (syscall) ├── Scheduler (планировщик)
─────────────────────────→ ├── Memory Manager (VM)
←───────────────────────── ├── VFS (файловая система)
↑ результат / errno ├── Network Stack
├── Device Drivers
└── IPCПроцесс: экземпляр запущенной программы. Имеет собственное адресное пространство, файловые дескрипторы, PID. Изолирован от других процессов.
Поток (Thread): единица выполнения внутри процесса. Потоки одного процесса разделяют адресное пространство, файловые дескрипторы, но имеют собственный стек и регистры.
Системный вызов (syscall): интерфейс между user space и kernel. open(), read(), write(), fork(), mmap() и другие вызовы являются syscall-ами. Переход из user mode в kernel mode через прерывание.
Файловый дескриптор (fd): целое число, ссылка на открытый ресурс ядра (файл, сокет, pipe, устройство). Процесс работает с ресурсами только через fd.
Виртуальная память: каждый процесс видит своё изолированное адресное пространство. MMU + page table транслируют виртуальные адреса в физические. Процессы не видят память друг друга.
errno: глобальная переменная (thread-local), содержащая код последней ошибки syscall-а. Проверяй после каждого вызова, если он вернул ошибку.
Процессы
Адресное пространство процесса
Высокие адреса (0xFFFF...)
┌─────────────────────────┐
│ Kernel Space │ Недоступно из user space
├─────────────────────────┤
│ Stack ↓ │ Локальные переменные, адреса возврата
│ ... │ Растёт вниз
├─────────────────────────┤
│ │
│ (свободно / mmap) │ Динамически загружаемые библиотеки, mmap
│ │
├─────────────────────────┤
│ Heap ↑ │ malloc/new, растёт вверх
├─────────────────────────┤
│ BSS │ Неинициализированные глобальные переменные (нули)
├─────────────────────────┤
│ Data │ Инициализированные глобальные переменные
├─────────────────────────┤
│ Text (Code) │ Исполняемый код (read-only)
└─────────────────────────┘
Низкие адреса (0x0000...)Жизненный цикл
fork() → создать копию процесса (parent → child)
exec() → заменить образ процесса новой программой
wait() → родитель ждёт завершения ребёнка
exit() → завершить процесс// fork + exec — основной паттерн запуска программ
pid_t pid = fork();
if (pid == 0) {
// Дочерний процесс
execvp("ls", (char *[]){"ls", "-la", NULL});
perror("exec failed"); // сюда попадём только при ошибке exec
_exit(1);
} else if (pid > 0) {
// Родительский процесс
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("Child exited with code %d\n", WEXITSTATUS(status));
}
} else {
perror("fork failed");
}fork() создаёт почти полную копию процесса. Возвращает 0 в ребёнке, PID ребёнка в родителе. Использует Copy-on-Write (COW), при котором физические страницы памяти копируются только при записи.
Состояния процесса
fork()
↓
┌─► RUNNING ◄──── CPU выполняет
│ ↓ ↑
│ READY (в очереди планировщика)
│ ↓
│ SLEEPING ─── ожидание I/O, таймера, сигнала
│ ↓
│ STOPPED ──── SIGSTOP / SIGTSTP (Ctrl+Z)
│ ↓
└── ZOMBIE ───── завершён, но родитель не вызвал wait()
↓
REMOVED ──── родитель вызвал wait(), PID освобождёнD — Uninterruptible sleep (ожидание I/O, нельзя прервать сигналом)
R — Running / Runnable
S — Interruptible sleep (ожидание события)
T — Stopped (SIGSTOP)
Z — Zombie (завершён, ждёт wait() от родителя)Инструменты
# Информация о процессах
ps aux # все процессы
ps -ef # все процессы (другой формат)
ps -eLf # с потоками (LWP)
ps -o pid,ppid,state,rss,vsz,comm # кастомный формат
# Дерево процессов
pstree -p # с PID-ами
pstree -p <pid> # поддерево конкретного процесса
# Интерактивный мониторинг
top # классический
htop # удобнее (F6 сортировка, F5 дерево)
btop # современный
# Детали процесса
cat /proc/<pid>/status # статус, память, потоки
cat /proc/<pid>/maps # карта памяти
cat /proc/<pid>/fd/ # открытые файловые дескрипторы
ls -la /proc/<pid>/fd # то же через ls
cat /proc/<pid>/cmdline | tr '\0' ' ' # командная строка
cat /proc/<pid>/environ | tr '\0' '\n' # переменные окружения
cat /proc/<pid>/limits # лимиты (ulimit)
cat /proc/<pid>/io # статистика I/O
# Файловые дескрипторы
lsof -p <pid> # открытые файлы процесса
lsof -i :8080 # кто слушает порт 8080
lsof +D /path/to/dir # кто использует директорию
# Системные вызовы
strace -p <pid> # трассировка syscall-ов работающего процесса
strace -f -e trace=open,read,write cmd # трассировка конкретных syscall-ов
strace -c cmd # статистика syscall-ов (сколько, время)
strace -e trace=network cmd # только сетевые вызовы
# Библиотечные вызовы
ltrace cmd # трассировка вызовов libc
# Лимиты
ulimit -a # текущие лимиты
ulimit -n # макс. файловых дескрипторов
ulimit -n 65536 # установить (для текущего shell)
cat /proc/sys/fs/file-max # системный лимит
# /etc/security/limits.conf (постоянные лимиты)
# * soft nofile 65536
# * hard nofile 65536Zombie и Orphan
Zombie: процесс завершился, но родитель не вызвал wait(). Занимает PID и запись в таблице процессов. Решение: исправить родителя или убить родителя (init/systemd подберёт zombie).
Orphan: родитель завершился раньше ребёнка. Ребёнок «усыновляется» init (PID 1) или subreaper. Это нормально.
# Найти zombie-процессы
ps aux | awk '$8 == "Z"'Сигналы
Асинхронные уведомления процессу. Прерывают нормальное выполнение.
Сигнал Номер По умолчанию Когда
──────────────────────────────────────────────────────
SIGHUP 1 Завершение Терминал закрыт / перечитать конфиг
SIGINT 2 Завершение Ctrl+C
SIGQUIT 3 Завершение + core Ctrl+\
SIGKILL 9 Завершение Безусловное убийство (нельзя перехватить)
SIGSEGV 11 Завершение + core Обращение к невалидной памяти
SIGPIPE 13 Завершение Запись в pipe без читателя
SIGALRM 14 Завершение Таймер alarm()
SIGTERM 15 Завершение Вежливое завершение (можно перехватить)
SIGCHLD 17 Игнор Дочерний процесс завершился
SIGCONT 18 Продолжить Продолжить после SIGSTOP
SIGSTOP 19 Остановить Остановить процесс (нельзя перехватить)
SIGTSTP 20 Остановить Ctrl+Z
SIGUSR1 10 Завершение Пользовательский сигнал 1
SIGUSR2 12 Завершение Пользовательский сигнал 2kill <pid> # SIGTERM (вежливое завершение)
kill -9 <pid> # SIGKILL (безусловное)
kill -HUP <pid> # SIGHUP (перечитать конфиг)
kill -USR1 <pid> # SIGUSR1
kill -0 <pid> # проверить, жив ли процесс (не посылает сигнал)
killall nginx # по имени
pkill -f "python server.py" # по паттерну в командной строкеПравильное завершение:
1. Послать SIGTERM → процесс завершается gracefully (закрывает соединения, пишет данные)
2. Подождать (5-30 секунд)
3. Если не завершился → SIGKILL (принудительно, без cleanup)Обработка в коде
#include <signal.h>
volatile sig_atomic_t running = 1;
void handle_sigterm(int sig) {
running = 0; // только atomic-безопасные операции в обработчике!
}
int main() {
struct sigaction sa = {
.sa_handler = handle_sigterm,
.sa_flags = 0,
};
sigemptyset(&sa.sa_mask);
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
while (running) {
// основной цикл
}
// cleanup
return 0;
}// Go
import "os/signal"
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
defer stop()
<-ctx.Done()
// graceful shutdown# Python
import signal, sys
def handler(signum, frame):
print("Shutting down...")
sys.exit(0)
signal.signal(signal.SIGTERM, handler)
signal.signal(signal.SIGINT, handler)В обработчике сигнала нельзя: malloc, printf, mutex lock, вызывать любые async-signal-unsafe функции. Только атомарные записи в volatile sig_atomic_t или write() в fd.
Потоки (Threads)
POSIX Threads (pthreads)
#include <pthread.h>
void *worker(void *arg) {
int id = *(int *)arg;
printf("Thread %d started\n", id);
// работа...
return NULL;
}
int main() {
pthread_t threads[4];
int ids[4];
for (int i = 0; i < 4; i++) {
ids[i] = i;
pthread_create(&threads[i], NULL, worker, &ids[i]);
}
for (int i = 0; i < 4; i++) {
pthread_join(threads[i], NULL); // ждать завершения
}
}
// Компиляция: gcc -pthread program.c
Модели многопоточности
1:1 — один user thread = один kernel thread (Linux pthreads, Go runtime)
Полный параллелизм, планировщик ОС.
M:N — M user threads на N kernel threads (Go goroutines, Erlang processes)
User-space планировщик + kernel threads.
Go: goroutine = лёгкий «поток» (~2KB стека), M:N на GOMAXPROCS kernel threads.
Green threads — все в user space, нет параллелизма (Python GIL, Ruby GIL).
Конкурентность есть, параллелизм — нет.Конкурентность vs Параллелизм
Конкурентность (Concurrency):
Управление несколькими задачами одновременно.
Может быть на 1 ядре (переключение контекста).
Структура программы.
Параллелизм (Parallelism):
Выполнение нескольких задач одновременно.
Требует несколько ядер/процессоров.
Способ выполнения.
Конкурентность без параллелизма: event loop (Node.js), async/await (Python asyncio)
Параллелизм без конкурентности: SIMD, GPU-вычисления
Оба: Go goroutines, Rust tokio, Java virtual threadsПримитивы синхронизации
Mutex (Mutual Exclusion):
lock() → захватить (если занят — ждать)
unlock() → освободить
Защищает критическую секцию. Один поток одновременно.
RWLock (Read-Write Lock):
read_lock() → несколько читателей одновременно
write_lock() → эксклюзивный доступ (ждёт пока все читатели отпустят)
Оптимизация для read-heavy нагрузки.
Semaphore:
wait() / acquire() → уменьшить счётчик (если 0 — ждать)
post() / release() → увеличить счётчик
Ограничивает одновременный доступ N потокам.
Condition Variable:
wait(mutex) → отпустить mutex + заснуть, при пробуждении — захватить mutex
signal() / notify() → разбудить один ожидающий поток
broadcast() → разбудить все ожидающие потоки
Spinlock:
Как mutex, но вместо сна — busy-wait (крутится в цикле).
Для очень коротких критических секций. Не спит — тратит CPU.
Atomic operations:
compare_and_swap (CAS), fetch_and_add, load, store
Без блокировок (lock-free). Для счётчиков, флагов.// Mutex
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex);
// критическая секция
shared_counter++;
pthread_mutex_unlock(&mutex);
// Condition Variable — паттерн producer/consumer
pthread_mutex_lock(&mutex);
while (queue_empty()) { // while, не if! (spurious wakeup)
pthread_cond_wait(&cond, &mutex);
}
item = dequeue();
pthread_mutex_unlock(&mutex);Проблемы многопоточности
Data Race:
Два потока одновременно обращаются к одной памяти, хотя бы один пишет.
Результат недетерминирован.
Решение: mutex, atomic, не делить mutable state.
Deadlock:
Поток A держит lock1 и ждёт lock2.
Поток B держит lock2 и ждёт lock1.
Оба ждут вечно.
Решение: всегда захватывать locks в одном порядке, использовать trylock с таймаутом.
Livelock:
Потоки реагируют друг на друга, но не продвигаются.
Как два человека, уступающих друг другу в коридоре.
Priority Inversion:
Низкоприоритетный поток держит lock, высокоприоритетный ждёт.
Среднеприоритетный вытесняет низкоприоритетный → высокоприоритетный голодает.
Решение: priority inheritance.
False Sharing:
Потоки пишут в разные переменные, но они попали в одну cache line (64 байта).
Каждая запись инвалидирует cache line на всех ядрах.
Решение: padding (выравнивание по cache line).Инструменты
# Потоки процесса
ps -eLf | grep <process> # LWP = thread ID
ls /proc/<pid>/task/ # директории потоков
cat /proc/<pid>/status | grep Threads # количество потоков
# Thread sanitizer (компиляция)
gcc -fsanitize=thread -g program.c # обнаруживает data races
go test -race ./... # Go race detector
# Valgrind
valgrind --tool=helgrind ./program # обнаружение data races
valgrind --tool=drd ./program # альтернативный детекторПамять
Виртуальная память
Виртуальный адрес → Page Table → Физический адрес
Page (страница) — минимальная единица: 4KB (обычно)
Huge Page — 2MB или 1GB (для больших объёмов данных)
TLB (Translation Lookaside Buffer) — кеш page table в CPU.
TLB miss → обращение к page table в RAM (медленно).Page Fault
Minor page fault:
Страница есть в RAM, но нет в page table процесса.
Быстро — обновить page table.
Пример: COW после fork(), доступ к mmap-файлу уже в page cache.
Major page fault:
Страницы нет в RAM — нужно читать с диска.
Медленно (~10ms для HDD, ~0.1ms для SSD).
Пример: swap in, первое обращение к mmap-файлу.malloc и аллокаторы
malloc(size):
Маленькие аллокации (<128KB) → brk()/sbrk() → расширение heap
Большие аллокации (≥128KB) → mmap() → отдельный регион
free(ptr):
Память возвращается аллокатору, но не всегда ОС.
Аллокатор переиспользует освобождённые блоки.
Аллокаторы:
glibc (ptmalloc2) — стандартный, потокобезопасный, per-thread arenas
jemalloc — Facebook, меньше фрагментация (Redis, Rust)
tcmalloc — Google, per-thread cache (Go runtime основан на идее)
mimalloc — Microsoft, компактный, быстрыйmmap
// Маппинг файла в память
int fd = open("data.bin", O_RDONLY);
void *ptr = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
// ptr указывает на содержимое файла — доступ как к массиву
// Данные подгружаются лениво (page fault при первом обращении)
munmap(ptr, file_size);
// Анонимный маппинг (не привязан к файлу)
void *mem = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
// Shared маппинг (IPC — несколько процессов видят одну память)
void *shared = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);MAP_PRIVATE — изменения видны только этому процессу (COW)
MAP_SHARED — изменения видны всем, записываются в файл
MAP_ANONYMOUS — без файла (аналог malloc для больших блоков)OOM Killer
Когда физическая память исчерпана, ядро убивает процесс с наибольшим oom_score:
# OOM score процесса (чем выше — тем вероятнее будет убит)
cat /proc/<pid>/oom_score
# Защитить процесс от OOM killer
echo -1000 > /proc/<pid>/oom_score_adj # никогда не убивать
echo 1000 > /proc/<pid>/oom_score_adj # убить первым
# Отключить overcommit (строгий режим)
# /etc/sysctl.conf:
# vm.overcommit_memory = 2
# vm.overcommit_ratio = 80Overcommit: Linux по умолчанию разрешает malloc() больше памяти, чем физически доступно. Реальная память выделяется при первой записи. Если её не хватает, срабатывает OOM killer.
Инструменты
# Память системы
free -h # общее состояние
vmstat 1 # виртуальная память (каждую секунду)
cat /proc/meminfo # детальная информация
# Память процесса
cat /proc/<pid>/status | grep -E 'VmRSS|VmSize|VmPeak'
# VmSize — виртуальная (выделено)
# VmRSS — резидентная (реально в RAM)
# VmPeak — максимум за время жизни
pmap -x <pid> # карта памяти процесса
smem -p # USS/PSS/RSS для всех процессов
# USS — уникальная память (не shared)
# PSS — пропорциональная (shared / кол-во процессов)
# RSS — резидентная (включая shared)
# Утечки памяти
valgrind --leak-check=full ./program # C/C++
# Go: pprof (net/http/pprof)
# Python: tracemalloc, objgraph
# Java: jmap, jcmd, VisualVM
# Perf (профилирование)
perf stat ./program # счётчики (cycles, instructions, cache misses)
perf record -g ./program # запись профиля
perf report # анализИерархия памяти
Размер Задержка Пропускная способность
──────────────────────────────────────────────────────────────────────
L1 cache (ядро) 32-64 KB ~1 ns ~1 TB/s
L2 cache (ядро) 256 KB-1 MB ~3-5 ns ~500 GB/s
L3 cache (shared) 8-64 MB ~10-20 ns ~200 GB/s
RAM 16-512 GB ~50-100 ns ~50 GB/s
SSD (NVMe) TB ~10-100 µs ~5 GB/s
HDD TB ~5-10 ms ~200 MB/s
Сеть (LAN) — ~0.5 ms ~10 Gbps
Сеть (Internet) — ~10-100 ms variesЧисла, которые нужно знать:
L1 cache reference: 1 ns
L2 cache reference: 4 ns
RAM reference: 100 ns
SSD random read: 16,000 ns = 16 µs
HDD seek: 10,000,000 ns = 10 ms
Отправка пакета в LAN: 500,000 ns = 0.5 ms
Round-trip в датацентре: 1,000,000 ns = 1 msCache line = 64 байта. Последовательный доступ к данным (arrays) гораздо быстрее случайного (linked lists, hash maps) из-за CPU prefetch и cache locality.
Файловая система и I/O
Файловые дескрипторы
Каждый процесс имеет таблицу fd:
0 — stdin
1 — stdout
2 — stderr
3, 4, 5, ... — открытые файлы, сокеты, pipes
fd → File Description (в ядре) → inode → данные на диске// Базовые операции
int fd = open("/path/to/file", O_RDONLY); // открыть
int fd = open("/path/to/file", O_WRONLY | O_CREAT | O_TRUNC, 0644); // создать
ssize_t n = read(fd, buf, sizeof(buf)); // прочитать
ssize_t n = write(fd, buf, len); // записать
off_t pos = lseek(fd, 0, SEEK_SET); // перемотать
close(fd); // закрыть
// Флаги open():
O_RDONLY — только чтение
O_WRONLY — только запись
O_RDWR — чтение и запись
O_CREAT — создать если не существует
O_TRUNC — обрезать до 0 при открытии
O_APPEND — писать в конец (атомарно)
O_NONBLOCK — неблокирующий режим
O_CLOEXEC — закрыть при exec() (предотвращает утечку fd в дочерние процессы)
O_DIRECT — bypass page cache (прямой I/O)
O_SYNC — синхронная запись (ждать записи на диск)Буферизация
Приложение → libc buffer (fwrite) → kernel page cache → диск
User Space Kernel Space Hardware
┌──────────┐ ┌────────────┐ ┌──────┐
fwrite() ──────→ │ stdio │ ──write()→ │ page cache │ ──────→│ disk │
│ buffer │ │ (RAM) │ │ │
└──────────┘ └────────────┘ └──────┘
fflush() fsync()/fdatasync()
сбросить сбросить
в ядро на диск
fflush() — сбросить libc буфер → kernel page cache
fsync(fd) — сбросить page cache → физический диск (данные + метаданные)
fdatasync(fd)— сбросить только данные (без метаданных, быстрее)
sync() — сбросить все буферы всех файловI/O модели
1. Блокирующий I/O (по умолчанию):
read(fd) → процесс спит, пока данные не будут готовы.
Прост, но один поток обслуживает один fd.
2. Неблокирующий I/O:
O_NONBLOCK: read(fd) → если данных нет, сразу EAGAIN.
Нужно poll-ить в цикле (busy wait) или с мультиплексированием.
3. I/O мультиплексирование (event-driven):
epoll / select / poll: ожидать события на многих fd одновременно.
Один поток обслуживает тысячи соединений.
4. Асинхронный I/O:
io_uring (Linux 5.1+): отправить запрос → ядро выполнит → уведомит.
Нет блокировки, нет syscall на каждую операцию.epoll: основа всех event loop-ов
// epoll — эффективное мультиплексирование I/O (Linux)
// Используется в: nginx, Node.js (libuv), Go runtime, Redis, ...
int epfd = epoll_create1(0);
struct epoll_event ev = {
.events = EPOLLIN | EPOLLET, // чтение, edge-triggered
.data.fd = server_fd,
};
epoll_ctl(epfd, EPOLL_CTL_ADD, server_fd, &ev);
struct epoll_event events[MAX_EVENTS];
while (1) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1); // -1 = ждать бесконечно
for (int i = 0; i < n; i++) {
if (events[i].data.fd == server_fd) {
// новое соединение → accept()
} else {
// данные готовы → read()
}
}
}select() — старый, лимит 1024 fd, O(n) на каждый вызов
poll() — без лимита на количество fd, но всё ещё O(n)
epoll() — O(1) для ожидания, O(n) только для готовых fd. Linux-specific.
kqueue() — аналог epoll для BSD/macOS
Level-triggered (LT): уведомлять пока данные доступны (по умолчанию, проще)
Edge-triggered (ET): уведомлять один раз при появлении данных (быстрее, сложнее)io_uring
Современный асинхронный I/O (Linux 5.1+). Submission Queue + Completion Queue.
Минимум syscall-ов: один вызов может отправить множество запросов.
Используется: Rust (tokio, glommio), Java (Netty), базы данных.
Submission Queue (SQ) → ядро обрабатывает → Completion Queue (CQ)
[read, write, ...] [результаты]
Shared memory между user space и kernel — zero-copy для метаданных.Полезные утилиты для I/O
# Мониторинг I/O
iostat -x 1 # статистика дисков (каждую секунду)
iotop # процессы с наибольшим I/O
pidstat -d 1 # I/O по процессам
# Файловая система
df -h # использование дисков
du -sh /path # размер директории
findmnt # точки монтирования
stat /path/to/file # метаданные файла (inode, size, timestamps)
filefrag -v /path/to/file # фрагментация файла
# Производительность
dd if=/dev/zero of=test bs=1M count=1024 oflag=direct # тест записи (~sequential)
fio --name=randread --ioengine=libaio --direct=1 \
--bs=4k --size=1G --numjobs=4 --rw=randread # тест случайного чтенияСеть
Сокеты
// TCP-сервер (упрощённо)
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = INADDR_ANY,
.sin_port = htons(8080),
};
bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
listen(server_fd, 128); // backlog = 128
while (1) {
int client_fd = accept(server_fd, NULL, NULL);
// обработать client_fd (read/write)
close(client_fd);
}socket() → создать сокет (fd)
bind() → привязать к адресу:порту
listen() → начать принимать соединения (backlog — очередь ожидания)
accept() → принять соединение → новый fd для клиента
connect() → установить соединение (клиент)
send() / recv() → отправить / получить данные
close() → закрыть сокетTCP-стек
Клиент Сервер
│ │
│──── SYN ────────────────→│ 3-way handshake
│←─── SYN-ACK ────────────│
│──── ACK ────────────────→│
│ │
│←──→ DATA ←──────────────→│ передача данных
│ │
│──── FIN ────────────────→│ 4-way handshake (закрытие)
│←─── ACK ────────────────│
│←─── FIN ────────────────│
│──── ACK ────────────────→│Состояния TCP-соединения
LISTEN — сервер ждёт подключений
ESTABLISHED — соединение активно
TIME_WAIT — соединение закрыто, ждём 2*MSL (~60с) для поздних пакетов
CLOSE_WAIT — получен FIN, ждём close() от приложения
FIN_WAIT_1/2 — инициировали закрытие, ждём подтверждения# Состояния соединений
ss -tan # все TCP-соединения
ss -tan state time-wait # только TIME_WAIT
ss -tlnp # слушающие порты + PID
ss -s # статистика
# Если много TIME_WAIT:
cat /proc/sys/net/ipv4/tcp_tw_reuse # разрешить переиспользование (1)
sysctl net.ipv4.tcp_tw_reuse=1Socket options
// Переиспользование адреса (быстрый рестарт сервера)
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int));
// Переиспользование порта (несколько процессов слушают один порт)
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &(int){1}, sizeof(int));
// Keep-alive
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &(int){1}, sizeof(int));
// TCP_NODELAY — отключить Nagle's algorithm (меньше задержка)
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &(int){1}, sizeof(int));
// Размер буферов
setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &(int){65536}, sizeof(int));
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &(int){65536}, sizeof(int));
// Таймаут
struct timeval tv = {.tv_sec = 5};
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));Сетевые инструменты
# DNS
dig example.com # DNS-запрос
dig +short example.com A # только IP
nslookup example.com
host example.com
resolvectl query example.com # systemd-resolved
# Подключение и отладка
curl -v http://localhost:8080 # HTTP-запрос с подробным выводом
curl -w "%{time_total}\n" -o /dev/null -s http://localhost:8080 # время ответа
nc -zv host 8080 # проверить порт (TCP)
nc -l 8080 # слушать порт
telnet host 8080 # подключиться к порту
# Трафик
tcpdump -i eth0 port 8080 # перехват пакетов
tcpdump -i any -A port 80 # HTTP-трафик в ASCII
tcpdump -i eth0 -w capture.pcap # записать в файл
tshark -r capture.pcap # анализ (Wireshark CLI)
# Маршрутизация
ip route # таблица маршрутизации
ip addr # сетевые интерфейсы
traceroute example.com # маршрут до хоста
mtr example.com # traceroute + ping (интерактивный)
# Производительность сети
iperf3 -s # сервер
iperf3 -c host # клиент (тест пропускной способности)
# Настройки ядра
sysctl net.core.somaxconn # максимальный backlog (по умолчанию 4096)
sysctl net.ipv4.ip_local_port_range # диапазон ephemeral-портов
sysctl net.ipv4.tcp_max_syn_backlog # SYN backlog
sysctl net.core.netdev_max_backlog # очередь входящих пакетовIPC (межпроцессное взаимодействие)
Pipe
# Анонимный pipe (|) — однонаправленный, между родственными процессами
ls -la | grep ".txt" | wc -l
# Named pipe (FIFO) — между любыми процессами
mkfifo /tmp/myfifo
echo "hello" > /tmp/myfifo & # блокируется, пока никто не читает
cat /tmp/myfifo # → hello// Анонимный pipe
int pipefd[2]; // pipefd[0] = read end, pipefd[1] = write end
pipe(pipefd);
if (fork() == 0) {
close(pipefd[0]);
write(pipefd[1], "hello", 5);
close(pipefd[1]);
_exit(0);
} else {
close(pipefd[1]);
char buf[5];
read(pipefd[0], buf, 5);
close(pipefd[0]);
wait(NULL);
}Unix Domain Socket
Как TCP-сокет, но для процессов на одной машине. Быстрее TCP (нет сетевого стека).
Файл в файловой системе: /var/run/app.sock# Подключиться к unix socket
curl --unix-socket /var/run/docker.sock http://localhost/containers/json
nc -U /var/run/app.sock
socat - UNIX-CONNECT:/var/run/app.sockShared Memory
// POSIX shared memory — самый быстрый IPC (нет копирования)
int fd = shm_open("/myshm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 4096);
void *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// ptr доступен нескольким процессам
// Синхронизация — через семафоры или futex
munmap(ptr, 4096);
shm_unlink("/myshm");Сравнение IPC
Метод Скорость Сложность Направление
──────────────────────────────────────────────────────────
Pipe средняя низкая однонаправленный
Named Pipe (FIFO) средняя низкая однонаправленный
Unix Socket высокая средняя двунаправленный
Shared Memory макс. высокая двунаправленный
Signal — низкая уведомление (без данных)
Message Queue средняя средняя двунаправленный (mq_*)
File (+ flock) низкая низкая двунаправленныйКонтейнеры изнутри
Контейнеры не являются виртуальными машинами. Это процессы с изоляцией через механизмы ядра:
Namespaces (изоляция)
PID namespace — свои PID-ы (PID 1 внутри контейнера)
Network namespace — своя сеть (интерфейсы, IP, порты, iptables)
Mount namespace — своя файловая система (chroot на стероидах)
UTS namespace — свой hostname
User namespace — свои UID/GID (root в контейнере ≠ root на хосте)
IPC namespace — своя shared memory, semaphores
Cgroup namespace — свой view на cgroups# Создать процесс в новых namespaces
unshare --pid --fork --mount-proc bash # новый PID namespace
# ps aux покажет только процессы внутри namespace
# Войти в namespace другого процесса
nsenter -t <pid> -p -n -m bash # PID + Net + Mount namespaces контейнера
nsenter -t $(docker inspect -f '{{.State.Pid}}' mycontainer) -p -n bash
# Просмотр namespaces
ls -la /proc/<pid>/ns/ # namespaces процесса
lsns # все namespaces в системеCgroups (ограничение ресурсов)
# cgroups v2 (unified hierarchy)
# /sys/fs/cgroup/
# CPU
echo 100000 > /sys/fs/cgroup/mygroup/cpu.max # 100ms из 100ms (100% одного ядра)
echo "50000 100000" > /sys/fs/cgroup/mygroup/cpu.max # 50% одного ядра
# Память
echo 256M > /sys/fs/cgroup/mygroup/memory.max # лимит 256MB
echo 128M > /sys/fs/cgroup/mygroup/memory.high # throttling с 128MB
# Процессы
echo <pid> > /sys/fs/cgroup/mygroup/cgroup.procs # добавить процесс
# Docker использует cgroups автоматически:
docker run --memory=256m --cpus=0.5 myappCapabilities (гранулярные права root)
Вместо «root или не root» — набор отдельных привилегий:
CAP_NET_BIND_SERVICE — биндить порты <1024 (без root)
CAP_NET_RAW — raw sockets (ping)
CAP_SYS_PTRACE — ptrace (strace, debugger)
CAP_SYS_ADMIN — mount, sethostname, ... (почти как root)
CAP_DAC_OVERRIDE — обходить проверки прав файлов
CAP_CHOWN — менять владельца файлов# Посмотреть capabilities процесса
getpcaps <pid>
cat /proc/<pid>/status | grep Cap
# Дать бинарнику capability
sudo setcap cap_net_bind_service=+ep /path/to/binary
# Теперь binary может слушать порт 80 без root
# Docker: урезать capabilities
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myappseccomp (фильтрация syscall-ов)
# Docker по умолчанию блокирует ~44 syscall-а
# Например: mount, reboot, kexec_load, ...
# Свой профиль
docker run --security-opt seccomp=profile.json myapp
# Отключить (не для prod)
docker run --security-opt seccomp=unconfined myappSystemd
# Управление сервисами
systemctl start nginx
systemctl stop nginx
systemctl restart nginx
systemctl reload nginx # перечитать конфиг без рестарта
systemctl status nginx # статус + последние логи
systemctl enable nginx # автозапуск при загрузке
systemctl disable nginx
systemctl is-active nginx
systemctl is-enabled nginx
# Логи
journalctl -u nginx # логи сервиса
journalctl -u nginx -f # follow
journalctl -u nginx --since "1 hour ago"
journalctl -u nginx -p err # только ошибки
journalctl -k # логи ядра (kernel)
journalctl --disk-usage # сколько места занимают логиUnit-файл
# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
After=network.target postgresql.service
Wants=postgresql.service
[Service]
Type=simple # simple, forking, oneshot, notify
User=app
Group=app
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/server
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
StartLimitBurst=3
StartLimitIntervalSec=60
# Безопасность
NoNewPrivileges=yes
ProtectSystem=strict # read-only /usr, /boot, /efi
ProtectHome=yes # /home, /root, /run/user недоступны
ReadWritePaths=/var/lib/myapp /var/log/myapp
PrivateTmp=yes # изолированный /tmp
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
# Лимиты
LimitNOFILE=65536
MemoryMax=512M
CPUQuota=200% # 2 ядра
# Окружение
Environment=ENV=production
EnvironmentFile=/etc/myapp/env
[Install]
WantedBy=multi-user.target# После изменения unit-файла
systemctl daemon-reload
systemctl restart myapp
# Проверить конфигурацию
systemd-analyze verify /etc/systemd/system/myapp.service
systemd-analyze security myapp # аудит безопасностиПроизводительность: инструменты
Общая картина
# Быстрая диагностика (60-секундный чеклист)
uptime # load average
dmesg -T | tail # ошибки ядра
vmstat 1 5 # CPU, memory, I/O
mpstat -P ALL 1 # CPU по ядрам
pidstat 1 # CPU по процессам
iostat -xz 1 # дисковый I/O
free -h # память
sar -n DEV 1 # сетевой трафик
ss -tlnp # слушающие портыCPU
# Load average
uptime
# 3 числа: за 1, 5, 15 минут
# load = количество процессов в Running + Waiting for I/O
# load = число ядер → CPU загружен на 100%
# load > число ядер → очередь, процессы ждут
nproc # количество ядер
cat /proc/cpuinfo # информация о CPU
# По процессам
top -o %CPU # сортировка по CPU
pidstat 1 # CPU по процессам/потокам
# Профилирование
perf top # real-time профиль (какие функции жгут CPU)
perf record -g -p <pid> -- sleep 10 # запись 10 секунд
perf report # анализ
# Flame graph
perf record -g -p <pid> -- sleep 30
perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svgДиск
iostat -x 1
# %util — загруженность диска (>80% = bottleneck)
# await — среднее время ожидания I/O (ms)
# r/s, w/s — операции чтения/записи в секунду
# По процессам
iotop -oPa # только активные, накопительно
pidstat -d 1 # I/O по процессамСеть
# Пропускная способность
sar -n DEV 1 # трафик по интерфейсам
nload # визуальный мониторинг
iftop # трафик по соединениям
# Соединения
ss -s # статистика (total, established, time-wait, ...)
ss -tan state established | wc -l # количество established
conntrack -L | wc -l # количество отслеживаемых соединений (NAT)
# Пакеты
cat /proc/net/netstat | awk '{print $1}' # статистика TCP
netstat -s # то же, человеко-читаемо
nstat -sz # дельта-статистика
# Потери и ретрансмиты
ss -ti # TCP info (retransmits, cwnd, rtt)
netstat -s | grep -i retrans/proc и /sys (виртуальные файловые системы)
# /proc — информация о процессах и ядре
/proc/<pid>/ — всё о конкретном процессе
/proc/cpuinfo — информация о CPU
/proc/meminfo — информация о памяти
/proc/loadavg — load average
/proc/net/ — сетевая статистика
/proc/sys/ — настройки ядра (sysctl)
# /sys — информация об устройствах и драйверах
/sys/class/net/ — сетевые интерфейсы
/sys/block/ — блочные устройства
/sys/fs/cgroup/ — cgroups
# Изменение параметров ядра
sysctl -a # все параметры
sysctl vm.swappiness # конкретный
sysctl -w vm.swappiness=10 # изменить (до перезагрузки)
# /etc/sysctl.conf — постоянные измененияТюнинг для высоконагруженного сервера
# /etc/sysctl.conf
# Память
vm.swappiness = 10 # использовать swap неохотно
vm.overcommit_memory = 0 # эвристический overcommit
# Сеть
net.core.somaxconn = 65535 # backlog для listen()
net.core.netdev_max_backlog = 65535 # очередь входящих пакетов
net.ipv4.tcp_max_syn_backlog = 65535 # SYN backlog
net.ipv4.ip_local_port_range = 1024 65535 # ephemeral-порты
net.ipv4.tcp_tw_reuse = 1 # переиспользование TIME_WAIT
net.ipv4.tcp_fin_timeout = 15 # таймаут FIN_WAIT_2
net.ipv4.tcp_keepalive_time = 300 # keepalive через 5 минут
net.ipv4.tcp_keepalive_intvl = 30 # интервал keepalive
net.ipv4.tcp_keepalive_probes = 5 # попыток keepalive
net.ipv4.tcp_slow_start_after_idle = 0 # не сбрасывать cwnd при idle
# Файлы
fs.file-max = 2097152 # максимум fd в системе
fs.inotify.max_user_watches = 524288 # для file watchers (IDE, hot reload)
# /etc/security/limits.conf
# * soft nofile 65536
# * hard nofile 65536Частые проблемы
Процесс не отвечает (D-state):
ps aux | awk '$8 ~ /D/' # найти процессы в D-state
cat /proc/<pid>/wchan # на чём заблокирован
cat /proc/<pid>/stack # стек ядра (kernel stack)
dmesg -T | tail # ошибки ядраD-state = uninterruptible sleep, обычно ожидание I/O. Даже kill -9 не поможет. Причины: зависший NFS, проблемы с диском, баг в драйвере.
Too many open files:
# Проверить лимит
ulimit -n # текущий лимит
cat /proc/<pid>/limits | grep "open files"
# Сколько открыто
ls /proc/<pid>/fd | wc -l
lsof -p <pid> | wc -l
# Увеличить
ulimit -n 65536 # для текущего shell
# Или через systemd: LimitNOFILE=65536
# Или /etc/security/limits.confВысокий load average при низком CPU:
# Load average включает процессы, ожидающие I/O (D-state)
vmstat 1 # wa = I/O wait
iostat -x 1 # %util дискаПричина: I/O bottleneck (медленный диск, NFS, swap). Решение: оптимизировать I/O, добавить RAM, перейти на SSD.
OOM killer убивает процессы:
dmesg -T | grep -i "oom\|killed" # кто был убит
journalctl -k | grep -i oom
# Защитить важный процесс
echo -1000 > /proc/<pid>/oom_score_adj
# Найти, кто жрёт память
ps aux --sort=-%mem | head -20
smem -rs pss | tail -20Port already in use:
ss -tlnp | grep :8080 # кто слушает порт
fuser -k 8080/tcp # убить процесс на порту (осторожно)
# Причина: TIME_WAIT от предыдущего процесса
# Решение: SO_REUSEADDR в коде или:
sysctl net.ipv4.tcp_tw_reuse=1Утечка файловых дескрипторов:
# Мониторинг количества открытых fd
watch -n1 'ls /proc/<pid>/fd | wc -l'
# Что именно открыто
ls -la /proc/<pid>/fd # типы: socket, pipe, файлы
lsof -p <pid>Причина: не закрытые файлы, сокеты, pipes в коде. В C используйте close(fd) в finally / defer / destructor. В Go используйте defer f.Close(). Утечка fd приводит к “too many open files”.
Swap thrashing (система еле шевелится):
free -h # Swap used?
vmstat 1 # si/so (swap in/out) > 0?
swapon --show # swap-устройства
# Уменьшить использование swap
sysctl vm.swappiness=10
# Или добавить RAM / убить жрущие процессы