Функция pipe() из заголовка <unistd.h> открывает 2 файловых дескриптора, один на чтение, другой на запись.
Можно использовать его для передачи байтов между родительским и дочерним процессом (от функции fork()).
Каждое направление связи должно иметь свой набор дескрипторов от функции pipe() .
Если направление одно - от родителя к дочернему, то и pipe() нужен 1.
Если направлений несколько - от родителя к дочернему и наоборот , то pipe() нужно несколько.
Итого, сколько направлений , столько и функций pipe() нужно.
Программа
pipe.c:
Ссылка
В этой программе реализован пример передачи сообщений в двух направлениях:
- от родительского к дочернему
- от дочернего к родительскому
Сперва определяются массивы из двух значений (0 - чтение, 1 -запись):
int pipe_parent_to_child[2] = { 0 };
int pipe_child_to_parent[2] = { 0 };Затем эти массивы заполняются новыми файловыми дескрипторами для общения, вызовом pipe():
pipe( pipe_parent_to_child );
pipe( pipe_child_to_parent );Делается fork() для разделения процесса:
pid_t pid = fork();Начинаются условные ветвления, в зависимости от pid, чтобы понять в каком процессе находится программа (родитель или дочерний):
if( pid > 0 )
{
/* родительский процесс */
...
}
else
{
/* дочерний процесс */
...
}Для работы с дескрипторами файлов используются стримы для удобства работы.
- Получаем стрим (
FILE*) через передачу дескриптора в функциюfdopen() - Что-то делаем со стримом, пишем в него через
fprintf()или читаем черезfgets(). - Освобождаем стрим через
fflush().
Массив дескрипторов (например, pipe_parent_to_child), заполненный функцией pipe() всегда имеет 2 элемента:
[0]- дескриптор для чтения.[1]- дескриптор для записи.
Пример отправки сообщения от родителя к дочернему:
/* send message to child */
FILE* to_child = fdopen(pipe_parent_to_child[1], "w");
fprintf(to_child, "<message from PARENT>\n");
fflush(to_child);Пример получения сообщения дочерним процессом от родителя:
/* read message from parent */
FILE* from_parent = fdopen(pipe_parent_to_child[0], "r");
fgets(parent_message, MESSAGE_MAX_BYTES, from_parent);
printf("message to child -> %s\n", parent_message);
fflush(from_parent);Пайпы (pipes) хороши своей производительностью, но не без греха:
- Работают только на связанных процессах (через
fork()) . - Только передача байтов.
- Раздутый код, на каждое направление передачи требуется свой
pipe()и набор данных для работы с ним. - Неудобно диагностировать ошибки из-за направленностей и раздутого кода.
FIFO еще называют named pipes . FIFO создается функцией mkfifo() , которая создает обычный файл.
Необходимые заголовки:
<sys/types.h> <sys/stat.h>
Сигнатура функции
mkfifo():int mkfifo(const char *pathname, mode_t mode);
Этот файл является точкой обмена сообщениями между процессами. Что намного удобнее, чем использовать pipe() .
Программа
fifo_sender.c:
fifo_receiver.c:
Одна программа отправитель - fifo_sender.c , другая получатель - fifo_receiver.c .
Рассмотрим fifo_sender.c :
- Создается файл
/tmp/fifo_example22функциейmkfifo()
int mkfifo_result = mkfifo(fifo_filename, 0644);
- Получаем дескриптор этого файла только для чтения:
fifo_fd = open(fifo_filename, O_WRONLY);
- Пишем в файл сообщение каждую секунду (
sleep(1)) :
while(1) { dprintf(fifo_fd, "%s\n", argv[1]); fprintf(stdout, "message send.\n"); sleep(1); }
- В обработчике сигналов (когда программа прерывается) делаем высвобождение:
/* закрываем дескриптор файла */ int close_res = close(fifo_fd); /* удаляем сам файл */ int unlink_res = unlink(fifo_filename);
Как только кто-то с этого файла начинает читать, появляется запись каждую секунду, которую получатель (тот кто читает файл) принимает себе.
Рассмотрим fifo_receiver.c :
Максимально простая программа, которая читает файл /tmp/fifo_example22 через стрим FILE* , и выводит сообщения из файла в терминал .
/* открываем стрим файла для чтения "r" */
FILE* fifo_file = fopen("/tmp/fifo_example22", "r");/* посимвольно выводит содержимое файла */
unsigned char c = { 0 };
while( ( c = getc(fifo_file) ) != EOF )
{
putchar(c);
}Компилируем программы и запускаем в таком порядке:
- Компиляция и запуск sender'a
gcc fifo_sender.c -o fifo_sender.out ./fifo_sender.out
Запуск получателя нужно делать в отдельном терминале , это две разные программы, которые должны быть запущены по отдельности!
- Компиляция и запуск receiver'а
gcc fifo_receiver.c -o fifo_receiver.out ./fifo_receiver.out
После запуска будет примерно такой результат:
Отправитель
./fifo_sender.out kika
message send.
message send.
message send.
message send.
message send.Получатель
./fifo_receiver.out
kika
kika
kika
kika
kikaКак только прерывается программа получателя fifo_receiver , автоматически (из-за сигналов) закроется программа fifo_sender.
Итого получился относительно удобный процесс передачи сообщений от одного процесса другому.
В отличии от pipe() здесь нет форков, оба процессы независимы, и общаются через один временный файл .
Очередь сообщений из заголовка <mqueue.h> , это более высокоуровневый и удобный инструмент для общения между процессами.
Общение идет через один mq файл. Записанные сообщения сохраняются пока не будут прочитаны. То есть обмен сообщениями идет в ленивом режиме, только по требованию получателя, и независимо от работающего процесса получателя.
mq-файлэто не самый обычный файл, поэтому все операции над его дескриптором идут не через стандартный ввод вывод, а функциями с префиксомmq_, например -mq_close()
Еще из отличий от FIFO - это обязанность получателя закрывать поток данных, а не отправителя.
Программа
mq_sender.c:
mq_receiver.c:
Рассмотрим отправителя mq_sender.c :
- Создание структуры-представления файла для очереди сообщений
Структура типаmq_attrявляется представлением для настройки обменом сообщений.struct mq_attr msg_attr = { 0 }; /* ограничение на кол-во сообщений */ msg_attr.mq_maxmsg = 10; /* максимальный размер сообщения в байтах */ msg_attr.mq_msgsize = 2048;
- Создание файла для обмена сообщениями
Нужно создать специальный файл через функциюmq_open(), который
будет в себе хранить настройки изmsg_attrи самую очередь сообщений.
Функцияmq_open()возвращает дескриптор файла.int mq_d = mq_open("/my_queue", O_CREAT | O_RDWR, 0644, &msg_attr);
- Запись сообщения в очередь
Функциейmq_send()через дескриптор файла, передается сообщение из аргумента программы./* mq_send ( дескриптор файла, сообщение, длина сообщения, приоритет) */ int mq_send_res = mq_send( mq_d, argv[1], strlen(argv[1]), 1 );
- Закрытие дескриптора файла.
Так как был открыт дескрипторmq-файла, требуется его освободить.mq_close( mq_d );
- Компиляция программы
gcc -Wall -Wextra -pedantic mq_sender.c -o mq_sender.out
В старых версиях Linux (до 2022, glibc ≥ 2.34) нужно явно указывать библиотеку
-lrt(rt - realtime)
- Отправка сообщений
./mq_sender.out "msg #0" ./mq_sender.out "msg #1" ./mq_sender.out "msg #2"
Рассмотрим получателя mq_receiver.c :
- Открытие
mq-файлаmqd_t mq_d = mq_open("/my_queue", O_RDONLY);
- Получение атрибутов из
mq-файлаstruct mq_attr msg_attr = { 0 }; int mq_get_attr_res = mq_getattr( mq_d, &msg_attr );
- Получение количества сообщений
long queue_count = msg_attr.mq_curmsgs;
- Вывод сообщений по очереди
for( int i = 0; i < queue_count; i++ ) { ssize_t receive_res = mq_receive(mq_d, buffer, msg_size, NULL); /* .... */ printf("%s\n", buffer ); memset( buffer, '\0', msg_size); }
- Очистка ресурсов, после использования
/* очистк буфера сообщения */ free(buffer); /* закрытие дескриптора */ mq_close(mq_d); /* удаление mq-файла */ mq_unlink("/my_queue");
- Компиляция программы
gcc -Wall -Wextra -pedantic mq_receiver.c -o mq_receiver.out
- Чтение сообщений
./mq_receiver.out current messages count in queue: 3 msg #0 msg #1 msg #2
Коммуникация между процессами, с помощью unix domain socket'ов (UDS) очень проста и удобна. UDS - это аналог TCP/IP сокетов, которые используются для обмена сообщениями через сеть.
UDS отличается от tcp/ip сокетов тем, что работает только локально (внутри одного хоста), и представлен специальным сокет-файлом (socket file) , то есть имеется обычный путь к файлу UDS-сокета в файловой системе, тогда как в tcp/ip сокетах адресом является адрес интернет-протокола.
Это самый распространенный способ создания коммуникации между процессами на одной машине. Однако UDS работает только для UNIX-подобных систем, если требуется кроссплатформенность, то стоит рассмотреть для межпроцессной связи - tcp/ip сокеты .
Программа
uds_server.c:
uds_client.c:
Рассмотрим сервер uds_server.c :
- Глобальное объявление имени файла для сокета
const char* sock_name = "/tmp/my_socket";
- Глобальное объявление дескрипторов - сокета и файла для данных
int sock_fd = -1; int data_fd = -1;
- Определение функций - обработчик сигнала и освобождение ресурсов
void on_signal(int signum); void dispose();
main()
- Инициализация обработчиков сигналов, с которыми будет вызвана функция
on_signal(int)sig_action.sa_handler = on_signal; sigfillset(&sig_action.sa_mask); sig_action.sa_flags = SA_RESTART; sigaction(SIGTERM, &sig_action, NULL); sigaction(SIGINT, &sig_action, NULL); sigaction(SIGQUIT, &sig_action, NULL); sigaction(SIGABRT, &sig_action, NULL); sigaction(SIGPIPE, &sig_action, NULL);
- Открытие uds-сокета
sock_fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
- Параметры адреса для сокета
struct sockaddr_un sock_addr = { 0 }; sock_addr.sun_family = AF_UNIX; strcpy(sock_addr.sun_path, sock_name);
- Установка связи с сокетом функцией
bind()int bind_res = bind( sock_fd, (const struct sockaddr*) &sock_addr, sizeof(struct sockaddr_un) );
- Установка прослушивания сокета функцией
listen()/* где 20 - максимальное количество клиентов. */ int listen_res = listen(sock_fd, 20);
- Установка подключения с клиентом функцией
accept()
В результате вернется новый дескриптор для обмена данными.data_fd = accept(sock_fd, NULL, NULL);
- Объявление главного цикла Main Loop
while(1) { /* Внутренний цикл чтения данных от клиента */ /* .... */ } /* в конце главного цикла отправляем сообщение клиенту, что их запрос обработан */ char* back_msg = "message received"; write(data_fd, back_msg, strlen(back_msg));
- Объявление цикла для чтения данных клиента
while(1) { read_res = read(data_fd, buffer, max_len ); if( read_res == -1 ) { perror("read() failed. "); dispose(); } else if( read_res == 0 ) { printf("> > client disconnected.\n"); dispose(); } else { printf("> > > message from client:\n\t%s\n", buffer); break; } }
- Реализация функций-обработчиков
void on_signal(int signum) { dispose(); } void dispose() { close(sock_fd); close(data_fd); unlink(sock_name); exit(0); }
Далее запускаем, и ждем сообщений клиента:
./uds_server.outРассмотрим клиента uds_client.c :
- Клиент создает свой сокет
int sock_fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
- Подключение к сокету, через общее имя файла
"/tmp/my_socket"/* ... */ /* имя этого файла - точка связи, у сервера такая-же. */ const char* sock_name = "/tmp/my_socket"; struct sockaddr_un sock_addr = { 0 }; sock_addr.sun_family = AF_UNIX; strcpy(sock_addr.sun_path, sock_name);
int conn_res = connect( sock_fd, (const struct sockaddr*) &sock_addr, sizeof(struct sockaddr_un) );
- Основной цикл
while(1) { printf("enter message: "); /* чтение сообщения от пользователя */ fgets( send_buffer, sizeof(send_buffer), stdin ); send_buffer[strcspn(send_buffer, "\n")] = '\0'; /* отправка сообщения серверу */ int write_res = write(sock_fd, send_buffer, strlen(send_buffer) + 1); /* получение ответа от сервера */ int read_res = read(sock_fd, recv_buffer, max_len); printf("%s\n", recv_buffer); }
Далее запускаем (в отдельном терминале, сервер должен быть запущен!), и пишем сообщения:
./uds_client.out
enter message: goga
message received
enter message: boka
message received
enter message:
...Смотрим что там с сервером:
...
> > client connected.
> > > message from client:
goga
> > > message from client:
bokaВсе работает!
Итоги:
- Клиент и сервер могут общаться друг с другом в двух направлениях, в отличии от
fifoиpipe - Коммуникация между сервером и клиентом происходит через единый файл данных сокета, кто-то туда пишет, кто-то от туда читает.
- Сокеты работают через подключения, а не через жизненный цикл приложения.
- Сокеты позволяют процессам быть проще и более независимыми, в отличии от других способов IPC (межпроцессной коммуникации).
- Сокеты (uds или tcp/ip) - это самый распространенный способ IPC.