Сегменты разделяемой памяти
Каналы обычно применяются для связи двух процессов. В случае, если взаимодействующих процессов больше, либо по каким-то причинам применение каналов исключается, для передачи данных можно использовать сегменты разделяемой памяти. При создании сегмента выделяется область памяти, идентифицируемая уникальным целочисленным ключом и доступная всем процессам, которые обратятся к ней по этому ключу.
Сегмент разделяемой памяти выделяется следующей функцией:
#include <sys/shm.h>
int shmget(key_t key, size_t size, int flag);
Функции необходимо передать следующие параметры:
· key – уникальный ключ. Правила формирования ключа аналогичны случаю с семафорами;
· tsize – размер выделяемой области, в байтах;
· flag – флаг доступа, аналогичный случаю с семафорами.
При успешном завершении операции функция возвращает идентификатор созданного сегмента, в противном случае получим -1. Доступ к созданной ранее области разделяемой памяти, как и в случае с семафорами, можно получить через ключ key.
Для непосредственного использования разделяемой памяти необходимо получить адрес созданного сегмента разделяемой памяти с помощью функции shmat():
#include <sys/shm.h>
void *shmat(int shmid, const void *addr, int flag);
Функция принимает идентификатор существующего сегмента, указатель, в который будет записан адрес сегмента, и флаг доступа. После вызова функции с памятью можно работать через прямой указатель. При этом данные, записанные через этот указатель, будут доступны процессам, использующим созданный сегмент. После завершения работы необходимо вызвать функцию shmdt() для отсоединения сегмента.
Для выполнения расширенных операций над сегментом разделяемой памяти используется функция shmctl(). В спектре ее возможностей – получение и установка параметров сегмента, его удаление, а также блокирование/разблокирование. Возможность выполнения операции блокировки сегмента предоставляют не все Unix-системы; на данный момент эта возможность присутствует только в Linux и Solaris.
Чтобы избежать накопления неиспользуемых сегментов разделяемой памяти, каждый из них должен быть вручную удален после завершения работы с ним. Использование функций exit() и exec() отсоединяет сегмент, но не удаляет его. Удаление сегмента разделяемой памяти производится посредством вызова функции shmctl() с параметром IPC_RMID. В действительности удаление происходит только тогда, когда все процессы, использующие сегмент, отсоединяют его.
Windows
Среди механизмов передачи данных между процессами в Windows наиболее часто используются каналы и разделяемая память.
Каналы
Как и в Linux, каналы в Windows также применяются для связи двух процессов. Процесс, создающий канал, называют сервером, а процесс, использующий этот канал – клиентом. Каналы в Windows бывают двух типов: анонимные и именованные. Анонимные каналы быстры и универсальны, однако процессу-клиенту сложнее получить дескриптор такого канала, кроме того, они не поддерживают дуплексную передачу данных и не работают в сетях. Именованные каналы не имеют подобных недостатков, однако немного более тяжеловесны для операционной системы.
Использование каналов в Windows во многом подобно таковому в Linux. Рассмотрим основные функции для работы с каналами. Создание анонимного канала:
BOOL CreatePipe( PHANDLE hReadPipe, PHANDLE hWritePipe, LPSECURITY_ATTRIBUTES lpPipeAttributes, DWORD nSize);
Функция создает анонимный канал, используя следующие параметры:
· hReadPipe – дескриптор чтения;
· hWritePipe – дескрпитор записи;
· lpPipeAttributes – атрибуты защиты;
· nSize – количество байт, резервируемых для канала.
Создание именованного канала:
HANDLE CreateNamedPipe( LPCTSTR lpName, DWORD dwOpenMode, DWORD dwPipeMode, DWORD nMaxInstances, DWORD nOutBufferSize, DWORD nInBufferSize, DWORD nDefaultTimeOut, LPSECURITY_ATTRIBUTES lpSecurityAttributes);
Функция принимает следующие параметры:
· lpName – имя канала. Как правило, представляет собой строку вида: «\\Имя сервера\pipe\Имя канала». При использовании именованных каналов на локальной машине данная строка сокращается до «\\.\pipe\Имя канала»;
· dwOpenMode – режим открытия канала;
· nMaxInstances – максимальное количество реализаций канала;
· nOutBufferSize – размер выходного буфера;
· nInBufferSize – размер входного буфера;
· nDefaultTimeOut – время ожидания, в миллисекундах;
· lpSecurityAttributes – атрибуты защиты.
С целью дальнейшего использования канала с серверной и клиентской сторон применяют следующие функции: ConnectNamedPipe() (для именованных каналов), CreateFile(), ReadFile(), WriteFile(). После установления соединения через канал с ним можно работать как с обычным файлом.
Разделяемая память
Еще один способ передать данные между процессами – использование файлов, отображаемых в память. При этом один процесс создает специальный объект, «файловую проекцию» (File Mapping), выделяя область памяти, которая связывается с определенным файлом и в дальнейшем может быть доступна глобально из других процессов.
Для передачи данных между процессами такое решение может показаться избыточным. Однако нет необходимости в специальном файле, проекция может использоваться исключительно для выделения виртуальной памяти без привязки к конкретному файлу.
Последовательность действий при этом такова. Для создания файловой проекции используется следующая функция:
HANDLE WINAPI CreateFileMapping( HANDLE hFile, LPSECURITY_ATTRIBUTES lpAttributes, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCTSTR lpName);
Данная функция принимает параметры:
· hFile – дескриптор файла, связываемого с выделяемой областью памяти. Если связывать файл не нужно, а функция используется только для выделения общей области памяти, данный параметр устанавливатся в INVALID_HANDLE_VALUE. При этом в качестве проецируемого файла будет использоваться системный файл подкачки. В этом случае также необходимо явно указать размер выделяемой памяти в параметрах dwMaximumSizeHigh и dwMaximumSizeLow;
· lpAttributes – атрибуты безопасности;
· flProtect – флаги защиты выделенной области;
· dwMaximumSizeHigh – старшее слово размера выделяемой памяти, в байтах;
· dwMaximumSizeLow – младшее слово размера выделяемой памяти, в байтах;
· lpName – имя объекта «файловая проекция».
После создания файлового отображения необходимо получить его адрес в памяти для того, чтобы записать туда передаваемые данные. Получить адрес можно при помощи следующей функции:
LPVOID MapViewOfFile( HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap);
Принимаемые параметры:
· hFileMappingObject – дескриптор файловой проекции, полученный от предыдущей функции;
· dwDesiredAccess – режим доступа к области памяти;
· dwFileOffsetHigh – старшее слово смещения файла, с которого начинается отображение;
· dwFileOffsetLow – младшее слово смещения;
· dwNumberOfBytesToMap – число отображаемых байт. Если параметр равен нулю, отображается весь файл.
Данная функция возвращает указатель на спроецированную область памяти. После его получения можно записывать в полученную общую память необходимые данные.
Процесс-клиент должен получить доступ к памяти, выделенной другим процессом. Здесь необходимо учитывать, что файловая проекция уникально идентифицируется именем, указанным в функции CreateFileMapping(). Для получения дескриптора проекции следует воспользоваться функцией OpenFileMapping(). Далее по полученному дескриптору при помощи функции MapViewOfFile() можно получить указатель на искомую область памяти.
Задание
Задание выполняется в двух вариантах: под Linux и Windows. Необходимо создать два процесса: клиентский и серверный. Серверный процесс ждет ввода пользователем текстовой строки и по нажатию клавиши Enter инициируются следующие действия:
· клиентский процесс ожидает уведомления о том, что серверный процесс готов начать передачу данных (синхронизация);
· серверный процесс передает полученную от пользователя строку клиентскому процессу, используя либо каналы, либо сегменты разделяемой памяти / файловые проекции;
· клиентский процесс принимает строку и выводит ее на экран;
· серверный процесс ожидает уведомления от клиентского процесса об успешном получении строки;
· серверный процесс вновь ожидает ввода строки пользователем и т.д.
В данной работе продолжается освоение синхронизации процессов. Уведомление процессов должно производиться посредством семафоров. Реализация механизма непосредственной передачи данных остается на выбор студента, однако в теории освоены должны быть все варианты.
Лабораторная работа №4
Работа с потоками
Цель работы: научиться создавать и уничтожать вычислительные потоки, а также управлять ими.
Понятие потока несколько различается в системах Linux и Windows. Более того, диспетчеризация потоков может быть реализована как на уровне пользователя (облегченные потоки Windows, fibers), так и на уровне ядра. В дальнейшем будем рассматривать исключительно потоки на уровне ядра.
Linux
В Linux поток представляет собой облегченную версию процесса (lightweight process, LWP). Потоки можно представить как особые процессы, принадлежащие родительскому процессу и разделяющие с ним адресное пространство, файловые дескрипторы и обработчики сигналов. Однако, в отличие от процессов, потоки более легковесны: переключение между ними выполняется быстрее, производительность системы увеличивается. Однако использование общего адресного пространства порождает ряд проблем, связанных с синхронизацией и некоторые трудности при отладке.
При создании любого потока ему назначается так называемая потоковая функция, иногда именуемая телом потока. Эта функция содержит код, который поток должен выполнить. При выходе из потоковой функции поток завершается.
В Linux существует множество библиотек, предоставляющих интерфейс к системным вызовам, управляющим потоками. Стандартом де-факто является библиотека pthreads (POSIX threads), основные функции которой рассмотрим ниже:
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void * (*start_routine)(void *), void *arg);
Функция создает новый поток и принимает следующие параметры:
· thread – указатель, по которому будет записан идентификатор созданного потока;
· attr – атрибуты потока;
· start_routine – указатель на функцию потока. Она должна иметь прототип void * start_routine(void *). Имя функции произвольно;
· arg – аргументы, передаваемые в поток. Если при создании потока необходимо передать в него какие-то данные, их можно записать в отдельную область памяти и данным параметром передать указатель на нее. Этот указатель будет являться и аргументом потоковой функции.
Для завершения потока используется функция pthread_exit().
Поскольку потоки выполняются параллельно, вопрос о синхронизации остается открытым. Для решения этой проблемы главным образом используется функция pthread_join(). Данная функция принимает в качестве первого аргумента идентификатор потока и приостанавливает выполнение текущего потока, пока не будет завершен поток с указанным идентификатором.
Применение функции pthread_join() эффективно, но не всегда подходит для «тонкой» синхронизации, когда оба потока должны, не завершаясь, синхронизировать свои действия. В этом случае используются специальные объекты – мьютексы.
Мьютекс представляет собой бинарный флаг, имеющий два состояния: «свободно» и «занято». Для мьютексов применима общая процедура синхронизации, описанная в л.р. №2. При этом используются следующие функции:
· pthread_mutex_init – создание мьютекса;
· pthread_mutex_lock – установка мьютекса в положение «занято». Если мьютекс уже занят, функция ждет его освобождения, а потом занимает его;
· pthread_mutex_unlock – установка мьютекса в положение «свободно»;
· pthread_mutex_trylock – функция, которая отличается от аналогичной функции pthread_mutex_lock тем, что при занятости мьютекса не ждет его освобождения, а немедленно возвращает значение EBUSY;
· pthread_mutex_destroy – уничтожение мьютекса.
Windows
В Windows ситуация несколько отлична от Linux. Здесь процессы на самом деле не выполняют никакого кода, а являются лишь контейнерами для вычислительных потоков. Потоки представляют собой отдельные объекты ядра, коренным образом отличающиеся от процессов. Любой процесс может иметь несколько параллельно выполняющихся внутри него потоков, объединенных общим адресным пространством, общими таблицами дескрипторов процесса и т.д. Процесс должен содержать как минимум один поток, иначе он завершается. При создании процесса создается первичный поток, который вызывает входную функцию main(), WinMain() и т.д. При завершении входной функции управление вновь передается стартовому коду, и тот вызывает функцию ExitProcess(), завершая текущий процесс.
Работа с потоками в Windows, несмотря на основополагающие архитектурные отличия, во многом аналогична работе в Linux. В потоках Windows тоже используется понятие потоковой функции. Поток создается следующей функцией WinAPI:
HANDLE CreateThread( PSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, PTHREAD_START_ROUTINE lpStartAddress, PVOID lpParameter, DWORD dwCreationFlags, PDWORD lpThreadId);
Функция принимает следующие параметры:
· lpThreadAttributes – атрибуты безопасности потока;
· dwStackSize – размер стека, выделяемого под поток;
· lpStartAddress – адрес потоковой функции, созданной в соответствии с прототипом DWORD WINAPI ThreadFunc(PVOID pvParam);
· lpParameter – указатель на параметры, передаваемые функции потока при его создании;
· dwCreationFlags – флаг создания потока (поток запускается немедленно или создается в остановленном состоянии);
· lpThreadId – указатель, по которому будет записан идентификатор созданного потока.
Завершается поток одним из следующих способов:
· функция потока возвращает управление;
· поток вызывает функцию ExitThread();
· текущий (или другой) поток вызывает функцию TerminateThread();
· процесс, в котором выполняется поток, завершается.
Важно отметить, что создание потока при помощи функции CreateThread() может привести к некорректной работе приложения, если одновременно используются глобальные функции и переменные стандартной библиотеки C/C++ (errno, strok, strerror и т.д.). В таких случаях рекомендуется прибегнуть к функциям _beginthreadex и _endthreadex, которые реализуют аналогичную функциональность, но безопасны в работе.
Для решения проблемы синхронизации в Windows также применяются мьютексы, которые здесь, по сути, представляют собой бинарные семафоры и предназначены в первую очередь для взаимодействия процессов. Тем не менее, использование подобных глобальных объектов в многопоточном приложении не всегда оправдано.[2]
Более подходящим механизмом для синхронизации потоков являются критические секции, логика работы которых во многом сходна с работой мьютексов в pthreads. Для работы с критическими секциями существует пять основных функций:
· InitializeCriticalSection – создание и инициализация объекта «критическая секция»;
· EnterCriticalSection – вход в критическую секцию. Это действие аналогично блокировке мьютекса. Если блокируемая критическая секция уже используется каким-либо потоком, функция ждет ее освобождения;
· LeaveCriticalSection – выход из критической секции;
· TryEnterCriticalSection – попытка входа в критическую секцию. Функция аналогична EnterCriticalSection(). Если критическая секция занята, возвращается 0, в противном случае – ненулевое значение;
· DeleteCriticalSection – удаление критической секции.
Задание
Задание выполняется в двух вариантах: под Linux и Windows. Задание аналогично заданию 2.3 из лабораторной работе №2, но с реализацией с помощью потоков.
Лабораторная работа №5
Асинхронные файловые операции
Динамические библиотеки
Цель работы: научиться осуществлять асинхронные файловые операции; получить представление о динамических библиотеках, научиться создавать их и динамически использовать в приложениях.