Синхронизация потоков c linux

Обновлено: 03.07.2024

Pthread-ы обеспечивают синхронизацию потоков в форме взаимного исключения (мьютексов) и условных переменных.

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

Общая последовательность для защиты структуры общих данных с помощью мьютекса:

работаем с общими данными

Условная переменная представляет собой механизм синхронизации, который является более полезным для ожидания событий, чем для блокировки ресурса. Условная переменная связана с предикатом (логическим выражением, которое имеет значение ИСТИНА, TRUE, или ЛОЖЬ, FALSE), основанным на некоторых общих данных. Функции отправляются спать на условной переменной и пробуждают один или все потоки, если результат предиката изменяется.

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

Давайте подробно обсудим реализацию мьютекса и условной переменной для pthread-ов.

Мьютекс для pthread-ов

Мьютекс инициализируется во время определения:

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

Он также может быть проинициализирован во время выполнения вызовом функции pthread_mutex_init .

int pthread_mutex_init(pthread_mutex_t *mutex,

const pthread_mutexattr_t *mutexattr);

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

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

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

Общие данные могут быть защищены с помощью функций блокировки и разблокировки мьютекса следующим образом:

/* работаем с общими данными */

Есть три типа мьютекса.

Поведения этих трёх типов мьютекса похожи; они отличаются только когда владелец мьютекса вновь вызывает pthread_mutex_lock , что снова захватить его.

Быстрый, рекурсивный мьютекс и мьютекс проверки ошибки инициализируются во время определения следующим образом:

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

/* Мьютекс проверки ошибки */

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

Рекурсивные мьютекс инициализируется во время выполнения следующим образом:

Мьютекс проверки ошибки инициализируется во время выполнения аналогично показанному выше; только в вызове pthread_mutexattr_settype изменяется тип мьютекса на PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP .

Условная переменная pthread-ов

Как и мьютекс, условная переменная инициализируется либо во время определения, либо во время выполнения вызовом функции pthread_cond_init .

pthread_cond_t cond_var = PTHREAD_COND_INITIALIZER;

Давайте вернёмся к нашему MP3-плееру, чтобы понять различные операции с условной переменной pthread-ов. В них участвуют три объекта, показанные на Рисунке 6.6.

Основной поток порождает поток декодера при запуске приложения.

/* Создаём поток декодера звука */

if (pthread_create(&decoder_tid, NULL, audio_decoder,

printf("Audio decoder thread creation failed.\n");

Возникают три вопроса:

Чтобы ответить на эти вопросы, давайте сначала посмотрим на детали потока декодера звука.

void* audio_decoder(void *unused)

printf("Audio Decoder thread started\n");

/* декодируем данные в буфере */

/* посылаем декодированные данные на выход для воспроизведения */

Обратите, пожалуйста, внимание на следующий кусок кода в функции audio_decoder .

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

int pthread_cond_wait(pthread_cond_t *cond,

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

<--- О состоянии сигнализирует другой поток --->

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

pthread_mutex_lock(&lock); <-- Получаем мьютекс

while(is_empty_queue()) <-- проверяем предикат

pthread_cond_wait(&cond,&lock);<-- засыпаем на условной переменной

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

Теперь, что происходит с мьютексом, когда поток переходит в спящий режим в pthread_cond_wait ? Если мьютекс остаётся в состоянии блокировки, то никакой другой поток не сможет просигнализировать о состоянии, так как этот поток тоже попытается захватить тот же мьютекс перед изменением предиката. У нас проблема: один поток удерживает блокировки и спит на условной переменной, а другой поток ожидает блокировку для установки состояния. Для того чтобы избежать такой проблемы, соответствующий мьютекс должен быть разблокирован после засыпания потока на условной переменной. Это делается в функции pthread_cond_wait . Функция помещает поток в состояние сна и автоматически освобождает мьютекс.

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

pthread_mutex_lock(&lock); <-- Получаем мьютекс

while(is_empty_queue()) <-- Проверяем условие

pthread_cond_wait(&cond,&lock); <-- ждём на условной переменной

<-- Мьютекс повторно захватывается внутри pthread_cond_wait -->

buffer = get_queue(); <-- Обрабатываем условие

pthread_mutex_unlock(&lock);<-- По завершении освобождаем мьютекс

Давайте посмотрим, как поток может просигнализировать о состоянии. Шагами являются:

Основной поток нашего плеера пробуждает поток декодера звука после добавления в очередь данных.

char *buffer = (char *)malloc(MAX_SIZE);

fread(buffer, MAX_SIZE, 1, fp);

pthread_mutex_lock(&lock); <-- Получаем мьютекс

add_queue(buffer); <-- изменяем условие. Добавление

буфера в очередь делает её

pthread_mutex_unlock(&lock); <-- Освобождаем мьютекс

pthread_cond_signal(&cond); <-- Пробуждаем поток декодера

pthread_cond_signal пробуждает единственный поток, спящий на условной переменной. Чтобы пробудить все потоки, которые спят на условной переменной, также доступна функция pthread_cond_broadcast .

1. Резьбовой Самая большая особенность - это совместное использование ресурсов , Несколько потоков могут работать с общими ресурсами;
2. Последовательность потоков, управляющих общими ресурсами, не определена;
3. Операция процессора с памятью обычно не является атомарной.

2. Между несколькими потоками существует несколько особых критических ресурсов:

Глобальные данные 、 Данные кучи 、 Дескриптор файла Распространяется между несколькими потоками.

Три, метод обработки

Linux предоставляет множество способов обработки синхронизации потоков, наиболее часто используемыми являются Мьютекс 、 Переменная состояния с участием сигнал 。

3.1 Критическая зона

3.1.1 Обзор

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

3.1.2 Примитив операции:

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

3.1.3 Выбор критического сечения

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

3.2 Мьютекс

3.2.1 Обзор

Блокировки взаимного исключения аналогичны критическим областям. Они полностью контролируют критические ресурсы. Если один поток завершает операцию блокировки, другие потоки больше не могут завершить блокировку и не могут получить доступ к критическим ресурсам.

3.2.2 Механизм блокировки


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

3.2.3 Примитивы операций

3.2.4 Примеры

3.2 Тупик

1. Тот же поток снова запрашивает блокировку A, но владеет блокировкой A.
2. Поток 1 владеет блокировкой A и запрашивает блокировку B; поток 2 владеет блокировкой B и запрашивает блокировку A

3.3 Переменные условия

3.3.1 Обзор

1. В отличие от мьютекса, условная переменная Ждать Не используется для запирания.
2. Используется переменная условия. Автоматически блокировать поток , Пока не возникнет особая ситуация. Обычно условные переменные и блокировки мьютексов используются одновременно.
3. Переменная условия разделена на две части: состояние с участием переменная . Само условие защищено мьютексом. Перед изменением состояния условия поток должен заблокировать мьютекс. Переменные состояния позволяют нам спать и ждать появления определенного условия.
4. Переменные условия - это механизм синхронизации с использованием глобальных переменных, совместно используемых потоками. , В основном включает в себя два действия: один поток ждет, пока "условие переменной условия не будет установлено" и зависает; другой поток выполняет "условие установлено" (дает сигнал условного установления). Состояние проверяется под защитой блокировки мьютекса. Если условие ложно, поток автоматически блокирует и освобождает мьютекс, ожидая изменения состояния. Если другой поток изменяет условие, он отправляет сигнал связанной переменной условия, пробуждает один или несколько потоков, ожидающих его, повторно запрашивает мьютекс и повторно оценивает условие. Если два процесса совместно используют доступную для чтения и записи память, условные переменные можно использовать для синхронизации потоков между двумя процессами.

3.3.2 Примитивы операций

3.3.3 Пример (модель производителя-потребителя)

3.3 Семафор

3.3.1 Обзор

1. "Семафор" действует как страж кода.
2. Двоичный семафор: один из простейших семафоров, только «0», «1» два значения.
3. Семафор подсчета: имеет больший диапазон значений, обычно используется, чтобы надеяться, что ограниченное число потоков может выполнить данный код.
4. Подобно процессам, потоки также могут взаимодействовать через семафоры, хотя они легковесны. Имена семафорных функций начинаются с " sem_ "начало.
5. Заголовочный файл <semaphore.h>

3.3.2 Примитивы операций

1. Инициализация семафора

эффект: Инициализировать данный объект семафора

2. Отпустите семафор.

эффект: Добавить «1» к значению семафора и уведомить другие ожидающие потоки.

3. Дождитесь семафора

эффект: Вычесть «1» из значения семафора, но он всегда будет ждать, пока семафор не станет ненулевым значением, прежде чем начинать вычитание.

Мы продолжаем знакомство с многопоточностью в Linux. В предыдущей статье мы научились создавать потоки и досрочно завершать их. Мы уже знаем, что если запрос на досрочное завершение потока поступил в неподходящий момент, поток может отложить досрочное завершение до тех пор, пока не станет готов к нему.

Механизм отложенного досрочного завершения очень полезен, но для действительно эффективного управления завершением потоков необходим еще и механизм, оповещающий поток о досрочном завершении. Оповещение о завершении потоков в Unix-системах реализовано на основе тех же принципов, что и оповещение о завершении самостоятельных процессов. Если нам нужно выполнять какие-то специальные действия в момент завершения потока (нормального или досрочного), мы устанавливаем функцию-обработчик, которая будет вызвана перед тем, как поток завершит свою работу. Для потоков наличие обработчика завершения даже более важно, чем для процессов. Предположим, что поток выделяет блок динамической памяти и затем внезапно завершается по требованию другого потока. Если бы поток был самостоятельным процессом, ничего особенно неприятного не случилось бы, так как система сама убрала бы за ним мусор. В случае же процесса-потока не высвобожденный блок памяти так и останется «висеть» в адресном пространстве многопоточного приложения. Если потоков много, а ситуации, требующие досрочного завершения, возникают часто, утечки памяти могут оказаться значительными. Устанавливая обработчик завершения потока, высвобождающий занятую память, мы можем быть уверены, что поток не оставит за собой бесхозных блоков памяти (если, конечно, в системе не случится какого-то более серьезного сбоя).

Для установки обработчика завершения потока применяется макрос pthread_cleanup_push(3). Подчеркиваю жирной красной чертой, pthread_cleanup_push() – это макрос, а не функция . Неправильное использование макроса pthread_cleanup_push() может привести к неожиданным синтаксическим ошибкам. У макроса pthread_cleanup_push() два аргумента. В первом аргументе макросу должен быть передан адрес функции-обработчика завершения потока, а во втором – нетипизированный указатель, который будет передан как аргумент при вызове функции-обработчика. Этот указатель может указывать на что угодно, мы сами решаем, какие данные должны быть переданы обработчику завершения потока. Макрос pthread_cleanup_push() помещает переданные ему адрес функции- обработчика и указатель в специальный стек. Само слово «стек» указывает, что мы можем назначить потоку произвольное число функций-обработчиков завершения. Поскольку в стек записывается не только адрес функции, но и ее аргумент, мы можем назначить один и тот же обработчик с несколькими разными аргументами.

В процессе завершения потока функции-обработчики и их аргументы должны быть извлечены из стека и выполнены. Извлечение обработчиков из стека и их выполнение может производиться либо явно, либо автоматически. Автоматически обработчики завершения потока выполняются при вызове потоком функции pthread_exit(), завершающей работу потока, а также при выполнении потоком запроса на досрочное завершение. Явным образом обработчики завершения потока извлекаются из стека с помощью макроса pthread_cleanup_pop(3). Во всех случаях обработчики извлекаются из стека (и выполняются) в порядке, противоположном тому, в котором они были помещены в стек. Если мы используем макрос pthread_cleanup_pop() явно, мы можем указать, что обработчик необходимо только извлечь из стека, но выполнять его не следует. Мы рассмотрим методы назначения и выполнения обработчиков завершения потока на простом примере (программа exittest):

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

Еще один тонкий момент связан с выходом из функции потока с помощью оператора return. Сам по себе такой выход из функции потока не приводит к вызову обработчиков завершения. В нашем примере мы вызвали обработчики явно с помощью pthread_cleanup_pop() непосредственно перед выполнением return, но рассмотрим другой вариант функции thread_func():

Пусть этот вариант выглядит несколько неестественно, суть его в том, что теперь в функции потока определено несколько точек выхода. При выполнении условия i == 2 функция потока завершится в результате выполнения оператора return и обработчик завершения потока при этом вызван не будет. Эту проблему нельзя решить добавлением еще одного макроса pthread_cleanup_pop(). Вариант функции

вообще не скомпилируется, поскольку лишний макрос pthread_cleanup_pop() нарушит синтаксис программы. Правильное решение заключается в использовании функции pthread_exit() вместо return:

Средства синхронизации потоков

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

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

Семафоры – не единственное средство синхронизации потоков. Для разграничения доступа к глобальным объектам потоки могут использовать мьютексы. Все функции и типы данных, имеющие отношение к мьютексам, определены в файле pthread.h. Мьютекс создается вызовом функции pthread_mutex_init(3). В качестве первого аргумента этой функции передается указатель на переменную pthread_mutex_t, которая играет роль идентификатора нового мьютекса. Вторым аргументом функции pthread_mutex_init() должен быть указатель на переменную типа pthread_mutexattr_t. Эта переменная позволяет установить дополнительные атрибуты мьютекса. Если нам нужен обычный мьютекс, мы можем передать во втором параметре значение NULL. Для того чтобы получить исключительный доступ к некоему глобальному ресурсу, поток вызывает функцию pthread_mutex_lock(3), (в этом случае говорят, что «поток захватывает мьютекс»). Единственным параметром функции pthread_mutex_lock() должен быть идентификатор мьютекса. Закончив работу с глобальным ресурсом, поток высвобождает мьютекс с помощью функции pthread_mutex_unlock(3), которой также передается идентификатор мьютекса. Если поток вызовет функцию pthread_mutex_lock() для мьютекса, уже захваченного другим потоком, эта функция не вернет управление до тех пор, пока другой поток не высвободит мьютекс с помощью вызова pthread_mutex_unlock() (после этого мьютекс, естественно, перейдет во владение нового потока). Удаление мьютекса выполняется с помощью функции pthread_mutex_destroy(3). Стоит отметить, что в отличие от многих других функций, приостанавливающих работу потока, вызов pthread_mutex_lock() не является точкой останова. Иначе говоря, поток, находящийся в режиме отложенного досрочного завершения, не может быть завершен в тот момент, когда он ожидает выхода из pthread_mutex_lock().

Атрибуты потоков

Вы можете превратить присоединяемый поток в отделенный с помощью вызова функции pthread_detach(3), однако придать потоку свойство «отделенности» можно и на этапе его создания, с помощью дополнительного атрибута DETACHED. Для того чтобы назначить потоку дополнительные атрибуты, нужно сначала создать объект, содержащий набор атрибутов. Этот объект создается функцией pthread_attr_init(3). Единственный аргумент этой функции – указатель на переменную типа pthread_attr_t, которая служит идентификатором набора атрибутов. Функция pthread_attr_init() инициализирует набор атрибутов потока значениями, заданными по умолчанию, так что мы можем модифицировать только те атрибуты, которые нас интересуют, и не беспокоиться об остальных. Для добавления атрибутов в набор используются специальные функции с именами pthread_attr_set . Например, для того, чтобы добавить атрибут «отделенности», мы вызываем функцию pthread_attr_setdetachstate(3). Первым аргументом этой функции должен быть адрес объекта набора атрибутов, а вторым аргументом – константа, определяющая значение атрибута. Константа PTHREAD_CREATE_DETACHED указывает, что создаваемый поток должен быть отделенным, тогда как константа PTHREAD_CREATE_JOINABLE определяет создание присоединяемого (joinable) потока, который может быть синхронизирован функций pthread_join(3). После того, как мы добавили необходимые значения в набор атрибутов потока, мы вызываем функцию создания потока pthread_create(). Набор атрибутов потока передается в качестве второго аргумента этой функции.

На этом мы закончим ликбез, посвященный потокам. Если вы любите потоки также сильно, как люблю их я, к вашим услугам неисчерпаемое море специальной литературы (например, книга Ричарда Стивенса) и страницы man, в которых вы найдете все то, чему не хватило места на страницах журнала. Ну а перед тем, как приступать к чтению следующей статьи этой серии, запаситесь серой, ибо мы познакомимся с демонами.

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

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

Примечание: 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 файл на файловой системе «только-на-чтение»

Пример

mkfifo.c

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

Компилируем программу, затем запускаем ее:

В соседнем терминальном окне выполняем:

В результате мы увидим следующий вывод от программы:

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


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

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

Функция возвращает файловый дескриптор, который связан с объектом памяти. Этот дескриптор в дальнейшем можно использовать другими функциями (к примеру, mmap() или mprotect()).

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

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

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

Пример

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

shm_open.c

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

Компилировать код на этот раз нужно с опцией -lrt:

Смотрим что получилось:

Аргумент «create» в нашей программе мы используем как для создания разделенной памяти, так и для изменения ее содержимого.

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

Семафор

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

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

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

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

Итак, для реализации семафоров будем использовать POSIX функцию sem_open():

В функцию для создания семафора мы передаем имя семафора, построенное по определенным правилам и управляющие флаги. Таким образом у нас получится именованный семафор.
Имя семафора строится следующим образом: в начале идет символ "/" (косая черта), а следом латинские символы. Символ «косая черта» при этом больше не должен применяться. Длина имени семафора может быть вплоть до 251 знака.

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

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

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

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

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

sem_open.c

В одной консоли запускаем:

В соседней консоли запускаем:

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

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

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

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

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

Функция инициализирует мьютекс (перемнную mutex) аттрибутом mutexattr. Если mutexattr равен NULL, то мьютекс инициализируется значением по умолчанию. В случае успешного выполнения функции (код возрата 0), мьютекс считается инициализированным и «свободным».

  • EAGAIN — недостаточно необходимых ресурсов (кроме памяти) для инициализации мьютекса
  • ENOMEM — недостаточно памяти
  • EPERM — нет прав для выполнения операции
  • EBUSY — попытка инициализировать мьютекс, который уже был инициализирован, но не унечтожен
  • EINVAL — значение mutexattr не валидно

Функция pthread_mutex_lock(), если mutex еще не занят, то занимает его, становится его обладателем и сразу же выходит. Если мьютекс занят, то блокирует дальнейшее выполнение процесса и ждет освобождения мьютекса.
Функция pthread_mutex_trylock() идентична по поведению функции pthread_mutex_lock(), с одним исключением — она не блокирует процесс, если mutex занят, а возвращает EBUSY код.
Фунция pthread_mutex_unlock() освобождает занятый мьютекс.

  • EINVAL — mutex неправильно инициализирован
  • EDEADLK — мьютекс уже занят текущим процессом
  • EBUSY — мьютекс уже занят
  • EINVAL — мьютекс неправильно инициализирован
  • EINVAL — мьютекс неправильно инициализирован
  • EPERM — вызывающий процесс не является обладателем мьютекса

Пример mutex

mutex.c

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

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

Компилировать код нужно с дополнительным параметром -lpthread:

Запускаем и меняем значение переменной просто вводя новое значение в терминальном окне:

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

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

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

Читайте также: