Для совместной работы массив семафоров могут использовать. Реализация семафоров в Linux: Нейл Мэтью

Реализация семафоров в Linux

Теперь, когда вы увидели, что такое семафоры и как они действуют в теории, можно рассмотреть, как их свойства реализованы в ОС Linux. Интерфейс тщательно проработан и предлагает гораздо больше возможностей, чем обычно требуется. Все функции семафоров в Linux оперируют массивами семафоров общего вида, а не одним двоичным семафором. На первый взгляд кажется, что такой подход все усложняет, но если процесс нуждается в блокировке нескольких ресурсов, способность оперировать массивом семафоров - большое подспорье. В этой главе мы сосредоточимся на применении одиночных семафоров, поскольку в большинстве случаев это все, что вам нужно.

#include
int semctl(int sem_id, int sem_num, int command, ...);

Примечание

Обычно заголовочный файл sys/sem.h опирается на два других заголовочных файла: sys/types.h и sys/ipc.h. Как правило, они автоматически включаются в программу файлом sys/sem.h и вам не нужно задавать их явно в директивах # include .

Прорабатывая каждую функцию отдельно, помните о том, что все они спроектированы для использования массивов значений семафоров, что делает их работу существенно более сложной, чем та, что необходима для обработки одного семафора.

Обратите внимание на то, что параметр key действует во многом как имя файла, т.к. он тоже представляет ресурс, который программы могут использовать и кооперироваться при этом, если соблюдают соглашение об общем имени для него. Аналогичным образом идентификатор, возвращаемый функцией semget и применяемый другими функциями, совместно использующими память, очень похож на файловый поток FILE* , возвращаемый функцией fopen и представляющий собой значение, применяемое процессом для доступа к совместно используемому файлу. Как и в случае файлов, у разных процессов будут разные идентификаторы семафоров, несмотря на то, что они ссылаются на один и тот же семафор. Такое применение ключа и идентификаторов - общее для всех средств IPC, обсуждаемых здесь, несмотря на то, что каждое средство применяет независимые ключи и идентификаторы.

semget

Функция semget создает новый семафор или получает ключ существующего семафора.

int semget(key_t key, int num_sems, int sem_flags);

Первый параметр key - целочисленное значение, позволяющее несвязанным процессам обращаться к одному и тому же семафору. Ко всем семафорам осуществляется непрямой доступ с помощью программы, предоставляющей ключ, для которого система затем генерирует идентификатор семафора. Ключ семафора применяется только в функции semget . Все остальные функции семафора используют идентификатор семафора, возвращаемый функцией semget .

Существует особое значение ключа семафора IPC_PRIVATE , которое предназначено для создания семафора, доступ к которому получает только процесс-создатель, но такой семафор редко бывает полезен. Для создания нового семафора следует задавать уникальное ненулевое целое число.

Параметр num_sems определяет количество требуемых семафоров. Почти всегда он равен 1.

Параметр sem_flags - набор флагов, очень похожих на флаги функции open. Младшие девять байтов - права доступа к семафору, ведущие себя, как права доступа к файлу. Кроме того, для создания нового семафора с помощью поразрядной операции OR их можно объединить со значением IPC_CREAT . Не считается ошибкой наличие флага IPC_CREAT и задание ключа существующего семафора. Флаг IPC_CREAT безмолвно игнорируется, если в нем нет нужды. Можно применять флаги IPC_CREAT и IPC_EXCL для гарантированного получения нового уникального семафора. Если семафор уже существует, функция вернет ошибку.

Функция semget вернет в случае успеха положительное (ненулевое) значение, представляющее собой идентификатор, применяемый остальными функциями семафора. В случае ошибки возвращается -1.

semop

Функция semop применяется для изменения значения семафора.

int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);

Первый параметр sem_id - идентификатор семафора, возвращенный функцией semget . Второй параметр sem_ops - указатель на массив структур, у каждой из которых есть, по крайней мере, следующие элементы:

struct sembuf {
short sem_num;
short sem_op;
short sem_flg;
}

Первый параметр sem_num - номер семафора, обычно 0, если вы не работаете с массивом семафоров. Элемент sem_op - значение, на которое должен изменяться семафор. (Вы можете увеличивать и уменьшать семафор на значения, не равные 1.) Как правило, применяются только два значения: -1 для операции P, заставляющей ждать, пока семафор не станет доступен, и +1 для операции V , оповещающей о том, что в данный момент семафор доступен.

Последний элемент sem_flg обычно задается равным SEM_UNDO . Это значение заставляет операционную систему отслеживать изменения значения семафора, сделанные текущим процессом, и, если процесс завершается, не освободив семафор, позволяет операционной системе автоматически освободить семафор, если он удерживался этим процессом. Хорошо взять за правило установку sem_flg , равным SEM_UNDO , если вам не требуется иного поведения. Если же вы все-таки решили, что вам нужно значение, отличное от SEM_UNDO , очень важно быть последовательным, иначе вы можете оказаться в замешательстве относительно попыток ядра системы "убрать" ваши семафоры, когда ваш процесс завершается.

Все действия, предусмотренные semop , собраны вместе, чтобы избежать состояния гонок, вызванного использованием множественных семафоров. Все подробности функционирования semop можно найти на страницах интерактивного справочного руководства.

semctl

Функция semctl позволяет напрямую управлять данными семафора.

int semctl (int sem_id, int sem_num, int command, ...);

Первый параметр sem_id - идентификатор семафора, полученный от функции semget . Параметр sem_num - номер семафора. Он применяется при работе с массивом семафоров. Обычно этот параметр равен 0, первый и единственный семафор. Параметр command - предпринимаемое действие, и четвертый параметр, если присутствует, - union (объединение) типа semun , которое в соответствии со стандартом X/Open должно содержать как минимум следующие элементы:

union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
}

В большинстве версий ОС Linux определение объединения semun включено в заголовочный файл (обычно sem.h), несмотря на то, что стандарт X/Open настаивает на том, что вы должны привести собственное объявление. Если вы поймете, что должны объявить его самостоятельно, проверьте, нет ли объявления этого объединения на страницах интерактивного справочного руководства, относящихся к функции semctl . Если вы найдете его, мы полагаем, что вы примените определение из вашего справочного руководства, даже если оно отличается от приведенного на страницах этой книги.

Различные типы блокировок используются в программировании, чтобы оградить критический участок кода от одновременного исполнения. Поэтому блокировки чаще всего используются для защиты фрагментов кода, а не областей данных, хотя, например, семафоры, (не бинарные) используются, главным образом, для ограничения доступа к данным.

В этой статье мы начнём обсуждение различных типов блокировок, доступных при программировании модулей ядра.

Типы блокировок

До появления и широкого распространения SMP, когда физической реализации параллелизма ещё не существовало, блокировки использовались классическим способом (как описывал Э. Дейкстра), защищая критические области программы от одновременного исполнения несколькими процессами. Такие механизмы работают за счёт вытеснения запрашивающих процессов в блокированное состояние до времени освобождения запрошенных ресурсов. Подобные блокировки мы будем называть пассивными блокировками, и в этом случае процессор прекращает выполнение текущего процесса в точке блокирования и переключается на выполнение другого процесса (возможно idle).

Принципиально другой вид блокировок —активные блокировки — появился вместе с SMP системами, когда процессор, ожидая освобождения недоступного ресурса, не переводится в блокированное состояние, а выполняет пустые циклы. В этом случае, процессор не освобождается для выполнения другого ожидающего процесса в системе, а продолжает активное выполнение ("пустых" циклов) в контексте текущей ветви исполнения.

Эти два типа блокировок (каждый из которых включает несколько подвидов) принципиально отличаются по ключевым параметрам:

  • возможностью использования: пассивно заблокировать (переключить контекст) можно только фрагмент кода, имеющий свой собственный контекст (запись задачи), куда позже можно вернуться (активировать процесс), а в обработчиках прерываний или тасклетах это условие не соблюдается;
  • эффективностью: активные блокировки не всегда проигрывают пассивным в производительности, так как переключение контекста в системе это трудоёмкий процесс, поэтому для ожидания короткого интервала времени активные блокировки могут оказаться даже эффективнее, чем пассивные.

Семафоры (мьютексы)

Семафоры ядра определены в файле . Так как задачи, конфликтующие при захвате блокировки, переводятся в состояние ожидания и в этом состоянии ожидают освобождения блокировки, то семафоры хорошо подходят для блокировок, которые могут удерживаться в течение длительного времени. С другой стороны, семафоры не очень пригодны для блокировок, которые удерживаются в течение короткого интервала, так как накладные затраты на перевод процессов в состояние ожидания могут превысить время, в течение которого удерживается блокировка. Кроме того, существует и явное ограничение на использование семафоров в ядре, так как их невозможно использовать в коде, который не должен переходить в блокированное состояние, например, при обработке верхней половины прерываний.

Спин-блокировки позволяют удерживать блокировку только одной задачи в любой момент времени, но для семафора количество задач (count), которые разрешено одновременно удерживать с его помощью (владеть семафором), может быть задано при декларации переменной семафора в соответствующем поле структуры:

struct semaphore { spinlock_t lock; unsigned int count; struct list_head wait_list; };

Если значение count больше 1, то семафор называется счетным семафором и допускает количество потоков, которые одновременно удерживают блокировку, не больше, чем значение счетчика использования (count). Встречается ситуация, когда разрешенное количество потоков, которые одновременно могут удерживать семафор, равно 1 (как и для спин-блокировок), и такие семафоры называются бинарными или взаимоисключающими блокировками (mutex, мютекс, потому что он гарантирует взаимоисключающий доступ — mutual exclusion). Бинарные семафоры (мьютексы) чаще всего используются для обеспечения взаимоисключающего доступа к фрагментам кода, называемым критической секцией.

Независимо от того, определено ли поле владельца, захватившего мютекс (так как это делается по разному в различных POSIX-совместимых ОС), принципиальными особенностями мютекса, в отличии от счётного семафора будет то, что:

  1. у захваченного мютекса всегда будет единственный владелец, захвативший его;
  2. освободить блокированные на мютексе потоки (освободить мютекс) может только один владеющий мютексом поток.

В случае счётного семафора освободить потоки, блокированные на семафоре, может любой из потоков, владеющий семафором.

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

static DECLARE_SEMAPHORE_GENERIC(name, count);

Для создания взаимоисключающей блокировки (mutex) есть более короткий синтаксис:

static DECLARE_MUTEX(name);

— где в обоих случаях name — это имя переменной типа семафор.

Но чаще всего семафоры создаются динамически, как составная часть более крупных структур данных. В таком случае для инициализации счётного семафора используется функция:

void sema_init(struct semaphore *sem, int val);

А для инициализации бинарных семафоров (мютексов) используются макросы:

init_MUTEX(struct semaphore *sem); init_MUTEX_LOCKED(struct semaphore *sem);

В ОС Linux для захвата семафора (мютекса) используется операция down() , уменьшающая его счетчик на единицу. Если значение счетчика больше или равно нулю, то блокировка захвачена успешно и задача может входить в критический участок. Если значение счетчика (после декремента) меньше нуля, то задание помещается в очередь ожидания и процессор переходит к выполнению других задач. Метод up() используется для того, чтобы освободить семафор (после завершения выполнения критического участка), его выполнение увеличивает счётчик семафора на единицу, при этом один из имеющихся заблокированных потоков может захватить блокировку (принципиальным является то, что невозможно повлиять на то, какой конкретно поток из числа заблокированных будет выбран). Ниже перечислены другие операции над семафорами.

  • void down(struct semaphore *sem) — переводит задачу в блокированное состояние ожидания с флагом TASK_UNINTERRUPTIBLE . В большинстве случаев это нежелательно, так как процесс, который ожидает освобождения семафора, не будет отвечать на сигналы.
  • int down_interruptible(struct semaphore *sem) — выполняет попытку захватить семафор. Если эта попытка неудачна, то задача переводится в блокированное состояние с флагом TASK_INTERRUPTIBLE (в структуре задачи). Такое состояние процесса означает, что задание может быть возвращено к выполнению с помощью сигнала, а такая возможность обычно очень ценна. Если сигнал приходит в то время, когда задача блокирована на семафоре, то задача возвращается к выполнению, а функция down_interruptible() возвращает значение — EINTR .
  • int down_trylock(struct semaphore *sem) — используется для неблокирующего захвата семафора. Если семафор уже захвачен, то функция немедленно возвращает ненулевое значение. В случае успешного захвата семафора возвращается нулевое значение и захватывается блокировка.
  • int down_timeout(struct semaphore *sem, long jiffies) — используется для попытки захвата семафора на протяжении интервала времени jiffies системных тиков.

Спин-блокировки

Блокирующая попытка входа в критическую секцию при использовании семафоров означает потенциальный перевод задачи в блокированное состояние и переключение контекста, что является дорогостоящей операцией. Спин-блокировки (spinlock_t) используются для синхронизации в случаях, когда:

  • контекст выполнения не позволяет переходить в блокированное состояние (контекст прерывания);
  • или требуется кратковременная блокировка без переключение контекста.

Они представляют собой активное ожидание освобождения в пустом цикле. Если необходимость синхронизации связана только с наличием в системе нескольких процессоров, то для небольших критических секций следует использовать спин-блокировку, основанную на простом ожидании в цикле. Спин-блокировка может быть только бинарной. Определения, относящиеся к spinlock_t , распределены по нескольким заголовочным файлам:

$ ls spinlock* spinlock_api_smp.h spinlock_api_up.h spinlock.h spinlock_types.h spinlock_types_up.h spinlock_up.h typedef struct { raw_spinlock_t raw_lock; ... } spinlock_t;

Для инициализации spinlock_t и родственного типа rwlock_t , о котором будет подробно рассказано ниже, раньше (и в литературе) использовались макросы:

spinlock_t lock = SPIN_LOCK_UNLOCKED; rwlock_t lock = RW_LOCK_UNLOCKED; // SPIN_LOCK_UNLOCKED and RW_LOCK_UNLOCKED defeat lockdep state tracking and // are hence deprecated.

То есть, эти макроопределения объявлены не поддерживаемыми и могут быть исключены в любой последующей версии. Поэтому для определения и инициализации используются новые макросы (эквивалентные по смыслу записанным выше) вида:

DEFINE_SPINLOCK(lock); DEFINE_RWLOCK(lock);

Эти макросы представляют собой статические определения отдельных (автономных) переменных типа spinlock_t . И так же, как и для других примитивов, возможна динамическая инициализация ранее объявленной переменной (чаще всего эта переменная является полем в составе более сложной структуры):

void spin_lock_init(spinlock_t *sl);

Основной интерфейс spinlock_t содержит пару вызовов для захвата и освобождения блокировки:

spin_lock (spinlock_t *sl); spin_unlock(spinlock_t *sl);

Если при компиляции ядра не было активировано SMP (использование многопроцессорности) и не сконфигурировано вытеснение кода в ядре (обязательно выполнение обоих условий), то spinlock_t вообще не компилируются (и на их месте останутся пустые места) за счёт препроцессорных директив условной трансляции.

Примечание : В отличие от реализаций в некоторых других операционных системах, спин-блокировки в операционной системе Linux не рекурсивны. Это означает, что показанный ниже код автоматически приведёт к ситуации deadlock (процессор будет бесконечно выполнять этот фрагмент и произойдёт деградация системы, так как число процессоров, доступных в системе, уменьшится):

DEFINE_SPINLOCK(lock); spin_lock(&lock); spin_lock(&lock);

Рекурсивный захват спин-блокировки может неявно произойти в обработчике прерываний, поэтому перед захватом такой блокировки нужно запретить прерывания на локальном процессоре. Это общий случай, поэтому для него предоставляется специальный интерфейс:

DEFINE_SPINLOCK(lock); unsigned long flags; spin_lock_irqsave(&lock, flags); /* критический участок... */ spin_unlock_irqre_store(&lock, flags);

Для спин-блокировки определены ещё такие вызовы, как:

  • int spin_try_lock(spinlock_t *sl) — попытка захвата без блокирования, если блокировка уже захвачена, функция возвратит ненулевое значение;
  • int spin_is_locked(spinlock_t *sl) — возвращает ненулевое значение, если блокировка в данный момент захвачена.

Блокировки чтения-записи

Особым, но часто встречающимся, случаем синхронизации являются сценарий "чтения-записи". "Читатели" только считывают состояние некоторого ресурса, и поэтому могу иметь к нему совместный параллельный доступ. "Писатели" изменяют состояние ресурса, и поэтому писатель должен иметь к ресурсу монопольный доступ, причем чтение ресурса для всех читателей в этот момент времени так же должно быть заблокировано. Для реализации блокировок чтения-записи в ядре Linux существуют отдельные версии семафоров и спин-блокировок. Мьютексы реального времени не имеют реализации, подходящей для использования в данном сценарии.

Примечание : Обратим внимание на то, что в точности той же функциональности можно достичь, используя классические примитивы синхронизации (мьютекс или спин-блокировку), просто захватывая критический участок независимо от типа предстоящей операции. Блокировки чтения-записи введены из соображений эффективности реализации для массового сценария такого использования.

Для семафоров вместо структуры struct semaphore вводится структура struct rw_semaphore , а набор интерфейсных функций для захвата/освобождения (простые down() / up()) расширяется до:

  • down_read(&rwsem) — попытка захватить семафор для чтения;
  • up_read(&rwsem — освобождение семафора для чтения;
  • down_write(&rwsem) — попытка захватить семафор для записи;
  • up_write(&rwsem) — освобождение семафора для записи;

Семантика этих операций следующая:

  • если семафор ещё не захвачен, то любой захват (down_read() или down_write()) будет успешным (без блокирования);
  • чтения , то последующие попытки захвата семафора для чтения (down_read()) будут завершаться успешно (без блокирования), но запрос на захват такого семафора для записи (down_write()) закончится блокированием;
  • если семафор захвачен уже для записи , то любая последующая попытка захвата семафора (down_read() или down_write()) закончится блокированием;

Статически определенный семафор чтения-записи создаётся макросом:

static DECLARE_RWSEM(name);

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

void init_rwsem(struct rw_semaphore *sem);

Примечание : Из описания инициализации видно, что семафоры чтения-записи являются исключительно бинарными (не счётными), то есть (в терминологии Linux) фактически не семафорами, а мютексами.

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

struct data { int value; struct list_head list; }; static struct list_head list; static struct rw_semaphore rw_sem; int add_value(int value) { struct data *item; item = kmalloc(sizeof(*item), GFP_ATOMIC); if (!item) goto out; item->value = value; down_write(&rw_sem); /* захватить для записи */ list_add(&(item->list), &list); up_write(&rw_sem); /* освободить по записи */ return 0; out: return -ENOMEM; } int is_value(int value) { int result = 0; struct data *item; struct list_head *iter; down_read(&rw_sem); /* захватить для чтения */ list_for_each(iter, &list) { item = list_entry(iter, struct data, list); if(item->value == value) { result = 1; goto out; } } out: up_read(&rw_sem); /* освободить по чтению */ return result; } void init_list(void) { init_rwsem(&rw_sem); INIT_LIST_HEAD(&list); }

Точно так же, как это сделано для семафоров, вводится и блокировка чтения-записи для спин-блокировки:

typedef struct { raw_rwlock_t raw_lock; ... } rwlock_t;

С набором операций:

read_lock(rwlock_t *rwlock); read_unlock(rwlock_t *rwlock); write_lock(rwlock_t *rwlock); write_unlock (rwlock_t *rwlock);

Примечание : Если при компиляции ядра не было установлено SMP и не сконфигурировано вытеснение кода в ядре, то spinlock_t вообще не скомпилируются (на их месте останутся пустые места), а, значит, и соответствующие им rwlock_t .

Также, блокировку, захваченную для чтения, уже нельзя далее повысить до блокировки, захваченной для записи.:

read_lock(&rwlock); write_lock(&rwlock);

Такая последовательностью операторов гарантирует возникновение deadlock, так как при захвате блокировки на запись будет выполняться периодическая проверка, пока все потоки, которые захватили блокировку для чтения, ее не освободят, это касается и текущего потока, который не сможет сделать этого никогда...

Но несколько потоков чтения могут безопасно захватывать одну и ту же блокировку чтения-записи, поэтому один поток также может безопасно рекурсивно захватывать одну и ту же блокировку для чтения несколько раз, например в обработчике прерываний без запрета прерываний.

Заключение

На момент создания механизма блокировок чтения-записи с их внедрением связывали значительное повышение производительности, и они вызывали заметный энтузиазм разработчиков. Но последующая практика показала, что этому механизму присуща скрытая опасность того, что при высокой и равномерной плотности запросов чтения, запрос на модификацию (запись) структуры записи может отсрочиваться на неограниченно большие интервалы времени. Об этой особенности нужно помнить, оценивая возможность применения этого механизма в своём коде. Частично смягчить это ограничение пытается следующий подвид блокировок - сериальные блокировки, которые мы рассмотрим в следующей статье.

Linux: Полное руководство Колисниченко Денис Николаевич

26.6. Семафоры

26.6. Семафоры

Семафор - это объект IPC, управляющий доступом к общим ресурсам (устройствам). Семафоры не позволяют одному процессу захватить устройство до тех пор, пока с этим устройством работает другой процесс. Семафор может находиться в двух положениях: 0 (устройство занято) и 1 (устройство свободно).

Одиночный семафор используется редко, практически никогда. Для контроля доступа к ресурсам обычно используются множества семафоров, даже если это множество состоит всего из одного семафора. Например, пусть у нас есть три принтера. Когда вы посылаете задание на печать, диспетчер печати просматривает множество семафоров принтеров и выясняет, есть ли свободный принтер. Если да, то он начинает печатать ваше задание, если же нет, диспетчер ставит ваше задание в очередь печати.

Еще один пример использования семафоров - это счетчики ресурсов. Представим, что вместо принтера есть некий контроллер, позволяющий выполнять 100 заданий одновременно. Когда он свободен, значение семафора равно 100. По мере поступления заданий диспетчер контроллера уменьшает значение семафора на 1, а по мере их выполнения увеличивает на 1. Когда значение достигает 0, новое задание ставится в очередь до освобождения контроллера.

Как и в случае с очередями сообщений, для семафоров в ядре Linux есть своя структура - semid_ds, которая описана в файле /usr/src/linux/include/linux/sem.h:

struct semid_ds {

struct ipc_perm sem_perm; /* права доступа */

time_t sem_otime; /* время последней операции */

time_t sem_ctime; /* время последнего изменения */

struct sem *sem_base; /* указатель на первый семафор */

struct wait_queue *eventn; /* очереди ожидания */

struct wait_queue *eventz;

struct sem_undo *undo; /* запросы undo в этом массиве */

ushort sem_nsems; /* номера семафоров в массиве */

Обратите внимание: в структуре есть указатель на первый семафор. Тип указателя - sem. Данный тип описывает семафор:

short sempid; /* pid последней операции */

ushort semval; /* текущее значение семафора */

ushort semncnt; /* число процессов, ожидающих

освобожд. рес. */

ushort semzcnt; /* число процессов, ожидающих

освоб. всех рес. */

PID процесса, который произвел последнюю операцию над семафором.

Текущее значение семафора.

Число процессов, ожидающих увеличения значения семафора, то есть освобождения ресурсов.

Число процессов, ожидающих освобождения всех ресурсов.

Из книги Архитектура операционной системы UNIX автора Бах Морис Дж

Из книги UNIX: взаимодействие процессов автора Стивенс Уильям Ричард

12.3 СЕМАФОРЫ Поддержка системы UNIX в многопроцессорной конфигурации может включать в себя разбиение ядра системы на критические участки, параллельное выполнение которых на нескольких процессорах не допускается. Такие системы предназначались для работы на машинах AT amp;T

Из книги Linux: Полное руководство автора Колисниченко Денис Николаевич

ГЛАВА 10 Семафоры Posix

Из книги Введение в QNX/Neutrino 2. Руководство по программированию приложений реального времени в QNX Realtime Platform автора Кёртен Роб

10.13. Ограничения на семафоры Стандартом Posix определены два ограничения на семафоры:? SEM_NSEMS_MAX - максимальное количество одновременно открытых семафоров для одного процесса (Posix требует, чтобы это значение было не менее 256);? SEM_VALUE_MAX - максимальное значение семафора (Posix

Из книги Программирование для Linux. Профессиональный подход автора Митчелл Марк

ГЛАВА 11 Семафоры System V 11.1.Введение В главе 10 мы описывали различные виды семафоров, начав с:? бинарного семафора, который может принимать только два значения: 0 и 1. По своим свойствам такой семафор аналогичен взаимному исключению (глава 7), причем значение 0 для семафора

Из книги Операционная система UNIX автора Робачевский Андрей М.

Семафоры Posix, размещаемые в памяти Мы измеряем скорость работы семафоров Posix (именованных и размещаемых в памяти). В листинге А.24 приведен текст функции main, а в листинге А.23 - текст функции incr.Листинг А.23. Увеличение счетчика с использованием семафоров Posix в

Из книги Разработка ядра Linux автора Лав Роберт

Именованные семафоры Posix В листинге А.26 приведен текст функции main, измеряющей быстродействие именованных семафоров Posix, а в листинге А.25 - соответствующая функция incr.Листинг А.25. Увеличение общего счетчика с использованием именованного семафора Posix//bench/incr_pxsem2.c40 void

Из книги автора

Семафоры System V Функция main программы, измеряющей быстродействие семафоров System V, приведена в листинге А.27, а функция incr показана в листинге А.28.Листинг А.27. Функция main для измерения быстродействия семафоров System V//bench/incr_svsem1.c1 #include "unpipc.h"2 #define MAXNTHREADS 1003 int nloop;4 struct {5 int

Из книги автора

26.6. Семафоры Семафор - это объект IPC, управляющий доступом к общим ресурсам (устройствам). Семафоры не позволяют одному процессу захватить устройство до тех пор, пока с этим устройством работает другой процесс. Семафор может находиться в двух положениях: 0 (устройство

Из книги автора

Семафоры Давайте переместимся из ванной комнаты на кухню, так как это социально адаптированное помещение для одновременного обитания более чем одного человека. На кухне вы можете не пожелать, чтобы все и каждый находились бы там одновременно. В действительности вы бы,

Из книги автора

4.4.2. Исключающие семафоры Решение проблемы гонки заключается в том, чтобы позволить только одному потоку обращаться к очереди в конкретный момент времени. Когда поток начинает просматривать очередь, все остальные потоки вынуждены дожидаться, пока он удалит очередное

Из книги автора

4.4.5. Обычные потоковые семафоры В предыдущем примере, в котором группа потоков обрабатывает задания из очереди, потоковая функция запрашивает задания до тех пор, пока очередь не опустеет, после чего поток завершается. Эта схема работает в том случае, когда все задания

Из книги автора

5.2. Семафоры для процессов Как говорилось в предыдущем разделе, процессы должны координировать свои усилия при совместном доступе к памяти. Вспомните: в разделе 4.4.5, "Обычные потоковые семафоры", рассказывалось о семафорах, которые являются счетчиками, позволяющими

Из книги автора

Семафоры Для синхронизации процессов, а точнее, для синхронизации доступа нескольких процессов к разделяемым ресурсам, используются семафоры. Являясь одной из форм IPC, семафоры не предназначены для обмена большими объемами данных, как в случае FIFO или очередей сообщений.

Из книги автора

Семафоры В операционной системе Linux семафоры (semaphore) - это блокировки, которые переводят процессы в состояние ожидания. Когда задание пытается захватить семафор, который уже удерживается, семафор помещает это задание в очередь ожидания (wait queue) и переводит это задание в

Из книги автора

Семафоры чтения-записи Семафоры, так же как и спин-блокировки, могут быть типа чтения-записи. Ситуации, в которых предпочтительнее использовать семафоры чтения-записи такие же как и в случае использования спин-блокировок чтения-записи.Семафоры чтения-записи

Здравствуйте!

Решаю задачу синхронизации процессов в Linux.

Задача первая и главная. Есть два/три процесса, все они получают одни и те же сообщения, примерно одновременно. Один из этих процессов, давайте называть ведущим, остальные - ведомыми. Необходимо при получении одного, из всего изобилия сообщений, ведущим процессом устанавливать некоторые значения для ведомых, ведомые же приостанавливать до окончания записи этих значений. А после, на ведомых, делать выбор между запуском функции обработки сообщения или продолжением работы без обработки.

Т.е. это похоже на очередь с "буфером" равным 1 или 2 (в зависимости от количества ведомых процессов). Причем ведущий процесс ничего не обрабатывает, а лишь, выдает команды, основываясь на знании того, сколько ведомых процессов в данный момент заняты обработкой: отдать 1му, отдать 2му, сбросить. Прелесть такого способа хороша, когда необходимо знать, сколько сообщений было отброшено (ведь процесс не имеет возможности слушать сообщения во время обработки).

В данном случае, семафоры хотелось бы использовать для организации блокировки (т.е. ожидания готовности данных, но не более ), а так же для передачи этих самых данных. Под данными я подразумеваю счетчик, определяющий сколько процессов свободны, и какие из них свободны (например двухбитный флаг).

Вторая задача - это упразднение ведущего процесса. Теперь все процессы равнозначны по задачам. Это дает выигрыш в обработке, так как третий процесс может быть задействован. Но здесь возникает вопрос, как синхронизировать процессы, чтобы не дать двум и более процессам обрабатывать одно и то же сообщение.

Для решения этой задачи я бы использовал все те же семафоры, позволяющие "следить" за счетчиком процессов, и, теперь уже, трехбитный флаг, который решает теперь две задачи: задачу выбора обрабатывающего процесса, по старшинству нулевого бита, и задачу идентификации свободных процессов.

Но, столкнулся с трудностями, не могу до конца осознать необходимые действия после получения идентификатора семафора в Linux. Пользуясь возможностями ftok , получаю уникальный ключ к своему семафору. Далее через sem_get получаю сам семафор или его создаю.

А дальше куда глядеть? Есть адекватное описание функций для реализации того, что мне нужно (развернутое описание того, что функция делает, какие параметры и каких типов принимает, что возвращает)? Не откажусь и от направления мыслей в области решения второй задачи. Все еще усугубляется тем, что блокировка процессов должна быть минимальной, любые задержки чреваты. Код должен быть легким, оптимальным и быстрым.

Межпроцессное взаимодействие (Inter-process communication (IPC) ) - это набор методов для обмена данными между потоками процессов. Процессы могут быть запущены как на одном и том же компьютере, так и на разных, соединенных сетью. IPC бывают нескольких типов: «сигнал», «сокет», «семафор», «файл», «сообщение»…

В данной статье я хочу рассмотреть всего 3 типа IPC:

Отступление: данная статья является учебной и расчитана на людей, только еще вступающих на путь системного программирования. Ее главный замысел - познакомиться с различными способами взаимодействия между процессами на POSIX-совместимой ОС.

Именованный канал

Для передачи сообщений можно использовать механизмы сокетов, каналов, D-bus и другие технологии. Про сокеты на каждом углу можно почитать, а про D-bus отдельную статью написать. Поэтому я решил остановиться на малоозвученных технологиях отвечающих стандартам POSIX и привести рабочие примеры.

Рассмотрим передачу сообщений по именованным каналам. Схематично передача выглядит так:

Для создания именованных каналов будем использовать функцию, mkfifo() :
#include int mkfifo(const char *pathname, mode_t mode);
Функция создает специальный FIFO файл с именем pathname , а параметр mode задает права доступа к файлу.

Примечание: mode используется в сочетании с текущим значением umask следующим образом: (mode & ~umask) . Результатом этой операции и будет новое значение umask для создаваемого нами файла. По этой причине мы используем 0777 (S_IRWXO | S_IRWXG | S_IRWXU ), чтобы не затирать ни один бит текущей маски.
Как только файл создан, любой процесс может открыть этот файл для чтения или записи также, как открывает обычный файл. Однако, для корректного использования файла, необходимо открыть его одновременно двумя процессами/потоками, одним для получение данных (чтение файла), другим на передачу (запись в файл).

В случае успешного создания FIFO файла, mkfifo() возвращает 0 (нуль). В случае каких либо ошибок, функция возвращает -1 и выставляет код ошибки в переменную errno .

Типичные ошибки, которые могут возникнуть во время создания канала:

  • EACCES - нет прав на запуск (execute) в одной из директорий в пути pathname
  • EEXIST - файл pathname уже существует, даже если файл - символическая ссылка
  • ENOENT - не существует какой-либо директории, упомянутой в pathname , либо является битой ссылкой
  • ENOSPC - нет места для создания нового файла
  • ENOTDIR - одна из директорий, упомянутых в pathname , на самом деле не является таковой
  • EROFS - попытка создать FIFO файл на файловой системе «только-на-чтение»
Чтение и запись в созданный файл производится с помощью функций read() и write() .

Пример

mkfifo.c
#include #include #include #include #define NAMEDPIPE_NAME "/tmp/my_named_pipe" #define BUFSIZE 50 int main (int argc, char ** argv) { int fd, len; char buf; if (mkfifo(NAMEDPIPE_NAME, 0777)) { perror("mkfifo"); return 1; } printf("%s is created\n", NAMEDPIPE_NAME); if ((fd = open(NAMEDPIPE_NAME, O_RDONLY)) <= 0) { perror("open"); return 1; } printf("%s is opened\n", NAMEDPIPE_NAME); do { memset(buf, "\0", BUFSIZE); if ((len = read(fd, buf, BUFSIZE-1)) <= 0) { perror("read"); close(fd); remove(NAMEDPIPE_NAME); return 0; } printf("Incomming message (%d): %s\n", len, buf); } while (1); } [скачать ]

Мы открываем файл только для чтения (O_RDONLY ). И могли бы использовать O_NONBLOCK модификатор, предназначенный специально для FIFO файлов, чтобы не ждать когда с другой стороны файл откроют для записи. Но в приведенном коде такой способ неудобен.

Компилируем программу, затем запускаем ее:
$ gcc -o mkfifo mkfifo.c $ ./mkfifo
В соседнем терминальном окне выполняем:
$ echo "Hello, my named pipe!" > /tmp/my_named_pipe
В результате мы увидим следующий вывод от программы:
$ ./mkfifo /tmp/my_named_pipe is created /tmp/my_named_pipe is opened Incomming message (22): Hello, my named pipe! read: Success

Разделяемая память

Следующий тип межпроцессного взаимодействия - разделяемая память (shared memory ). Схематично изобразим ее как некую именованную область в памяти, к которой обращаются одновременно два процесса:


Для выделения разделяемой памяти будем использовать POSIX функцию shm_open() :
#include int shm_open(const char *name, int oflag, mode_t mode);
Функция возвращает файловый дескриптор, который связан с объектом памяти. Этот дескриптор в дальнейшем можно использовать другими функциями (к примеру, mmap() или mprotect() ).

Целостность объекта памяти сохраняется, включая все данные связанные с ним, до тех пор пока объект не отсоединен/удален (shm_unlink() ). Это означает, что любой процесс может получить доступ к нашему объекту памяти (если он знает его имя) до тех пор, пока явно в одном из процессов мы не вызовем shm_unlink() .

Переменная oflag является побитовым «ИЛИ» следующих флагов:

  • O_RDONLY - открыть только с правами на чтение
  • O_RDWR - открыть с правами на чтение и запись
  • O_CREAT - если объект уже существует, то от флага никакого эффекта. Иначе, объект создается и для него выставляются права доступа в соответствии с mode.
  • O_EXCL - установка этого флага в сочетании с O_CREATE приведет к возврату функцией shm_open ошибки, если сегмент общей памяти уже существует.
Как задается значение параметра mode подробно описано в предыдущем параграфе «передача сообщений».

После создания общего объекта памяти, мы задаем размер разделяемой памяти вызовом ftruncate() . На входе у функции файловый дескриптор нашего объекта и необходимый нам размер.

Пример

Следующий код демонстрирует создание, изменение и удаление разделяемой памяти. Так же показывается как после создания разделяемой памяти, программа выходит, но при следующем же запуске мы можем получить к ней доступ, пока не выполнен shm_unlink() .
shm_open.c
#include #include #include #include #include #include #define SHARED_MEMORY_OBJECT_NAME "my_shared_memory" #define SHARED_MEMORY_OBJECT_SIZE 50 #define SHM_CREATE 1 #define SHM_PRINT 3 #define SHM_CLOSE 4 void usage(const char * s) { printf("Usage: %s ["text"]\n", s); } int main (int argc, char ** argv) { int shm, len, cmd, mode = 0; char *addr; if (argc < 2) { usage(argv); return 1; } if ((!strcmp(argv, "create") || !strcmp(argv, "write")) && (argc == 3)) { len = strlen(argv); len = (len<=SHARED_MEMORY_OBJECT_SIZE)?len:SHARED_MEMORY_OBJECT_SIZE; mode = O_CREAT; cmd = SHM_CREATE; } else if (! strcmp(argv, "print")) { cmd = SHM_PRINT; } else if (! strcmp(argv, "unlink")) { cmd = SHM_CLOSE; } else { usage(argv); return 1; } if ((shm = shm_open(SHARED_MEMORY_OBJECT_NAME, mode|O_RDWR, 0777)) == -1) { perror("shm_open"); return 1; } if (cmd == SHM_CREATE) { if (ftruncate(shm, SHARED_MEMORY_OBJECT_SIZE+1) == -1) { perror("ftruncate"); return 1; } } addr = mmap(0, SHARED_MEMORY_OBJECT_SIZE+1, PROT_WRITE|PROT_READ, MAP_SHARED, shm, 0); if (addr == (char*)-1) { perror("mmap"); return 1; } switch (cmd) { case SHM_CREATE: memcpy(addr, argv, len); addr = "\0"; printf("Shared memory filled in. You may run "%s print" to see value.\n", argv); break; case SHM_PRINT: printf("Got from shared memory: %s\n", addr); break; } munmap(addr, SHARED_MEMORY_OBJECT_SIZE); close(shm); if (cmd == SHM_CLOSE) { shm_unlink(SHARED_MEMORY_OBJECT_NAME); } return 0; } [скачать ]

После создания объекта памяти мы установили нужный нам размер shared memory вызовом ftruncate() . Затем мы получили доступ к разделяемой памяти при помощи mmap() . (Вообще говоря, даже с помощью самого вызова mmap() можно создать разделяемую память. Но отличие вызова shm_open() в том, что память будет оставаться выделенной до момента удаления или перезагрузки компьютера.)

Компилировать код на этот раз нужно с опцией -lrt :
$ gcc -o shm_open -lrt shm_open.c
Смотрим что получилось:
$ ./shm_open create "Hello, my shared memory!" Shared memory filled in. You may run "./shm_open print" to see value. $ ./shm_open print Got from shared memory: Hello, my shared memory! $ ./shm_open create "Hello!" Shared memory filled in. You may run "./shm_open print" to see value. $ ./shm_open print Got from shared memory: Hello! $ ./shm_open close $ ./shm_open print shm_open: No such file or directory
Аргумент «create» в нашей программе мы используем как для создания разделенной памяти, так и для изменения ее содержимого.

Зная имя объекта памяти, мы можем менять содержимое разделяемой памяти. Но стоит нам вызвать shm_unlink() , как память перестает быть нам доступна и shm_open() без параметра O_CREATE возвращает ошибку «No such file or directory».

Семафор

Семафор - самый часто употребляемый метод для синхронизации потоков и для контролирования одновременного доступа множеством потоков/процессов к общей памяти (к примеру, глобальной переменной). Взаимодействие между процессами в случае с семафорами заключается в том, что процессы работают с одним и тем же набором данных и корректируют свое поведение в зависимости от этих данных.

Есть два типа семафоров:

  1. семафор со счетчиком (counting semaphore), определяющий лимит ресурсов для процессов, получающих доступ к ним
  2. бинарный семафор (binary semaphore), имеющий два состояния «0» или «1» (чаще: «занят» или «не занят»)
Рассмотрим оба типа семафоров.

Семафор со счетчиком

Смысл семафора со счетчиком в том, чтобы дать доступ к какому-то ресурсу только определенному количеству процессов. Остальные будут ждать в очереди, когда ресурс освободится.

Итак, для реализации семафоров будем использовать POSIX функцию sem_open() :
#include sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
В функцию для создания семафора мы передаем имя семафора, построенное по определенным правилам и управляющие флаги. Таким образом у нас получится именованный семафор.
Имя семафора строится следующим образом: в начале идет символ "/" (косая черта), а следом латинские символы. Символ «косая черта» при этом больше не должен применяться. Длина имени семафора может быть вплоть до 251 знака.

Если нам необходимо создать семафор, то передается управляющий флаг O_CREATE . Чтобы начать использовать уже существующий семафор, то oflag равняется нулю. Если вместе с флагом O_CREATE передать флаг O_EXCL , то функция sem_open() вернет ошибку, в случае если семафор с указанным именем уже существует.

Параметр mode задает права доступа таким же образом, как это объяснено в предыдущих главах. А переменной value инициализируется начальное значение семафора. Оба параметра mode и value игнорируются в случае, когда семафор с указанным именем уже существует, а sem_open() вызван вместе с флагом O_CREATE .

Для быстрого открытия существующего семафора используем конструкцию:
#include sem_t *sem_open(const char *name, int oflag); , где указываются только имя семафора и управляющий флаг.

Пример семафора со счетчиком

Рассмотрим пример использования семафора для синхронизации процессов. В нашем примере один процесс увеличивает значение семафора и ждет, когда второй сбросит его, чтобы продолжить дальнейшее выполнение.
sem_open.c
#include #include #include #include #define SEMAPHORE_NAME "/my_named_semaphore" int main(int argc, char ** argv) { sem_t *sem; if (argc == 2) { printf("Dropping semaphore...\n"); if ((sem = sem_open(SEMAPHORE_NAME, 0)) == SEM_FAILED) { perror("sem_open"); return 1; } sem_post(sem); perror("sem_post"); printf("Semaphore dropped.\n"); return 0; } if ((sem = sem_open(SEMAPHORE_NAME, O_CREAT, 0777, 0)) == SEM_FAILED) { perror("sem_open"); return 1; } printf("Semaphore is taken.\nWaiting for it to be dropped.\n"); if (sem_wait(sem) < 0) perror("sem_wait"); if (sem_close(sem) < 0) perror("sem_close"); return 0; } [скачать ]

В одной консоли запускаем:
$ ./sem_open Semaphore is taken. Waiting for it to be dropped. <-- здесь процесс в ожидании другого процесса sem_wait: Success sem_close: Success
В соседней консоли запускаем:
$ ./sem_open 1 Dropping semaphore... sem_post: Success Semaphore dropped.

Бинарный семафор

Вместо бинарного семафора, для которого так же используется функция sem_open, я рассмотрю гораздо чаще употребляемый семафор, называемый «мьютекс» (mutex).

Мьютекс по существу является тем же самым, чем является бинарный семафор (т.е. семафор с двумя состояниями: «занят» и «не занят»). Но термин «mutex» чаще используется чтобы описать схему, которая предохраняет два процесса от одновременного использования общих данных/переменных. В то время как термин «бинарный семафор» чаще употребляется для описания конструкции, которая ограничивает доступ к одному ресурсу. То есть бинарный семафор используют там, где один процесс «занимает» семафор, а другой его «освобождает». В то время как мьютекс освобождается тем же процессом/потоком, который занял его.

Без мьютекса не обойтись в написании, к примеру базы данных, к которой доступ могут иметь множество клиентов.

Для использования мьютекса необходимо вызвать функцию pthread_mutex_init():
#include Int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
Функция инициализирует мьютекс (перемнную mutex ) аттрибутом mutexattr . Если mutexattr равен NULL , то мьютекс инициализируется значением по умолчанию. В случае успешного выполнения функции (код возрата 0), мьютекс считается инициализированным и «свободным».

Типичные ошибки, которые могут возникнуть:

  • EAGAIN - недостаточно необходимых ресурсов (кроме памяти) для инициализации мьютекса
  • ENOMEM - недостаточно памяти
  • EPERM - нет прав для выполнения операции
  • EBUSY - попытка инициализировать мьютекс, который уже был инициализирован, но не унечтожен
  • EINVAL - значение mutexattr не валидно
Чтобы занять или освободить мьютекс, используем функции:
int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);
Функция pthread_mutex_lock() , если mutex еще не занят, то занимает его, становится его обладателем и сразу же выходит. Если мьютекс занят, то блокирует дальнейшее выполнение процесса и ждет освобождения мьютекса.
Функция pthread_mutex_trylock() идентична по поведению функции pthread_mutex_lock() , с одним исключением - она не блокирует процесс, если mutex занят, а возвращает EBUSY код.
Фунция pthread_mutex_unlock() освобождает занятый мьютекс.

Коды возврата для pthread_mutex_lock() :

  • EINVAL - mutex неправильно инициализирован
  • EDEADLK - мьютекс уже занят текущим процессом
Коды возврата для pthread_mutex_trylock() :
  • EBUSY - мьютекс уже занят
Коды возврата для pthread_mutex_unlock() :
  • EINVAL - мьютекс неправильно инициализирован
  • EPERM - вызывающий процесс не является обладателем мьютекса

Пример mutex

mutex.c
#include #include #include #include static int counter; // shared resource static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void incr_counter(void *p) { do { usleep(10); // Let"s have a time slice between mutex locks pthread_mutex_lock(&mutex); counter++; printf("%d\n", counter); sleep(1); pthread_mutex_unlock(&mutex); } while (1); } void reset_counter(void *p) { char buf; int num = 0; int rc; pthread_mutex_lock(&mutex); // block mutex just to show message printf("Enter the number and press "Enter" to initialize the counter with new value anytime.\n"); sleep(3); pthread_mutex_unlock(&mutex); // unblock blocked mutex so another thread may work do { if (gets(buf) != buf) return; // NO fool-protection ! Risk of overflow ! num = atoi(buf); if ((rc = pthread_mutex_trylock(&mutex)) == EBUSY) { printf("Mutex is already locked by another process.\nLet"s lock mutex using pthread_mutex_lock().\n"); pthread_mutex_lock(&mutex); } else if (rc == 0) { printf("WOW! You are on time! Congratulation!\n"); } else { printf("Error: %d\n", rc); return; } counter = num; printf("New value for counter is %d\n", counter); pthread_mutex_unlock(&mutex); } while (1); } int main(int argc, char ** argv) { pthread_t thread_1; pthread_t thread_2; counter = 0; pthread_create(&thread_1, NULL, (void *)&incr_counter, NULL); pthread_create(&thread_2, NULL, (void *)&reset_counter, NULL); pthread_join(thread_2, NULL); return 0; } [скачать ]

Данный пример демонстрирует совместный доступ двух потоков к общей переменной. Один поток (первый поток) в автоматическом режиме постоянно увеличивает переменную counter на единицу, при этом занимая эту переменную на целую секунду. Этот первый поток дает второму доступ к переменной count только на 10 миллисекунд, затем снова занимает ее на секунду. Во втором потоке предлагается ввести новое значение для переменной с терминала.

Если бы мы не использовали технологию «мьютекс», то какое значение было бы в глобальной переменной, при одновременном доступе двух потоков, нам не известно. Так же во время запуска становится очевидна разница между pthread_mutex_lock() и pthread_mutex_trylock() .

Компилировать код нужно с дополнительным параметром -lpthread :
$ gcc -o mutex -lpthread mutex.c
Запускаем и меняем значение переменной просто вводя новое значение в терминальном окне:
$ ./mutex Enter the number and press "Enter" to initialize the counter with new value anytime. 1 2 3 30 <--- новое значение переменной Mutex is already locked by another process. Let"s lock mutex using pthread_mutex_lock(). New value for counter is 30 31 32 33 1 <--- новое значение переменной Mutex is already locked by another process. Let"s lock mutex using pthread_mutex_lock(). New value for counter is 1 2 3

Вместо заключения

В следующих статьях я хочу рассмотреть технологии d-bus и RPC. Если есть интерес, дайте знать.
Спасибо.

UPD: Обновил 3-ю главу про семафоры. Добавил подглаву про мьютекс.

Теги:

  • linux
  • posix
  • ipc
  • программирование
Добавить метки

Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: