Unix - статьи

       

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


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

#define FTOK_FILE "./shmemserv" #define MAXLEN 512 struct memory_block { int server_lock; int client_lock; int turn; int readlast; char string[MAXLEN]; };

Механизм разделяемой памяти не налагает никаких ограничений на структуру блока памяти. Структура memory_block определенна нами, исходя исключительно из наших собственных потребностей. Первые четыре поля структуры memory_block – служебные, они нужны для реализации модифицированного алгоритма Петерсона, о котором будет сказано ниже. Последнее поле предназначено собственно для передачи данных. В нашем заголовочном файле мы не определяем ключ для идентификации разделяемого блока, но указываем имя некоего файла (в нашем случае – исполнимого файла сервера). Это имя будет передано функции ftok() для получения ключа. Естественно, это метод сработает только если сервер будет скомпилирован под именем shmemserv. Рассмотрим исходный код инициализации сервера:

key_t key; int shmid; struct memory_block * mblock; key = ftok(FTOK_FILE, 1); shmid = shmget(key, sizeof(struct memory_block), 0666 | IPC_CREAT); mblock = (struct memory_block *) shmat(shmid, 0, 0);

Как уже отмечалось, ftok() генерирует ключ, используя в качестве «затравки» имя файла, в нашем случае - имя исполнимого файла сервера. Использование имени самой программы для генерации ключа до некоторой степени гарантирует уникальность ключа. Функции для работы с разделяемой памятью объявлены в файлах sys/ipc.h sys/shm.h. Разделяемый блок памяти выделяется при помощи функции shmget(2), которой передаются три параметра. В первом параметре передается ключ, идентифицирующий выделяемый блок памяти. Второй параметр позволяет указать размер блока в байтах. В третьем параметре передается маска прав доступа и флаги, аналогичные флагам msgget(). Функция shmget() возвращает идентификатор выделенного блока памяти (его не следует путать с указателем на блок). Для того, чтобы получить указатель на созданный блок разделяемой памяти, этот блок нужно отобразить в локальное адресное пространство процесса. Отображение блока разделяемой памяти в адресное пространство процесса выполняет функция shmat(2). У этой функции тоже три параметра. Первый параметр, это идентификатор, возвращенный функцией shmget(). Во втором параметре передается желательный начальный адрес для отображения разделяемого блока в локальном адресном пространстве. Функция shmat() «постарается» отобразить разделяемый блок в локальное пространство, начиная с указанного адреса, но успешный результат не гарантирован. Если во втором параметре shmat() передать нулевое значение, функция сама выберет начальный адрес области отображения. Значение желательного адреса должно быть выравнено по границе страничных областей. Можно также не выравнивать адрес, но передать в третьем параметре функции флаг SHM_RND, и тогда функция сама скорректирует значение адреса. Среди дополнительных флагов, которые можно передать в третьем параметре, отметим флаг SHM_RDONLY, который присваивает отображаемой области статус «только для чтения». При успешном выполнении функция shmat() возвращает указатель на начало области отображения, с которым мы можем работать как с обычным указателем на выделенный блок памяти. Для того чтобы понять дальнейшую работу сервера, следует иметь в виду, что сами по себе объекты разделяемой памяти не предоставляют никаких средств синхронизации доступа, так что нам приходится самим позаботиться об этих средствах. Для синхронизации работы клиента и сервера и разграничения доступа мы используем упомянутый уже алгоритм Петерсона [], который позволяет разграничить доступ к блоку разделяемой памяти, используя неатомарные операции.




Неатомарность спин-блокировок

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

while (spin_lock == TRUE); spin_lock = TRUE; ... // Доступ к разделяемому ресурсу spin_lock = FALSE;

Проблема заключается в том, что этот алгоритм простых спин-блокировок (spin locks) не только прожорлив, но и не гарантирует надежного разграничения доступа. Допустим, что один процесс начал проверять значение переменной spin_lock, дождался того момента, когда оно станет равно FALSE и переходит к строке

spin_lock = TRUE;

В многозадачной и системе (особенно на нескольких процессорах) другой процесс, ожидающий доступа к разделяемому ресурсу, может «вклиниться» в тот момент, когда первый процесс уже проверил, но еще не изменил значение spin_lock. Второй процесс проверит значение, все еще равное FALSE и. В результате оба процесса окажутся в критической области и получат доступ к разделяемому ресурсу. Описанная проблема вызвана тем, что операция «проверить значение – изменить значение» неатомарна, то есть, ее выполнение может быть прервано другим процессом. Первый алгоритм, гарантирующий синхронизацию при использовании неатомарных операций, придумал Т. Деккер, а применил Э. Дейкстра в 1965 году. В 1981 году Г. Петерсон предложил более простой алгоритм.

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

while (spin_lock == TRUE); spin_lock = TRUE; ... // Доступ к разделяемому ресурсу spin_lock = FALSE;

Проблема заключается в том, что этот алгоритм простых спин-блокировок (spin locks) не только прожорлив, но и не гарантирует надежного разграничения доступа. Допустим, что один процесс начал проверять значение переменной spin_lock, дождался того момента, когда оно станет равно FALSE и переходит к строке spin_lock = TRUE;

В многозадачной и системе (особенно на нескольких процессорах) другой процесс, ожидающий доступа к разделяемому ресурсу, может «вклиниться» в тот момент, когда первый процесс уже проверил, но еще не изменил значение spin_lock. Второй процесс проверит значение, все еще равное FALSE и. В результате оба процесса окажутся в критической области и получат доступ к разделяемому ресурсу. Описанная проблема вызвана тем, что операция «проверить значение – изменить значение» неатомарна, то есть, ее выполнение может быть прервано другим процессом. Первый алгоритм, гарантирующий синхронизацию при использовании неатомарных операций, придумал Т. Деккер, а применил Э. Дейкстра в 1965 году. В 1981 году Г. Петерсон предложил более простой алгоритм.



Функция shmdt(2) удаляет область отображения в локальном адресном пространстве ( но не удаляет блок разделяемой памяти). Блок разделяемой памяти удаляется вызовом shmctl(shmid, IPC_RMID, 0);

который и по форме, и по сути подобен приведенному выше вызову msgctl(). Клиент получает доступ к блоку разделяемой памяти с помощью вызовов

shmid = shmget(key, sizeof(struct memory_block), 0666); if (shmid == -1) { printf("Server is not running!\n"); return EXIT_FAILURE; }mblock = ( struct memory_block *) shmat(shmid, 0, 0);

При этом мы проверяем, запущен ли сервер. Далее клиент читает информацию, записанную в разделяемый блок сервером, считывает строку с терминала и передает строку серверу, используя механизм спин-блокировок. Скомпилируйте обе программы (скомандовав make shmemdemo), запустите сервер, затем клиент и набирайте строки в окне клиента. Работа программ завершится когда вы наберете q и нажмете ввод. Поработав с программами, вы, конечно, обратили внимание на медлительность, с которой сервер отвечает клиенту. Кроме того, вы могли заметить существенный рост потребления ресурсов процессора при работе программ. Виной всему спин-блокировки, которые используются в алгоритме Петерсона. Именно из-за спин-блокировок процессор проводит значительную часть времени в цикле непрерывного опроса значения переменной. Сам алгоритм Петерсона сегодня можно найти только в учебниках по разработке операционных систем, хотя и современные ОС его практически не используют. Вместо этого ОС создают объекты синхронизации, контролирующие доступ к критическим секциям с помощью специальных атомарных операций процессора, и предоставляют пользовательским программам доступ к этим объектам. Именно такими объектами являются рассмотренные далее семафоры.


Содержание раздела