Взаимодействие процессов
Виды межпроцессного взаимодействия в Linux
Межпроцессное взаимодействие (IPC, Inter-Process Communication) может быть локальным или сетевым.
Виды IPC:
- файлы (обмен данными через совместно используемые файлы)
- сигналы (некий аналог процессорных прерываний)
- каналы (pipes)
- именованные
- неименованные (перенаправление потоков ввода-вывода через | )
- очереди сообщений (mq_open, mq_send, ...)
- механизм разделямой памяти, shared memory (самый быстрый способ - диапазон адресов отображается на адресное пространство двух процессов, когда процесс изменяет память в этом участке - он одновременно изменяет память второго процесса)
- сокеты
- POSIX-семафоры
- RPC (Remote Procedure Call) - man rpc
- IPC (SystemV)
Каналы
Первоначально каналы были только неименованными. Передать дескриптор канала можно было только процессу-потомку. Для передачи данных через канал требуется в одном процессе получить дескриптор на чтение, в другом - на запись. Канал является односторонним, для двустороннего обмена данными требуется создавать два канала.
Для создания неименованного канала используется функция pipe, pipefd[0] будет дескриптором для чтения, pipefd[1] - для записи (пример рабочей программы есть в man-справке):
#include <unistd.h>
int pipe(int pipefd[2]);
Чтобы читать вывод команд, которые обычно используются в консоли, или передавать им данные на вход, используется функция popen, которая принимает команду для запуска другого приложения, тип канала (на запись или чтение) и возвращает дескриптор канала:
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
Именованные каналы обладают именем в пространстве файловых имен. Для создания используется функция mkfifo:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
Сигналы
Каждый сигнал обладает номером и сигнатурой, которые определены в signal.h. Сигналы работают по принципу прерываний. Когда приложению поступает сигнал - оно обязано его сразу обработать. В каждом процессе есть обработчики сигналов. Стандартные обработчики добавляются в программу в процессе линковки. Часть сигналов может быть обработана собственными обработчиками, часть переопределить нельзя.
- SIGINT (2)
- SIGABRT (6) (man abort)
- SIGTERM (15) (необязательная к исполнению, CTRL+C)
- SIGKILL (9) (не переопределяется, прибивает процесс)
- SIGCHILD (17) (при завершении потомка, см. waitpid/wait)
Сигналы используются для управления демонами. Для работы с сигналами используются две функции: kill отправляет процессу сигнал, signal определяет, как его обработать.
Из командной строки утилита kill отправяет сигнал процессу. kill -l выводит список сигналов. Среди них есть пользовательские SIGUSR1 и SIGUSR2, которые зарезервированы под использование пользователем и под которые можно писать свои обработчики.
Отправка сигнала с помощью функции kill:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
Установка обработчика сигнала функцией signal, которая принимает в качестве параметра указатель на функцию для обработки сигнала и возвращает указатель на предыдущий обработчик:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
Пример обработки сигнала SIGINT:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void mysignal_handler(int signalno)
{
printf("Called from signal %d\n", signalno);
}
int main()
{
signal(SIGINT, mysignal_handler);
int counter = 0;
while(1)
{
printf("Hello %d\n", counter++);
usleep(500000);
}
return 0;
}
Разделяемая память
Каждый процесс при создании получает одинаковое адресное пространство - диапазон адресов от 0 до ffffffff на 32-битной системе. В ОС есть механизм виртуальной памяти, который отображает память процесса частично на физическую память, частично на диск. Распределение адресного пространства процесса (memory layout) устроено так: "Сверху" в сторону уменьшения адресов располагается ядро, под которое зарезервирован 1 Гб адресного пространства. Снизу от младших адресов к старшим располагается процесс: сегмент кода, потом сегмент данных, диапазон адресов для кучи. От ядра в сторону уменьшения адресов растёт стек. Между кучей и стеком находится сегмент разделямой памяти, используемый для IPC.
Ядро ~1 Gb |
Стек |
Разделяемая память |
Куча |
Данные |
Код |
Для работы с разделяемой памятью служат функции:
- shmget позволяет получать или создавать регион памяти
- shmat позволяет подключаться к региону памяти
- shmdt отключает от региона памяти
- shmctl управляет параметрами регионов разделяемой памяти
Создание региона памяти с помощью shmget: в качестве параметров принимаются глобальный ключ key для доступа к региону, size округляется вверх до размера страницы памяти.
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
Для получения информации о созданных регионах shared-памяти используется утилита ipcs.