programming

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

  • 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 - код выполняемой программы (код команды из одной из групп ниже)
    • Группа команд чтения - главный процесс может прочесть информацию по адресу в адресном пространстве подчинённого процесса. (из сегмента кода/сегмента данных/контекста процесса) (доступны только при [[|приостановке]] сыновьего процесса)
    • Группа команд записи - главный процесс может записать/изменить информацию по адресу в адресном пространстве подчинённого процесса. (также доступны только при приостановке сыновьего процесса)
    • Группа команд управления
      1. Главный процесс может продолжить выполнение с точки останова. Может запустить приостановленный подчинённый процесс с заданного адреса. Включить “шаговый режим” (процесс приостановится по приходе сигнала SIG_TRAP),
      2. Сыновий процесс может использовать одну команду - 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

Установка контрольной точки

Статус отлаживаемого процесса (ОП) - выполнение

  1. Подчинённому процессу отправляется SIGTRAP.
  2. Ждём остановку ОП + анализируем точку остановки (статус ОП - ожидание).
  3. Чтение в адресном пространстве ОП, сохранение ().
  4. Запись BrPnt в .
  5. Продолжение выполнения с точки останова.

Приход в контрольную точку

Статус ОП - выполнение

  1. Ждём остановки ОП (при остановке ОП переходит в ожидание).
  2. Читаем информацию из контекста, анализируем точку остановки.
  3. Контрольная точка (совпадение адреса остановки + причины остановки).
  4. Действия по отладке ОП в состоянии ожидания

Снятие контрольной точки

Статус ОП - ожидание

  1. Всстановление содержимого
  2. Продолжаем с адреса

“Движение” через контрольную точку

Статус ОП - ожидание

  1. Восстанавливаем содержимое .
  2. Включаем “шаговый” режим.
  3. Продолжаем с адреса
  4. Ждём остановки ОП (анализ точки остановки)
  5. Запись BrPnt в
  6. Продолжаем с точки остановки

Контрольная точка может быть многократной.