Текст книги "Основы программирования в Linux"
Автор книги: Нейл Мэтью
Соавторы: Ричард Стоунс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 52 (всего у книги 67 страниц)
Простейший семафор – это переменная, способная принимать только значения 0 и 1, бинарный или двоичный семафор. Это наиболее распространенный вид семафора. Семафоры, принимающие много положительных значений, называют семафорами общего вида. В оставшейся части главы мы сосредоточимся на двоичных семафорах.
Определения операций P
и V
удивительно просты. Предположим, что у вас есть переменная-семафор sv
. В этом случае обе операции определяются так, как представлено в табл. 14.1.
Таблица 14.1
Р(sv) | Если sv больше нуля, она уменьшается на единицу. Если sv равна 0, выполнение данного процесса приостанавливается |
V(sv) | Если какой-то другой процесс был приостановлен в ожидании семафора sv , переменная заставляет его возобновить выполнение. Если ни один процесс не приостановлен в ожидании семафора sv , значение переменной увеличивается на единицу |
Другой способ описания семафора – считать, что переменная sv
, равная true
, когда доступна критическая секция, уменьшается на единицу с помощью P(sv)
и становится равна false
, когда критическая секция занята, и увеличивается на единицу операцией V(sv)
, когда критическая секция снова доступна. Имейте в виду, что обычная переменная, которую вы уменьшаете и увеличиваете на единицу, не годится, т.к. в языках С, С++, C# или практически в любом традиционном языке программирования у вас нет возможности сформировать единую атомарную операцию, проверяющую, равна ли переменная true
, и если это так, изменяющую ее значение на false
. Именно эта функциональная возможность делает операции с семафором особенными.
С помощью простого теоретического примера можно посмотреть, как действует семафор. Предположим, что у вас есть два процесса: proc1 и proc2, оба нуждающиеся в некоторый момент выполнения в монопольном доступе к базе данных. Вы определяете один бинарный семафор sv
, который стартует со значением 1 и доступен обоим процессам. Далее обоим процессам нужно выполнить одну и ту же обработку для доступа к критической секции программного кода; эти два процесса могут быть двумя разными выполняющимися экземплярами одной и той же программы.
Оба процесса совместно используют переменную-семафор sv
. Как только один процесс выполнил операцию P(sv)
, он получил семафор и может войти в критическую секцию программы. Второму процессу вход в критическую секцию запрещен, т.к., когда он попытается выполнить операцию P(sv)
, он вынужден будет ждать до тех пор, пока первый процесс не покинет критическую секцию и не выполнит операцию V(sv)
, освобождающую семафор.
Требуемый псевдокод у обоих процессов идентичен:
semaphore sv = 1;
loop forever {
P(sv);
critical code section;
V(sv);
noncritical code section;
}
Код на удивление прост, потому что определение операций P
и V
наделяет их большими функциональными возможностями.
Рис. 14.1
На рис. 14.1 показана схема действующих операций P
и V
, напоминающих ворота в критических секциях программного кода.
Теперь, когда вы увидели, что такое семафоры и как они действуют в теории, можно рассмотреть, как их свойства реализованы в ОС Linux. Интерфейс тщательно проработан и предлагает гораздо больше возможностей, чем обычно требуется. Все функции семафоров в Linux оперируют массивами семафоров общего вида, а не одним двоичным семафором. На первый взгляд кажется, что такой подход все усложняет, но если процесс нуждается в блокировке нескольких ресурсов, способность оперировать массивом семафоров – большое подспорье. В этой главе мы сосредоточимся на применении одиночных семафоров, поскольку в большинстве случаев это все, что вам нужно.
Далее приведены объявления функций семафоров:
#include
int semctl(int sem_id, int sem_num, int command, ...);
int semget(key_t key, int num_sems, int sem_flags);
int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
Примечание
Обычно заголовочный файл 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
. Если вы найдете его, мы полагаем, что вы примените определение из вашего справочного руководства, даже если оно отличается от приведенного на страницах этой книги.
Существует множество разных значений параметра command, допустимых в функции semctl
. Обычно применяются два из них, которые описаны далее. Более подробную информацию о функции semctl
см. в интерактивном справочном руководстве.
Два часто используемых значения command
таковы:
□ SETVAL
– применяется для инициализации семафора с заданным значением. Это значение передается как элемент val
объединения semun
. Такое действие необходимо для того, чтобы увеличить значение семафора перед первым его применением;
□ IPC_RMID
– применяется для удаления идентификатора семафора, когда он больше не нужен.
Функция semctl
возвращает разные значения, зависящие от параметра command
. Если значение команды – IPC_RMID
, функция в случае успешного завершения вернет 0 и -1 в противном случае.
Как видно из содержания предыдущих разделов, операции с семафорами могут быть очень сложными. Это не самое печальное, потому что программирование многих процессов или потоков с критическими секциями – очень трудная задача сама по себе, и наличие сложного программного интерфейса лишь увеличивает интеллектуальную нагрузку.
К счастью, большинство задач, нуждающихся в семафорах, можно решить, применяя единственный бинарный семафор – простейший тип семафора. В следующем примере (упражнение 14.1) вы используете полный программный интерфейс для создания очень простого интерфейса типа Р и V для бинарного семафора. Затем вы примените этот простенький интерфейс для демонстрации того, как функционируют семафоры.
В экспериментах с семафорами будет использоваться единственная программа sem1.с, которую вы сможете запускать несколько раз. Необязательный параметр будет применяться для того, чтобы показать, отвечает ли программа за создание и уничтожение семафора.
Вывод двух разных символов будет обозначать вход в критическую секцию и выход из нее. Программа, запущенная с параметром, выводит X
при входе в критическую секцию и выходе из нее. Другие экземпляры запущенной программы будут выводить символ О
при входе в свои критические секции и выходе из них. Поскольку в любой заданный момент времени только один процесс способен войти в свою критическую секцию, все символы X
и O
должны появляться парами.
Упражнение 14.1. Семафоры
1. После системных директив #include
вы включаете файл semun.h. Он определяет объединение типа semun
в соответствии со стандартом X/Open, если оно уже не описано в системном файле sys/sem.h. Далее следуют прототипы функций и глобальная переменная, расположенные перед входом в функцию main
. В ней создается семафор с помощью вызова semget
, который возвращает ID семафора. Если программа вызывается первый раз (т.е. вызывается с параметром и argc > 1
), выполняется вызов set_semvalue
для инициализации семафора и переменной op_char
присваивается значение O
.
#include
#include
#include
#include
#include «semun.h»
static int set_semvalue(void);
static void del_semvalue(void);
static int semaphore_p(void);
static int semaphore_v(void);
static int sem_id;
int main(int argc, char *argv[]) {
int i;
int pause_time;
char op_char = 'О';
srand((unsigned int)getpid());
sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
if (argc >1) {
if (!set_semvalue()) {
fprintf(stderr, «Failed to initialize semaphoren»);
exit(EXIT_FAILURE);
}
op_char = 'X';
sleep(2);
}
2. Далее следует цикл, в котором 10 раз выполняется вход в критическую секцию и выход из нее. Вы сначала выполняете вызов функции semaphore_p
, которая заставляет семафор ждать, когда эта программа будет готова войти в критическую секцию.
for (i = 0; i < 10; i++) {
if (!semaphore_p()) exit(EXIT_FAILURE);
printf(«%c», op_char);
fflush(stdout);
pause_time = rand() % 3;
sleep(pause_time);
printf(«%c», op_char);
fflush(stdout);
3. После критической секции вы вызываете функцию semaphore_v
, которая освобождает семафор перед повторным проходом цикла for
после ожидания в течение случайного промежутка времени. После цикла выполняется вызов функции del_semvalue
для очистки кода.
if (!semaphore_v()) exit(EXIT_FAILURE);
pause_time = rand() % 2;
sleep(pause_time);
}
printf(«n%d – finishedn», getpid());
if (argc > 1) {
sleep(10);
del_semvalue();
}
exit(EXIT_SUCCESS);
}
4. Функция set_semvalue
инициализирует семафор с помощью команды SETVAL
в вызове semctl
. Это следует сделать перед использованием семафора.
static int set_semvalue(void) {
union semun sem_union;
sem_union.val = 1;
if (semctl(sem_id, 0, SETVAL, sem_union) == -1) return(0);
return(1);
}
5. У функции del_semvalue
почти та же форма за исключением того, что в вызове semctl
применяется команда IPC_RMID
для удаления ID семафора.
static void del_semvalue(void) {
union semun sem_union;
if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
fprintf(stderr, «Failed to delete semaphoren»);
}
6. Функция semaphore_p изменяет счетчик семафора на -1. Это операция ожидания или приостановки процесса.
static int semaphore_p(void) {
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1; /* P() */
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1) {
fprintf(stderr, «semaphore_p failedn»);
return(0);
}
return(1);
}
7. Функция semaphore_v
аналогична за исключением задания элемента sem_op
структуры sembuf
, равного 1. Это операция «освобождения», в результате которой семафор снова становится доступен.
static int semaphore_v(void) {
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1; /* V() */
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1) {
fprintf(stderr, «semaphore_v failedn»);
return(0);
}
return(1);
}
Обратите внимание на то, что эта простая программа разрешает существование единственного двоичного семафора для каждой программы, хотя можно было бы увеличить количество, передав переменную семафора при необходимости. Обычно одного бинарного семафора достаточно.
Вы можете протестировать вашу программу, запустив ее несколько раз. В первый раз вы передадите параметр, чтобы сообщить программе о том, что она отвечает за создание и удаление семафора. У других экземпляров выполняющейся программы не будет параметра.
Далее приведен примерный вывод для двух запущенных экземпляров программы:
$ cc sem1.с -о sem1
$ ./sem1 1 &
[1] 1082
$ ./sem1
OOXXOOXXOOXXOOXXOOXXOOOOXXOOXXOOXXOOXXXX
1083 – finished
1082 – finished
Напоминаем, что символ О
представляет первый запущенный экземпляр программы, а символ X
– второй экземпляр выполняющейся программы. Поскольку каждый экземпляр программы выводит символ при входе в критическую секцию и при выходе из нее, каждый символ должен появляться только попарно. Как видите, символы О
и Х
на самом деле образуют пары, указывая на корректную обработку критических секций. Если программа не работает на вашей системе, можно применить команду stty -tostop
перед запуском программы, чтобы гарантировать, что фоновая программа, генерирующая вывод на tty
, не вызывает возбуждение сигнала.
Как это работает
Программа начинается с получения обозначения семафора на основе ключа (произвольного), который вы выбрали, применив функцию semget
. Флаг IPC_CREAT
приводит к созданию семафора, если он нужен.
Если у программы есть параметр, она отвечает за инициализацию семафора, которая выполняется функцией set_semvalue
, упрощенным вариантом функции общего назначения semctl
. Она также использует наличие параметра для определения символа вывода. Функция sleep
просто предоставляет некоторое время для запуска других экземпляров программы до того, как данная программа выполнит слишком много проходов своего цикла. Для включения в программу нескольких псевдослучайных промежутков времени вы используете функции srand
и rand
.
Далее программа выполняет 10 раз операторы тела цикла с псевдослучайными периодами ожидания в своей критической и некритической секциях. Критическая секция охраняется вызовами ваших функций semaphore_p
и semaphore_v
, упрощенных интерфейсов функции более общего вида semop
.
Перед удалением семафора программа, запущенная с параметром, ждет, пока завершится выполнение других экземпляров программы. Если семафор не удален, он будет продолжать существовать в системе, даже если нет программ, его использующих. В реальных программах очень важно убедиться в том, что вы случайно не оставили семафор после завершения выполнения. Он может вызвать проблемы при следующем запуске программы, кроме того, семафоры – разновидность ограниченных ресурсов, которые вы должны беречь.
Совместно используемая память
Совместно используемая или разделяемая память – вторая разновидность средств IPC. Она позволяет двум несвязанным процессам обращаться к одной и той же логической памяти. Хотя стандарт X/Open не требует этого, надо полагать, что большинство реализаций разделяемой памяти размещают память, совместно используемую разными процессами, так, что она ссылается на одну и ту же физическую память.
Совместно используемая память – это специальный диапазон адресов, создаваемых средствами IPC для одного процесса и включаемых в адресное пространство этого процесса. Другой процесс может затем "присоединить" тот же самый сегмент совместно используемой памяти к своему адресному пространству. Все процессы могут получать доступ к участкам памяти так, как будто эта память была выделена функцией malloc
. Если один процесс записывает в совместно используемую память, изменения немедленно становятся видимыми любому другому процессу, имеющему доступ к этой совместно используемой памяти.
Совместно используемая память обеспечивает эффективный способ разделения и передачи данных между разными процессами. Сама по себе совместная используемая память не предоставляет никаких средств синхронизации, поэтому вы, как правило, вынуждены применять некоторые другие механизмы для синхронизации доступа к совместно используемой памяти. Обычно совместно используемая память применяется для обеспечения эффективного доступа к обширным областям памяти, а для синхронизации доступа к ней передаются небольшие сообщения.
Не существует автоматических средств для того, чтобы помешать второму процессу начать считывание совместно используемой памяти до того, как первый процесс закончит запись в нее. За синхронизацию доступа отвечает программист. На рис. 14.2 показан принцип работы совместно используемой памяти.
Рис. 14.2
Стрелки показывают отображение логического адресного пространства каждого процесса на доступную физическую память. На практике ситуация сложнее, потому что доступная память на самом деле представляет собой смесь физической памяти и страниц памяти, которые были выгружены на диск.
Функции для работы с совместно используемой памятью напоминают функции семафоров:
#include
void *shmat(int shm_id, const void *shm_addr, int shmflg);
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
int shmdt(const void *shm_addr);
int shmget(key_t key, size_t size, int shmflg);
Как и в случае семафоров, заголовочные файлы sys/types.h и sys/ipc.h автоматически включаются в программу файлом shm.h.