Межпроцессные взаимодействия (IPC)
Необходимо обеспечить передачу данных, возможность использования совместно разделяемых ресурсов, обеспечить синхронизацию между различными процессами и потоками при использовании ресурсов.
Каждый процесс работает изолированно в своём адресном пространстве.
Задачи IPC:
- передача данных между процессами;
- совместное использование данных (без копирования);
- синхронизация между процессами и потоками при использовании ресурсов.
Средства IPC:
- сигналы;
- неименованные каналы (pipe);
- именованные каналы (FIFO) – особый тип файла в ФС;
- очереди сообщений (messages queue);
- разделяемая память (shared memory) – управление посредством семафоров;
- семафоры;
- сокеты.
Каналы (pipe)
Обмен данными между процессами порождает программный канал, обеспечивающий однонаправленную передачу между двумя процессами (задачами).
Системный вызов: int pipe(int *filedes);
filedes[0] – указатель на запись;
filedes[1] – указатель на чтение;
По сути, системный вызов вызывает 2 файловых дескриптора, которые позволяют открыть файл на запись и файл на чтение.
Эти каналы могут использоваться самим процессом и его потомками => pipes не подходят для взаимодействия между независимыми процессами. Используются только родственными процессами.
Работают медленно. Передача происходит через область памяти, которую выдаёт ФС (но она не является файлов, т.к. не видна извне и не именована). Возможна буферизация в рамках ОЗУ -> некоторое ускорение работы.
FIFO
В отличие от неименованных каналов (pipe) – возможен обмен данными не только между родственными процессами, так как буферизация происходит в рамках файловой системы с именованием -> cпециальный файл (тип FIFO). Чтение происходит в порядке записи.
Системный вызов: mknode(char *pathname, int mode, int dev);
*pathname – имя FIFO;
mode – флаги прав доступа;
Правила открытия канала на запись и чтение для независимых процессов:
1) при чтении n байт из N (n<N) системный вызов возвращает n байт, а остальное сохраняет до следующего чтения;
2) при чтении n байт из N (n<N) (n>N) возвращается N и происходит обработка прерывания;
3) если канал пуст, то возвращается 0 байт, если канал открыт на запись, то процесс будет заблокирован до того момента, пока не появится хотя бы 1 байт;
4) если в канал пишут несколько процессов, то информация не смешивается (у каждого свой буфер + использование tag’ов);
5) при записи, когда FIFO полон, процесс блокируется до тех пор, пока не появится место для записи.
При использовании FIFO не гарантируется атомарность операций.
Попытка записать в неоткрытый FIFO приводит к выработке сигнала SIGPIPE, который обрабатывается системой или программистом.
При записи FIFO работает как клиент-серверное приложение.
Функции сервера:
- создание FIFO (mknode());
- открытие FIFO на чтение (open());
- чтение сообщений (recv());
- запись сообщений (print());
- закрытие FIFO (close()).
Функции клиента:
- открытие FIFO на запись (open());
- запись сообщений для сервера в FIFO (send());
- закрытие FIFO (close()).
- удаление FIFO (rm()).
Пространство имен
Множество возможных имен объектов конкретного типа IPC, использующихся для очередей сообщений, семафоров, разделяемой памяти. За каждым объектом закреплено имя – ключ. Эти имена генерирует ftok() (параметры: имя файла и идентификатор проекта).
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(char *filename, char proj);
filename – имя сервера, должно существовать в момент создания ключа
Пространство имё создаётся для использования IPC неродственным процессам.
Для ссылок используются ID. У каждого свой уникальный ID.
Тип IPC | Пространство имен | Дескриптор | Системный вызов |
Канал pipe | - | Файловый дескриптор | pipe(); |
Канал FIFO | Имя файла | Файловый дескриптор | mknode(); |
Очередь сообщений | Ключ | Идентификатор | msgget(), msgctl() |
Семафор | Ключ | Идентификатор | semctl(), semset() |
Разделяемая память | Ключ | Идентификатор | shmget(), shmctl() |
Ошибка – возврат «-1».
ФД нужны для идентификации файлов процессами.
Inode нужны для того, чтобы их различало ядро.
Сообщения
Сообщения обслуживаются ОС, являются разделяемым системным ресурсом, являются частью ядра ОС, образуют очереди, которые образуют список. Сама очередь имеет уникальное имя – идентификатор.
Процессы могут читать сообщения из различных очередей.
Атрибуты сообщений:
- тип сообщения (мультиплексный/немультиплексный);
- длина сообщений (байт, может быть = 0);
- сами данные (могут быть структурированы).
Хранится очередь в виде однонаправленного односвязного списка. Для каждой очереди ядро создает свою структуру.
msgid_ds ----------------------------------------à ipc_perm
msg_perm // permission – права доступа
msg_cbytes // число байт
msg_num // число сообщений в очереди
msg_first // первый объект в списке
msg_last // последний в списке
Функции:
msg_snd() – отправка сообщения;
msg_rcv() – прием сообщения;
msg_get()– создание очереди;
msg_ctl()– удаление очереди.
Семафоры
Семафоры – средство синхронизации доступа нескольких процессов к разделяемым ресурсам (функция разрешения/запрещения использования ресурса).
Требования к семафорам:
- должны быть размещены в АП ядра (любой процесс может получить доступ к нему);
- операции над семафорами должны быть неделимыми (атомарными), что возможно только при выполнении операций в режиме ядра.
Семафоры – системный ресурс. Операции над семафорами производятся посредством системных вызовов. В UNIX системах под семафором понимается группа семафоров.
Структура для описания семафоров sem_id_ds:
sem_id – идентификатор семафора;
sem_perm – права доступа;
sem_first – указатель на первый элемент;
sem_num – число семафоров в группе;
sem_oper - время последней операции;
sem_time – время последнего изменения и другие.
Также в структуре sem хранится имя семафора, значение семафора, идентификаторы процесса, выполнившего последнюю операцию изменения семафора, число процессов, ожидающих увеличения семафора, число процессов, ожидающих обнуления семафора.
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflag);
С помощью этой функции можно получить дескриптор. После этого можно выполнять операции над семафором.
Структура sem_op – операции, определенные над семафором.
sem_op(int semid, struct sembuf *semop, size_t nops);
semid – идентификатор;
semop – указатель на структуру, содерж-ую 3 поля: номер в группе, операция, флаг операции.
struct sembuf{
short sem_num;
short sem_op;
short sem_flag;}
nops – количество операций над семафором, обычно – 3:
- если значение semop > 0 – добавляется значение, указанное в структуре sembuf.
- если значение semop = 0, процесс ожидает, пока семафор не обнулится;
- если значение semop < 0, процесс ожидает, пока значение семафора достигнет по абсолютной величине sem_op и обнуляет значение семафора.
ОС не накладывает ограничений на операции над семафорами, но для работы хватает и стандартных операций UNIX’а.
Применение семафоров с разделяемой памятью.
Разделяемая память
Разделяемая память может использоваться, если есть определенные объекты, управляющие семафорами. Применение разделяемой памяти, в соответствии со специальным механизмом позволяет повысить быстродействие.
При этом для сервера необходимо:
- получение доступа к разделяемой памяти посредством семафора;
- запись данных в область разделяемой памяти;
- освобождение памяти в соответствие со значением семафора.
Для клиента:
- получение доступа к разделяемой памяти посредством семафора;
- чтение данных из области разделяемой памяти;
- освобождение памяти в соответствие со значением семафора.
Для организации клиент-серверного приложения с доступом к разделяемой памяти необходимо 2 семафора.
Сигналы
Сигналы – способ передачи уведомления о событии, произошедшем либо между процессами, либо между процессом и ядром. Сигналы очень ресурсоемки. Ограничены с точки зрения системных средств. Они малоинформативны. Являются простейшим способом IPC. Используются для генерации простейших команд, уведомлений об ошибке. Обработка сигнала похожа на обработку прерывания. Сигнал имеет собственное имя и уникальный номер. Максимальное количество сигналов – 32.
Сигнал SIGINT генерируется при нажатии клавиши del, Ctrl+C и т.д. Сигнал может быть отправлен процессу ядром или другим процессом. Это можно сделать посредством системного вызова.
#include <sys/types.h>
#include <signal.h>
int kill (pid_t pid, int sig);
Причины генерации сигналов:
1) Ядро отправляет сигнал процессу при нажатии клавиши;
2) Внутренние прерывания (деление на 0, обращение в недопустимую область памяти). Могут быть обработаны системой либо пользователем.
3) Особые аппаратные ситуации (SIGALARM, например, таймеры);
4) Особые программные ситуации;
5) Управление заданиями (управление фоновыми и текущими задачами, например, сообщение потомка родителю о своём завершении);
6) Превышение квот;
7) Ответ на запрос по событию.
Суперпользователь может послать сигнал. Если обычный пользователь, то его процессы могут посылать друг другу сигналы в рамках одного и того же пользовательского сеанса. Сигнал процесс может отправить самому себе и своей группе.
Возможные действия при получении сигнала:
- завершение процесса с сохранением образа процесса в ОЗУ;
- завершение процесса без сохранения образа процесса в ОЗУ;
- игнорирование сигнала;
- приостановка процесса;
- возобновление процесса при условии, что он был приостановлен.
Основные действиия при приёме сигналов:
1) Завершение процесса с сохранением или без сохранения образа процесса в ОЗУ;
2) Игнорирование сигнала;
3) Приостановка процесса;
4) Возобновление процесса при условии, что он был приостановлен;
5) Блокировка сигнала (исключение SIGKILL, SIGSTOP).
6) Действия на данный сигнал, согласно умолчанию.
Процесс может изменить действие по умолчанию: на игнорирование или собственный обработчик.
Одно из возможных действий – диспозиция сигнала.
Две фазы существования сигнала:
· генерация и отправка;
· доставка и обработка.
Промежуточная стадия – ожидание доставки.
Генерация и отправка – в случае особых ситуаций, терминирование прерываний, системных вызовов, управления заданиями, ситуаций, когда возможна отправка сигналов по завершении временного интервала и уведомление (отправка сигнала по запросу самого процесса о каком-либо событии), нарушение квот.
Доставка и обработка.
Для каждого сигнала прописана обработка по умолчанию, задаваемая ядром либо обработка из нашего процесса.
Наиболее часты вариант – завершение процесса с сохранением образа в ОЗУ. Можно изменить действия по умолчанию на свой обработчик или указать системный. Можно заблокировать сигнал (отложить обработку на время).
Обработка.
Для обработки сигнала необходимо, чтобы на момент его получения процесс выполнялся. Фаза ожидания доставки может быть длительной. Могут существовать задержки между отправкой и доставкой. Обработка не может быть выполнена до тех пор, пока планировщик не выберет процесс обработки на исполнение.
Доставка.
Осуществляется ядром. Ядро от имени процесса осуществляет вызов ISSIG().
Данный системный вызов выполняется в трех случаях:
- возвращение процесса из режима ядра в пользовательский режим;
- переход процесса в состояние ожидания;
- выход процесса из состояния ожидания.
Все это происходит при условии, что приоритет процесса допускает его прерывание сигналом. Системный вызов ISSIG() определяет, есть ли сигналы, ожидающие доставки. Если нет – то процесс просто выполняется.