Системное программирование Linux

Концепции

Linux Man Pages | Linux Kernel Docs

Архитектура: User Space и Kernel Space

User Space — Приложения (процессы) выполняют системный вызов (syscall) для взаимодействия с ядром и получают результат / errno.

Kernel Space — Ядро Linux:

  • Scheduler (планировщик)
  • Memory Manager (VM)
  • VFS (файловая система)
  • 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…) – Низкие адреса (0x0000…):

  • Kernel Space – Недоступно из user space
  • Stack (растёт вниз) – Локальные переменные, адреса возврата
  • (свободно / mmap) – Динамически загружаемые библиотеки, mmap
  • Heap (растёт вверх) – malloc/new
  • BSS – Неинициализированные глобальные переменные (нули)
  • Data – Инициализированные глобальные переменные
  • Text (Code) – Исполняемый код (read-only)

Жизненный цикл

Жизненный цикл процесса
  • 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  65536

Zombie и Orphan

Zombie: процесс завершился, но родитель не вызвал wait(). Занимает PID и запись в таблице процессов. Решение: исправить родителя или убить родителя (init/systemd подберёт zombie).

Orphan: родитель завершился раньше ребёнка. Ребёнок «усыновляется» init (PID 1) или subreaper. Это нормально.

# Найти zombie-процессы
ps aux | awk '$8 == "Z"'

Сигналы

Асинхронные уведомления процессу. Прерывают нормальное выполнение.

СигналНомерПо умолчаниюКогда
SIGHUP1ЗавершениеТерминал закрыт / перечитать конфиг
SIGINT2ЗавершениеCtrl+C
SIGQUIT3Завершение + coreCtrl+\
SIGKILL9ЗавершениеБезусловное убийство (нельзя перехватить)
SIGSEGV11Завершение + coreОбращение к невалидной памяти
SIGPIPE13ЗавершениеЗапись в pipe без читателя
SIGALRM14ЗавершениеТаймер alarm()
SIGTERM15ЗавершениеВежливое завершение (можно перехватить)
SIGCHLD17ИгнорДочерний процесс завершился
SIGCONT18ПродолжитьПродолжить после SIGSTOP
SIGSTOP19ОстановитьОстановить процесс (нельзя перехватить)
SIGTSTP20ОстановитьCtrl+Z
SIGUSR110ЗавершениеПользовательский сигнал 1
SIGUSR212ЗавершениеПользовательский сигнал 2
kill <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 Параллелизм

Конкурентность 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

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 и аллокаторы

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);
Флаги mmap
  • 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 = 80

Overcommit: 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
RAM16-512 GB~50-100 ns~50 GB/s
SSD (NVMe)TB~10-100 us~5 GB/s
HDDTB~5-10 ms~200 MB/s
Сеть (LAN)~0.5 ms~10 Gbps
Сеть (Internet)~10-100 msvaries
Числа, которые нужно знать
  • L1 cache reference: 1 ns
  • L2 cache reference: 4 ns
  • RAM reference: 100 ns
  • SSD random read: 16,000 ns = 16 us
  • HDD seek: 10,000,000 ns = 10 ms
  • Отправка пакета в LAN: 500,000 ns = 0.5 ms
  • Round-trip в датацентре: 1,000,000 ns = 1 ms

Cache 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       синхронная запись (ждать записи на диск)

Буферизация

Буферизация I/O

Путь данных: Приложение – libc buffer (fwrite) – kernel page cacheдиск

User Space: fwrite() записывает в stdio buffer. fflush() сбрасывает буфер в ядро.

Kernel Space: данные попадают в page cache (RAM). fsync()/fdatasync() сбрасывают на диск.

  • fflush() – сбросить libc буфер в kernel page cache
  • fsync(fd) – сбросить page cache на физический диск (данные + метаданные)
  • fdatasync(fd) – сбросить только данные (без метаданных, быстрее)
  • sync() – сбросить все буферы всех файлов

I/O модели

Модели 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

io_uring

Современный асинхронный I/O (Linux 5.1+). Submission Queue + Completion Queue. Минимум syscall-ов: один вызов может отправить множество запросов.

Используется: Rust (tokio, glommio), Java (Netty), базы данных.

Submission Queue (SQ) [read, write, …] – ядро обрабатывает – Completion Queue (CQ) [результаты]. 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 API
  • socket() – создать сокет (fd)
  • bind() – привязать к адресу:порту
  • listen() – начать принимать соединения (backlog – очередь ожидания)
  • accept() – принять соединение – новый fd для клиента
  • connect() – установить соединение (клиент)
  • send() / recv() – отправить / получить данные
  • close() – закрыть сокет

TCP-стек

TCP-стек: установка и закрытие соединения

3-way handshake (установка):

  1. Клиент – SYN – Сервер
  2. Сервер – SYN-ACK – Клиент
  3. Клиент – ACK – Сервер

Передача данных: Клиент <– DATA –> Сервер

4-way handshake (закрытие):

  1. Клиент – FIN – Сервер
  2. Сервер – ACK – Клиент
  3. Сервер – FIN – Клиент
  4. Клиент – ACK – Сервер

Состояния TCP-соединения

Состояния 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=1

Socket 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

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.sock

Shared 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 (изоляция)

Linux 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 myapp

Capabilities (гранулярные права root)

Linux Capabilities

Вместо «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 myapp

seccomp (фильтрация syscall-ов)

# Docker по умолчанию блокирует ~44 syscall-а
# Например: mount, reboot, kexec_load, ...

# Свой профиль
docker run --security-opt seccomp=profile.json myapp

# Отключить (не для prod)
docker run --security-opt seccomp=unconfined myapp

Systemd

# Управление сервисами
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 -20

Port 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 / убить жрущие процессы