Взаимодействие

- MPI (Message Passing Interface) - библиотека (send/recieve) - базовый инструмент программирования распределённых вычислительных систем.
- Произвольные процессы реализуются при помощи PID, именованных каналов (fifo-files, упорядоченные файлы без seek).
- Также есть реализация, при которой ресурсы можно передавать через наследование (в такой реализации нет необходимости в явном указании id процесса).
- Приоритетность:
- Однородная/неоднородная
- Деление на главный/подчинённый (главный может с подчинённым процессом сделать что угодно, вплоть до изменения значения конкретного регистра.)
Сигналы
Сигнал - средство асинхронного уведомления процесса о наступлении некоторого события в системе.
-
Примеры сигналов <signal.h>:
- SIGINT (2)
- SIGQUIT (3)
- SIGKILL (9)
- SIGALARM (14)
- SIGCHLD (18)
- …
- SIGUSR1
-
Причины инициализации сигнала:
- Инициатор передачи сигнала - операционная система.
- При делении на ноль система выполняет прерывание и отправляет сигнал процессу.
- Инициатор - любой процесс (если есть право на отправку)
- Инициатор - процесс обращается к системе для отправки сигнала себе же.
- Инициатор передачи сигнала - операционная система.
-
Реакция на сигнал:
- Стандартная реакция - завершение процесса с системным кодом завершения, равным номеру сигнала.
- Игнорировать сигнал
- SIGINT (ctrl+c) - можно игнорировать (мягкое завершение процесса)
- SIGKILL - нельзя игнорировать (жёсткое завершение процесса)
- Вызов функции обработчика
Работа с сигналами
#include <sys/types.h>
#include <signal.h>
Отправка сигнала
int kill(pid_t pid, int sig);pid- идентификатор процесса, которому посылается сигналsig- номер посылаемого сигнала- Возвращает
0при удачном выполнении. Иначе --1
Нельзя передать сигнал процессам с номерами
0и1
Обработка сигнала
void(*signal (int sig, void (*disp) (int))) (int)sig- номер сигнала, для которого устанавливается реакцияdisp- определённая пользователем функция-обработчик сигнала, либо одна из констант:SIG_DFL- обработка по умолчаниюSIG_IGN- игнорирование
- Возвращает указатель на предыдущий обработчик данного сигнала, в случае успешного выполнения. Иначе -
-1иerrno.
Таким образом мы самостоятельно обрабатываем конкретный сигнал.
Пример использования
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
int count = 0;
void SigHndlr(int s) {
printf("\n I got SIGINT %d time(s) \n", count++);
if (count == 5) {
signal(SIGINT, SIG_DFL);
}
}
int main(int argc, char **argv) {
signal(SIGINT, SigHndlr);
while (1) {
...
}
return 0;
}5 раз ловим
SIGINTи выводим текст. На 6й раз вызываем стандартную обработку.
Пример. Программа-будильник
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void alrm(int s) {
printf("\nWaiting for name\n");
alarm(5);
}
int main(int argc, char **argv) {
char s[80];
signal(SIGALRM, alrm);
alarm(5);
printf("Write the name\n");
for (;;) {
printf("name: ");
if (scanf("%s", &s)) break;
}
printf("OK\n");
return 0;
}
alarm(5)задаёт таймер, по истечении которого отправляетсяSIGALRM, переходим к for. Обрабатываем сигнал функциейalrm()до тех пор, пока не выйдем из цикла и не придём к return.
forработает до тех пор, пока пользователь не введёт имя.
Двухпроцессный вариант программы-будильника
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
void alr(int s) {
printf("\nfaster.\n");
}
int main(int argc, char **argv) {
char s[80];
int pid;
signal(SIGALRM, alr);
if (pid = fork()) { // parent
for (;;) {
sleep(5);
kill(pid, SIGALRM);
}
} else { // child
printf("Write the name\n");
for (;;) {
printf("name: ");
if (scanf("%s", &s)) break;
}
printf("OK\n");
kill(getpid(), SIGKILL);
return 0;
}
}Объявляем обработку
SIGALRMчерезalr. Запускаем сыновий процесс, который ждёт ввода имени. Раз в 5 секунд из родительского процесса отправляем сыновьемуSIGALRM.
Pipes - Неименованные каналы
UNIX система на своих ресурсах может создавать неименованные каналы (буферы определённого размера в оперативной памяти. С этими файлами ассоциируются два файловых дескриптора. Один отвечает за запись, другой - за чтение. Стратегия доступа - fifo)
Неименованные каналы используются в родственных процессах.
Pipe - однонаправленный информационный канал, который можно использовать для межпроцессных коммуникаций.
#include <unistd.h>
-
int pipe(int pipefd[2]);pipefd[2]- массив, возвращающий два файловых дескриптора, относящихся к двум концам трубыpipefd[0]- читающий конец каналаpipefd[1]- записывающий конец канала
-
Данные, записанные через записывающий конец канала, хранятся ядром до тех пор, пока они не будут считаны с читающего конца канала (трубы).
-
Читающий процесс будет заблокирован, пока не закроются пишущие дескрипторы.
-
Если запрашивается больше информации, чем есть в канале, возвращается то, что доступно.
-
Если процесс пытается записать в канал порцию данных, превышающую свободное место в канале, порция записывается, а пишущий процесс блокируется до тех пор, пока из канала не заберут данные. Цикл до тех пор, пока не будет записана вся порция.
-
Если процесс пытается записать в канал данные, а к этому моменту в системе нет ни одного процесса с открытым дескриптором канала - процесс получит от системы сигнал
SIGPIPE -
При попытке чтения пустого канала процесс блокируется, пока не появится информация.
-
Возвращает
EOF, если все дескрипторы чтения закрыты. -
Канал может не создаться по причине:
- Закончилась таблица каналов внутри процесса
- Закончилось место в оперативной памяти
Пример использования pipes
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
char *s = "channel";
char buf[80];
int pipes[2];
pipe(pipes);
write(pipes[1], s, strlen(s) + 1);
read(pipes[0], buf, strlen(s) + 1);
close(pipes[0]);
close(pipes[1]);
printf("%s\n", buf);
}Создаём строку, создаём два канала pipes, записываем строку в pipe, пишем информацию из pipe в буфер и выводим.
Pipes для нескольких процессов
Каналы полезны для передачи информации между процессами.
Например: Родительский процесс обменивается с сыновьим.
int main(int argc, char **argv) {
int fd[2];
pipe(fd);
if(fork()) { // parent
close(fd[0]);
write(fd[1], ...) // write side
} else { // child
close(fd[1]);
while (read(fd[0], ...)) { // read side
...
}
...
}
}Создаём pipes, создаём сыновий процесс, оставляем только пишущий/читающий дескриптор в процессах. Тем самым создаём симплексный канал.
closeпринципиален. Если не использовать, читатель будет ждать вечно.
Реализация конвейера при помощи pipes
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv) {
int fd[2];
if (pipe(fd) == -1) return 1;
if (fork() > 0) { // parent process
if (dup2(fd[1], 1) == -1) return 1; //change fd
close(fd[1]);
close(fd[0]);
execlp("ls", "ls", NULL);
}
// child process
if (dup2(fd[0], 0) == -1) return 1; // change fd
close(fd[0]);
close(fd[1]);
execl("/usr/bin/wc", "wc", NULL);
return 0;
} Закрываем
fd[1]иfd[0], в родителе пишем выводlsв неименованный канал. Далее в сыновьем процессе заменяем канал ввода на выход родительского.
wcможет запуститься раньше, чемls, но из-за пустого ввода он будет ждать выполненияls
Добавил обработку ошибок для pipe() и dup2().
Совместное исползование сигналов и каналов - “Ping-pong”
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#define MAX_CNT 100
int target_pid, cnt; // cnt is a ball
int fd[2]; // unnamed pipe
int status;
// takes SIGUSR1
void SigHndlr(int s) {
if (cnt < MAX_CNT) {
read(fd[0], &cnt, sizeof(int)); // read counter
printf("%d\n", cnt);
cnt++;
write(fd[1], &cnt, sizeof(int));
kill(target_pid, SIGUSR1);
} else {
if (target_pid == getppid()) { // child process
printf("Child is going to be terminated.\n");
close(fd[1]);
close(fd[0]);
exit(0);
} else { // parent process
kill(target_pid, SIGUSR1);
}
}
}
int main(int argc, char **argv) {
if (pipe(fd) == -1) return -1;
signal(SIGUSR1, SigHndlr);
cnt = 0;
if (target_pid = fork()) { // parent process
write(fd[1], &cnt, sizeof(int));
while (wait(&status) == -1);
printf("Parent is going to terminated.\n");
close(fd[1]);
close(fd[0]);
return 0;
} else { // child process
read(fd[0], &cnt, sizeof(int)); // sync start
target_pid = getppid();
write(fd[1], &cnt, sizeof(int));
kill(target_pid, SIGUSR1);
for (;;)
}
}Обработка сигнала:
- Читаем
cntиз канала, выводим, прибавляем единицу, возвращаем в канал, завершаем сыновий процесс.- Если
cnt >= 100, закрываем канал и завершаем сыновий/завершаем родительский.Main():
- Создаём канал, меняем обработку сиганла,
cnt = 0.- Родитель:
cntв канал, ждём завершения сыновьего процесса, закрываем канал, завершаемся.- Сыновий: берём из канала
cnt, …, завершаем отправляем сигнал родителю, ждём.
Если при системном вызове в процесс приходит сигнал, процесс завершается с кодом
-1.
В
statusвозвращается статус завершения сыновьего процесса изwait().
Fifo - Именованные каналы.
В отличие от pipes позволяет взаимодействовать произвольным процессам.
-
int mkfifo(char *pathname, mode_t mode)pathname- имя создаваемого каналаmode- права доступа + режимы открытия- Блокировка при подключении (до тех пор, пока не будет подключения с обеих сторон “трубы”)
O_RDONLY- только чтение.O_RDWR- чтение + запись.O_NONBLOCK- открытие без блокирования.
-
Файлы fifo не допускают перемещения курсора.
-
Процессы, которые хотят взаимодействовать с fifo, могут открыть один файл и обмениваться.
-
Если открыть fifo на запись с
O_NONBLOCKбез читателей, вернётсяENXIO(устройство не существует).
Пример - Fifo - Клиент-Сервер
В билетах этого нет, но мб интересно будет.
Server:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/file.h>
int main(int argc, char **argv) {
int fd;
int pid;
mkfifo("fifo", FILE_MODE, 0666);
fd = open("fifo", O_RDONLY|O_NONBLOCK);
while (read(fd, &pid, sizeof(int)) != -1) {
printf("Server %d got message from %d !\n", getpid(), pid);
}
close(fd);
unlink("fifo");
}Client:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/file.h>
int main(int argc, char **argv) {
int fd;
int pid = getpid();
fd = open("fifo", O_RDWR);
write(fd, &pid, sizeof(int));
close(fd);
}Ptrace - Взаимодействие “Главный - подчинённый”

Не симметричное взаимодействие. (Два или более процессов взаимодействуют. Один процесс называется подчинённым, остальные - главными)
Подчинённым процессом может быть процесс потомок или любой другой, при наличии соответствующих прав в настройках ptrace_scope. Также трассировать можно уже запущенные процессы, процессы-зомби, многопоточные приложения (с нитями) и самого себя (например, с последующим вызовом exec()).
#include <sys/ptrace.h>
int ptrace(int cmd, int pid, int addr, int data);
или long ptrace(enum __ptrace_request op, pid_t pid, void *addr, void *data) как в man.
cmd- код выполняемой программы (код команды из одной из групп ниже)- Группа команд чтения - главный процесс может прочесть информацию по адресу в адресном пространстве подчинённого процесса. (из сегмента кода/сегмента данных/контекста процесса) (доступны только при [[|приостановке]] сыновьего процесса)
- Группа команд записи - главный процесс может записать/изменить информацию по адресу в адресном пространстве подчинённого процесса. (также доступны только при приостановке сыновьего процесса)
- Группа команд управления
- Главный процесс может продолжить выполнение с точки останова. Может запустить приостановленный подчинённый процесс с заданного адреса. Включить “шаговый режим” (процесс приостановится по приходе сигнала
SIG_TRAP), - Сыновий процесс может использовать одну команду -
PTRACE_TRACEME- разрешение собственной трассировки.
- Главный процесс может продолжить выполнение с точки останова. Может запустить приостановленный подчинённый процесс с заданного адреса. Включить “шаговый режим” (процесс приостановится по приходе сигнала
pid- идентификатор процесса-потомкаaddr- некоторый адрес в адресном пространстве процесса-потомкаdata- данные
В случае трассировки, по приходе сигнала, трассируемый процесс не завершается, а приостанавливается.
В man синопсис такой:
long ptrace(enum __ptrace_request op, pid_t pid, void *addr, void *data)
op- прописаны вman 2 ptrace.PTRACE_ATTACH- команда ядру, устанавливающая отношение трассировки для двух процессов.PTRACE_TRACEME- процесс позволяет трассировать себя.PTRACE_
Общая схема трассировки процессов

- Процесс разрешает себя трассировать -
PTRACE_TRACEME wait()ловитSIGTRAP-> сыновий процесс приостановлен, родитель может решать, что делать далее.
Сценарий использования - работа с контрольными точками
Контрольные точки - точки остановки отладчика
- breakpoint
Установка контрольной точки
Статус отлаживаемого процесса (ОП) - выполнение
- Подчинённому процессу отправляется
SIGTRAP. - Ждём остановку ОП + анализируем точку остановки (статус ОП - ожидание).
- Чтение в адресном пространстве ОП, сохранение ().
- Запись BrPnt в .
- Продолжение выполнения с точки останова.
Приход в контрольную точку
Статус ОП - выполнение
- Ждём остановки ОП (при остановке ОП переходит в ожидание).
- Читаем информацию из контекста, анализируем точку остановки.
- Контрольная точка (совпадение адреса остановки + причины остановки).
- Действия по отладке ОП в состоянии ожидания
Снятие контрольной точки
Статус ОП - ожидание
- Всстановление содержимого
- Продолжаем с адреса
“Движение” через контрольную точку
Статус ОП - ожидание
- Восстанавливаем содержимое .
- Включаем “шаговый” режим.
- Продолжаем с адреса
- Ждём остановки ОП (анализ точки остановки)
- Запись BrPnt в
- Продолжаем с точки остановки
Контрольная точка может быть многократной.
