Загрузка данных
---
Глава 7. Углублённое рассмотрение системных вызовов для работы с процессами
7.1. Расширенное семейство exec
В системном программировании существует шесть вариантов функций exec, различающихся способом передачи аргументов и переменных окружения:
Функция Формат аргументов Поиск по PATH Передача окружения
execl список (l) нет берёт текущее
execv массив (v) нет берёт текущее
execlp список да (p) берёт текущее
execvp массив да (p) берёт текущее
execle список нет передаётся явно (e)
execve массив нет передаётся явно (e)
Пример с передачей окружения:
```c
#include <unistd.h>
int main() {
char *envp[] = {"HOME=/tmp", "USER=test", NULL};
char *argv[] = {"/bin/bash", "-c", "echo $HOME", NULL};
execve(argv[0], argv, envp);
perror("execve");
return 1;
}
```
Важное замечание: после успешного exec все данные предыдущей программы (включая глобальные переменные, открытые файлы с флагом FD_CLOEXEC) теряются, за исключением открытых файлов без флага close-on-exec.
7.2. Управление файловыми дескрипторами при fork и exec
При вызове fork() файловые дескрипторы родителя копируются в потомка. Оба процесса разделяют одну и ту же файловую таблицу ядра — это означает, что позиция чтения/записи общая. Такое поведение часто нежелательно, особенно в серверах.
Решение: установить флаг FD_CLOEXEC с помощью fcntl:
```c
int fd = open("log.txt", O_WRONLY);
fcntl(fd, F_SETFD, FD_CLOEXEC);
```
Теперь при вызове exec этот дескриптор автоматически закроется в новом образе процесса.
Альтернатива: использовать dup2() для перенаправления стандартных потоков перед exec:
```c
int fd = open("output.txt", O_WRONLY | O_CREAT, 0644);
dup2(fd, STDOUT_FILENO); // перенаправляем stdout в файл
close(fd);
execl("/bin/ls", "ls", NULL);
```
7.3. Системный вызов clone() — fork на стероидах
В Linux fork() реализован через более мощный системный вызов clone(), который позволяет точно контролировать, что разделяется между родителем и потомком: память, таблица файлов, пространство имён, сокеты и т.д. Фактически clone() — это универсальный механизм создания как процессов, так и потоков (POSIX threads используют clone с флагом CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND).
```c
#define _GNU_SOURCE
#include <sched.h>
#include <sys/wait.h>
int child_func(void *arg) {
printf("Child: PID = %d\n", getpid());
return 0;
}
int main() {
char stack[8192];
pid_t pid = clone(child_func, stack + 8192,
CLONE_VM | CLONE_FILES | SIGCHLD, NULL);
waitpid(pid, NULL, 0);
return 0;
}
```
Флаги:
· CLONE_VM — разделять адресное пространство (как в потоках)
· CLONE_FILES — разделять таблицу открытых файлов
· CLONE_NEWNS — создать новое пространство имён монтирования
7.4. Процессы-демоны
Демон — это процесс, работающий в фоне и не привязанный к терминалу. Создание демона — классическая задача системного программирования:
```c
void daemonize() {
pid_t pid = fork();
if (pid > 0) exit(0); // завершаем родителя
setsid(); // создаём новую сессию
pid = fork();
if (pid > 0) exit(0); // гарантируем, что не лидер сессии
chdir("/"); // переходим в корневую директорию
// перенаправляем stdin, stdout, stderr в /dev/null
open("/dev/null", O_RDWR);
dup2(0, STDOUT_FILENO);
dup2(0, STDERR_FILENO);
umask(0); // сбрасываем маску прав
}
```
---
Глава 8. Модели памяти и атомарные операции
8.1. Проблема переупорядочивания операций
Современные компиляторы и процессоры активно переупорядочивают инструкции для оптимизации. В однопоточном коде это незаметно, но в многопоточном приводит к катастрофам:
```c
int flag = 0;
int data = 0;
void* writer(void* arg) {
data = 42; // (1)
flag = 1; // (2) - может выполниться раньше (1)!
}
void* reader(void* arg) {
while (!flag); // (3)
printf("%d", data); // (4) - может увидеть старый data
}
```
8.2. Барьеры памяти (memory barriers)
В системном программировании на C/C++ можно использовать встроенные функции компилятора:
```c
#define atomic_barrier() __sync_synchronize()
```
Или атомарные операции с заданием барьера:
```c
int old = __sync_fetch_and_add(&counter, 1); // атомарный инкремент
__sync_lock_test_and_set(&lock, 1); // атомарный захват
```
8.3. Стандарт C11 и атомики
Начиная с C11, язык предоставляет стандартную модель многопоточности и атомарных операций:
```c
#include <stdatomic.h>
#include <threads.h>
atomic_int counter = ATOMIC_VAR_INIT(0);
int increment(void* arg) {
atomic_fetch_add(&counter, 1); // атомарно
return 0;
}
```
Порядки (memory ordering):
· memory_order_relaxed — никаких гарантий порядка
· memory_order_acquire — последующие чтения/записи не переупорядочиваются раньше этой операции
· memory_order_release — предыдущие операции не переупорядочиваются позже
· memory_order_seq_cst — строгая последовательная согласованность (по умолчанию)
```c
atomic_store_explicit(&flag, 1, memory_order_release);
int val = atomic_load_explicit(&flag, memory_order_acquire);
```
8.4. Lock-free структуры данных
Lock-free алгоритмы позволяют обойтись без мьютексов, используя CAS (Compare-And-Swap). Простейший пример — lock-free стек:
```c
typedef struct node {
int value;
struct node* next;
} node;
_Atomic(node*) head = NULL;
void push(int val) {
node* new_node = malloc(sizeof(node));
new_node->value = val;
do {
new_node->next = atomic_load(&head);
} while (!atomic_compare_exchange_weak(&head, &new_node->next, new_node));
}
```
Преимущества lock-free: отсутствие дедлоков, высокая масштабируемость, гарантия прогресса хотя бы одного потока.
---
Глава 9. Отладка многопоточных и многопроцессных программ
9.1. Анализ гонок данных с ThreadSanitizer
Clang и GCC поддерживают санитайзер потоков:
```bash
gcc -fsanitize=thread -g -O1 program.c -pthread
```
ThreadSanitizer обнаружит гонки данных во время выполнения и выдаст подробный отчёт с указанием строк кода и стеков вызовов.
9.2. Valgrind Helgrind и DRD
helgrind — инструмент Valgrind для детектирования гонок данных:
```bash
valgrind --tool=helgrind ./program
```
drd — альтернативный инструмент, более быстрый, но менее подробный.
9.3. Отладка в GDB
GDB поддерживает отладку многопоточных программ:
```gdb
(gdb) info threads # список потоков
(gdb) thread 2 # переключиться на поток 2
(gdb) bt # backtrace выбранного потока
(gdb) break file.c:42 thread 3 # брейкпойнт только для потока 3
(gdb) set scheduler-locking on # остановить все потоки, кроме текущего
```
Для отладки процессов с fork():
```gdb
(gdb) set follow-fork-mode child # после fork переключиться на потомка
(gdb) set detach-on-fork off # не отсоединять родителя
```
9.4. strace и ltrace для трассировки системных вызовов
strace показывает все системные вызовы процесса в реальном времени:
```bash
strace -f -e trace=clone,fork,execve ./program
```
ltrace трассирует вызовы динамических библиотек (включая pthread).
9.5. Профилирование производительности
perf — мощный инструмент для анализа узких мест:
```bash
perf record -g --call-graph dwarf ./program
perf report
```
Позволяет увидеть, сколько времени процесс проводит в системных вызовах, в синхронизации, в пользовательском коде.
---
Глава 10. Пул потоков (Thread Pool) — реализация с нуля
10.1. Архитектура пула потоков
Пул потоков решает проблему накладных расходов на создание/уничтожение потоков под каждую задачу. Основные компоненты:
· очередь задач (thread-safe)
· массив рабочих потоков
· механизм завершения
10.2. Реализация на C с pthread
```c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
typedef struct task {
void (*function)(void*);
void* argument;
struct task* next;
} task_t;
typedef struct {
task_t* head;
task_t* tail;
pthread_mutex_t mutex;
pthread_cond_t cond;
int shutdown;
int threads_count;
pthread_t* threads;
} threadpool_t;
void* worker(void* arg) {
threadpool_t* pool = (threadpool_t*)arg;
while (1) {
pthread_mutex_lock(&pool->mutex);
while (pool->head == NULL && !pool->shutdown) {
pthread_cond_wait(&pool->cond, &pool->mutex);
}
if (pool->shutdown) {
pthread_mutex_unlock(&pool->mutex);
break;
}
task_t* task = pool->head;
pool->head = task->next;
if (pool->head == NULL) pool->tail = NULL;
pthread_mutex_unlock(&pool->mutex);
// Выполняем задачу
task->function(task->argument);
free(task);
}
return NULL;
}
threadpool_t* threadpool_create(int num_threads) {
threadpool_t* pool = malloc(sizeof(threadpool_t));
pool->head = pool->tail = NULL;
pool->shutdown = 0;
pool->threads_count = num_threads;
pthread_mutex_init(&pool->mutex, NULL);
pthread_cond_init(&pool->cond, NULL);
pool->threads = malloc(num_threads * sizeof(pthread_t));
for (int i = 0; i < num_threads; i++) {
pthread_create(&pool->threads[i], NULL, worker, pool);
}
return pool;
}
void threadpool_add_task(threadpool_t* pool, void (*func)(void*), void* arg) {
task_t* task = malloc(sizeof(task_t));
task->function = func;
task->argument = arg;
task->next = NULL;
pthread_mutex_lock(&pool->mutex);
if (pool->tail) {
pool->tail->next = task;
pool->tail = task;
} else {
pool->head = pool->tail = task;
}
pthread_cond_signal(&pool->cond);
pthread_mutex_unlock(&pool->mutex);
}
void threadpool_destroy(threadpool_t* pool) {
pthread_mutex_lock(&pool->mutex);
pool->shutdown = 1;
pthread_cond_broadcast(&pool->cond);
pthread_mutex_unlock(&pool->mutex);
for (int i = 0; i < pool->threads_count; i++) {
pthread_join(pool->threads[i], NULL);
}
pthread_mutex_destroy(&pool->mutex);
pthread_cond_destroy(&pool->cond);
free(pool->threads);
free(pool);
}
```
10.3. Пример использования
```c
void print_number(void* n) {
int num = *(int*)n;
printf("Task %d executed by thread %lu\n", num, pthread_self());
free(n);
}
int main() {
threadpool_t* pool = threadpool_create(4);
for (int i = 0; i < 20; i++) {
int* n = malloc(sizeof(int));
*n = i;
threadpool_add_task(pool, print_number, n);
}
sleep(1);
threadpool_destroy(pool);
return 0;
}
```
---
Глава 11. Сигналы и их взаимодействие с процессами и потоками
11.1. Основы сигналов
Сигналы — механизм асинхронных уведомлений в UNIX. Процесс может отправить сигнал другому процессу с помощью kill().
Основные сигналы:
· SIGKILL (9) — безусловное завершение (нельзя перехватить)
· SIGTERM (15) — запрос на завершение (можно обработать)
· SIGINT (2) — прерывание с клавиатуры (Ctrl+C)
· SIGSEGV (11) — ошибка сегментации
· SIGCHLD (17) — изменение состояния дочернего процесса
11.2. Установка обработчиков сигналов
```c
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void sigint_handler(int sig) {
printf("Caught SIGINT, but continuing...\n");
}
int main() {
struct sigaction sa = {0};
sa.sa_handler = sigint_handler;
sigaction(SIGINT, &sa, NULL);
while(1) {
printf("Running... PID = %d\n", getpid());
sleep(1);
}
return 0;
}
```
11.3. Асинхронно-безопасные функции
Внутри обработчика сигнала можно вызывать только небольшое подмножество функций (async-signal-safe): write(), _exit(), signal(), kill() и др. Нельзя вызывать printf(), malloc(), pthread_mutex_lock().
Правильный способ — установить глобальный флаг в обработчике:
```c
volatile sig_atomic_t flag = 0;
void handler(int sig) {
flag = 1;
}
int main() {
signal(SIGUSR1, handler);
while (!flag) {
pause();
}
printf("Signal received!\n");
}
```
11.4. Сигналы и потоки
В многопоточной программе сигналы имеют сложное поведение:
· Сигнал, сгенерированный для процесса, может быть доставлен любому потоку, который не блокирует этот сигнал.
· Для надёжной обработки нужно блокировать сигналы во всех потоках, кроме одного выделенного.
```c
sigset_t set;
sigfillset(&set);
pthread_sigmask(SIG_BLOCK, &set, NULL); // блокируем все сигналы в этом потоке
// В выделенном потоке:
sigemptyset(&set);
sigaddset(&set, SIGINT);
pthread_sigmask(SIG_UNBLOCK, &set, NULL);
```
11.5. Сигналы и fork()
При fork() потомок наследует копию обработчиков сигналов родителя, но ожидающие сигналы не наследуются.
---
Глава 12. Реальные кейсы из промышленного системного программирования
12.1. Кейс 1: Web-сервер на epoll + пул потоков
Высокопроизводительные серверы используют асинхронный ввод-вывод (epoll на Linux, kqueue на BSD) в сочетании с пулом потоков. Один поток принимает соединения и распределяет дескрипторы между рабочими потоками через eventfd.
12.2. Кейс 2: Обработчик очереди сообщений с приоритетами
Реализация многопоточной очереди сообщений, где разные приоритеты обрабатываются разными пулами потоков. Используются условные переменные с предикатом.
12.3. Кейс 3: Эмуляция многопроцессности на Windows с CreateProcess
Перенос UNIX-приложения, использующего fork-exec, на Windows через CreateProcess с передачей наследуемых дескрипторов.
12.4. Кейс 4: Отладка сложного deadlock в распределённой системе
Ситуация: взаимная блокировка трёх сервисов, использующих RPC и локальные мьютексы. Решение: внедрение дампа состояния всех мьютексов по сигналу SIGUSR1 с последующим анализом графа ожидания.
12.5. Кейс 5: Профилирование и оптимизация lock contention
Исходная версия использовала один глобальный мьютекс на всех. После замены на шардированные мьютексы (массив из 256 мьютексов, хеш от адреса данных) производительность выросла в 10 раз.
---
Глава 13. Сравнение с современными моделями параллелизма
13.1. Процессы/потоки против корутин (goroutines)
Go использует горутины — легковесные потоки, управляемые рантаймом. Десятки тысяч горутин работают поверх малого числа системных потоков. Планировщик Go переключает горутины на блокирующих операциях.
13.2. async/await в Rust и C++20
Асинхронное программирование с использованием async/await позволяет писать неблокирующий код в синхронном стиле. В отличие от потоков, async-задачи могут выполняться в одном потоке, переключаясь в точках await.
13.3. Акторная модель (Erlang, Akka)
Каждый актор — изолированная единица, общающаяся через асинхронные сообщения. Акторы могут быть реализованы как поверх потоков, так и поверх процессов.
13.4. Когда потоки всё ещё лучше?
· Сильно связанные вычисления с интенсивным обменом данными.
· Реальные системы, где важен приоритет и детерминизм.
· Код на C/C++, где нет встроенной поддержки корутин (до C++20).
---
Глава 14. Дополнительные материалы и справочная информация
14.1. Полезные системные вызовы для мониторинга
· /proc/[pid]/status — состояние процесса
· getrusage() — использованное CPU время
· sched_setaffinity() — привязка потока к ядру процессора
· mlockall() — блокировка страниц памяти в RAM
14.2. Лимиты ОС (ulimit)
```bash
ulimit -u # максимальное число процессов на пользователя
ulimit -n # максимальное число открытых файлов
ulimit -s # размер стека
```
14.3. Рекомендуемая литература
1. W. Richard Stevens — «UNIX: Профессиональное программирование» (Advanced Programming in the UNIX Environment)
2. David R. Butenhof — «Programming with POSIX Threads»
3. Maurice Herlihy, Nir Shavit — «The Art of Multiprocessor Programming» (lock-free структуры)
4. Michael Kerrisk — «The Linux Programming Interface»
5. Ulrich Drepper — «What Every Programmer Should Know About Memory»
---
Заключение (дополненное)
Добавленные разделы углубляют понимание процессов и потоков с точки зрения реального системного программирования. Мы рассмотрели:
· тонкости системных вызовов (clone, управление дескрипторами, демонизация);
· модели памяти и атомарные операции как основу lock-free программирования;
· инструменты отладки (ThreadSanitizer, Helgrind, GDB, strace, perf);
· полную реализацию пула потоков с нуля;
· сигналы и их сложное взаимодействие с потоками;
· реальные промышленные кейсы и сравнение с альтернативными моделями параллелизма.
Освоение этих тем превращает системного программиста из пользователя pthread в архитектора, способного проектировать высоконагруженные, отказоустойчивые и эффективные системы. Процессы и потоки — это не просто абстракции ОС, а фундамент, на котором строится всё современное серверное и системное программное обеспечение.
---