Семафоры, мьютексы, атомарные операции и взаимоблокировки

⏱ 40 мин · 🧩 заданий: 6

Семафор Дейкстры

Семафор — целочисленная переменная, доступная только через две неделимые операции:

Операция Другие имена Действие
P(S) wait, down, sem_wait если S > 0 — уменьшить на 1; иначе заснуть, пока S не станет > 0
V(S) signal, up, sem_post увеличить на 1 и разбудить одного ожидающего

Неделимость обеспечивает ядро (или атомарные инструкции), а ожидание — без траты процессора: процесс спит.

Аналогия

Семафор — корзина с жетонами у входа. P — взять жетон (если пусто — стоять в очереди). V — вернуть жетон. Начальное число жетонов определяет, сколько процессов может быть внутри одновременно.

Три шаблона

1. Взаимное исключение (S = 1)

sem_wait(&mutex);
/* критическая секция */
sem_post(&mutex);

2. Сигнализация (S = 0)

/* процесс A — ждёт события */        /* процесс B — сообщает */
sem_wait(&event);                      do_work();
use_result();                          sem_post(&event);

Неважно, кто придёт первым: если B успел сделать post раньше, A пройдёт wait без ожидания.

3. Производитель — потребитель (буфер на N ячеек)

sem_t empty;   /* = N: свободные ячейки */
sem_t full;    /* = 0: заполненные ячейки */
sem_t mutex;   /* = 1: доступ к буферу */

/* производитель */                   /* потребитель */
sem_wait(&empty);                      sem_wait(&full);
sem_wait(&mutex);                      sem_wait(&mutex);
put(item);                             item = get();
sem_post(&mutex);                      sem_post(&mutex);
sem_post(&full);                       sem_post(&empty);

Случай N = 1 — «почтовый ящик на одно письмо»: писатель кладёт, читатель забирает, писатель кладёт следующее. Именно такой шаблон нужен в задаче 8(б): каждый процесс кладёт пару (pid, ppid) в единственную ячейку Mem, родитель печатает её и освобождает ячейку.

POSIX-семафоры между процессами

Неименованный семафор живёт прямо в разделяемой памяти — достаточно добавить поле в структуру:

#include <semaphore.h>

typedef struct {
    int state;
    unsigned int counter;
    pid_t pid;
    pid_t ppid;
    sem_t full;      /* добавленные поля */
    sem_t empty;
} Mem;

Mem *mem = mmap(NULL, sizeof(Mem), PROT_READ | PROT_WRITE,
                MAP_SHARED | MAP_ANONYMOUS, -1, 0);
sem_init(&mem->empty, 1, 1);   /* pshared = 1: для процессов; значение 1 */
sem_init(&mem->full, 1, 0);
/* ... fork ... */
sem_destroy(&mem->full);

Второй аргумент sem_init — pshared: 0 — между потоками процесса, не 0 — между процессами (тогда семафор обязан лежать в разделяемой памяти).

Именованный семафор — для неродственных процессов:

sem_t *s = sem_open("/mysem", O_CREAT, 0600, 1);
sem_wait(s); /* ... */ sem_post(s);
sem_close(s);
sem_unlink("/mysem");

Компиляция: gcc prog.c -pthread.

Мьютексы

Мьютекс — замок с владельцем: открыть может только тот, кто закрыл. Для потоков:

pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&m);
/* ... */
pthread_mutex_unlock(&m);

Для процессов — мьютекс в разделяемой памяти с атрибутом:

pthread_mutexattr_t a;
pthread_mutexattr_init(&a);
pthread_mutexattr_setpshared(&a, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(&mem->mtx, &a);

К мьютексу добавляют условные переменные (pthread_cond_wait / pthread_cond_signal) — ждать, пока выполнится условие («ячейка свободна»).

Как это устроено: futex

В Linux семафоры и мьютексы построены на futex (fast userspace mutex): захват свободного замка — одна атомарная инструкция без входа в ядро; только если замок занят, процесс делает системный вызов futex(FUTEX_WAIT) и засыпает в ядре, а освобождающий вызывает futex(FUTEX_WAKE). Быстро в частом случае, без траты процессора в редком.

System V семафоры

Старый интерфейс: наборы семафоров, операции над несколькими сразу.

int id = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
semctl(id, 0, SETVAL, 1);
struct sembuf p = {0, -1, 0}, v = {0, +1, 0};
semop(id, &p, 1);   /* P */
semop(id, &v, 1);   /* V */
semctl(id, 0, IPC_RMID);

Взаимоблокировки

Взаимоблокировка (deadlock) — группа процессов, каждый ждёт ресурс, удерживаемый другим.

/* процесс 1 */              /* процесс 2 */
sem_wait(&S1);                sem_wait(&S2);
sem_wait(&S2);  /* ждёт */    sem_wait(&S1);  /* ждёт */

Необходимые условия (Коффман): взаимное исключение, удержание и ожидание, отсутствие вытеснения, круговое ожидание. Нарушьте любое — взаимоблокировки не будет.

Подход Как
Предотвращение единый порядок захвата ресурсов (нет цикла); захват всех ресурсов сразу
Обход алгоритм банкира: выдавать ресурс, только если состояние останется безопасным
Обнаружение и восстановление искать циклы в графе ожиданий, прерывать процесс
«Страус» игнорировать (так поступает большинство ОС для пользовательских процессов)

Взаимоблокировка с wait

wait — тоже ожидание ресурса. Если родитель ждёт в wait(NULL) своего потомка, а потомок ждёт на семафоре, который освободит только родитель после wait, — классическая взаимоблокировка. Эту ловушку важно обойти в задаче 8(б).

Итог

  • Семафор: P уменьшает или усыпляет, V увеличивает и будит; начальное значение задаёт смысл (1 — мьютекс, 0 — сигнализация, N — пул).
  • Между процессами: sem_init(&mem->s, 1, value) в разделяемой памяти, или sem_open; мьютекс с PTHREAD_PROCESS_SHARED.
  • Производитель-потребитель на семафорах empty/full — шаблон для «почтового ящика» в задаче 8.
  • Взаимоблокировка требует четырёх условий; проще всего нарушить круговое ожидание единым порядком захвата.

Практика решено 0 из 6

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Написать код

В структуру разделяемой памяти Mem добавлено поле sem_t lock;. Инициализируйте семафор как разделяемый между процессами со значением 1, а затем защитите им увеличение счётчика (sem_wait до, sem_post после mem->counter++).

Задание 2 Ввод ответа

Семафор инициализирован значением 0. Процесс A вызывает sem_wait, затем процесс B вызывает sem_post. Сколько процессов в итоге заблокировано? (число)

Задание 3 Заполнить таблицу

Каким начальным значением инициализировать семафор для каждой задачи?

01N
Взаимное исключение (мьютекс)
Процесс ждёт события от другого процесса
Не больше N процессов одновременно используют пул ресурсов
«Пустые места» в буфере из N ячеек (производитель-потребитель)
«Заполненные ячейки» в изначально пустом буфере
Отметьте ячейку — «+», пустая — «−».
Задание 4 Несколько вариантов

Какие условия необходимы для взаимоблокировки (условия Коффмана)?

Задание 5 Один вариант ответа

Два процесса: первый захватывает семафор S1, потом S2; второй — S2, потом S1. Как проще всего исключить взаимоблокировку?

Задание 6 Написать код

Инициализируйте мьютекс mem->mtx в разделяемой памяти так, чтобы его могли использовать разные процессы: атрибуты pthread_mutexattr_t a, pthread_mutexattr_init, pthread_mutexattr_setpshared(&a, PTHREAD_PROCESS_SHARED), затем pthread_mutex_init(&mem->mtx, &a).

Войдите, чтобы отмечать пройденные уроки.