Билеты (Graur) (1114773), страница 16
Текст из файла (страница 16)
dup2(fd[1], 1); /* отождествили стандартный вывод с файловым дескриптором канала, предназначенным для записи */
close(fd[1]); /* закрыли файловый дескриптор канала, предназначенный для записи */
close(fd[0]); /* закрыли файловый дескриптор канала, предназначенный для чтения */
exelp(“print”, ”print”, 0); /* запустили программу print */
}
/*процесс-потомок*/
dup2(fd[0], 0); /* отождествили стандартный ввод с файловым дескриптором канала, предназначенным для чтения*/
close(fd[0]); /* закрыли файловый дескриптор канала, предназначенный для чтения */
close(fd[1]); /* закрыли файловый дескриптор канала, предназначенный для записи */
execl(“/usr/bin/wc”, ”wc”, 0); /* запустили программу wc */
}
-
Совместное использование сигналов и каналов – «пинг-понг».
Пример программы с использованием каналов и сигналов для осуществления связи между процессами – весьма типичной ситуации в системе. При этом на канал возлагается роль среды двусторонней передачи информации, а на сигналы – роль системы синхронизации при передаче информации. Процессы посылают друг другу целое число, всякий раз увеличивая его на 1. Когда число достигнет некоего максимума, оба процесса завершаются.
#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;
int fd[2];
int status;
void SigHndlr(int s)
{
/* в обработчике сигнала происходит и чтение, и запись */
signal(SIGUSR1, SigHndlr);
if (cnt < MAX_CNT)
{
read(fd[0], &cnt, sizeof(int));
printf("%d \n", cnt);
cnt++;
write(fd[1], &cnt, sizeof(int));
/* посылаем сигнал второму: пора читать из канала */
kill(target_pid, SIGUSR1);
}
else
if (target_pid == getppid())
{
/* условие окончания игры проверяется потомком */
printf("Child is going to be terminated\n");
close(fd[1]); close(fd[0]);
/* завершается потомок */
exit(0);
} else
kill(target_pid, SIGUSR1);
}
int main(int argc, char **argv)
{
pipe(fd); /* организован канал */
signal (SIGUSR1, SigHndlr);
/* установлен обработчик сигнала для обоих процессов */
cnt = 0;
if (target_pid = fork())
{
/* Предку остается только ждать завершения потомка */
while(wait(&status) == -1);
printf("Parent is going to be terminated\n");
close(fd[1]); close(fd[0]);
return 0;
}
else
{
/* процесс-потомок узнает PID родителя */
target_pid = getppid();
/* потомок начинает пинг-понг */
write(fd[1], &cnt, sizeof(int));
kill(target_pid, SIGUSR1);
for(;;); /* бесконечный цикл */
}
}
Билет 38
Именованные каналы (FIFO)
Рассмотренные выше программные каналы имеют важное ограничение: так как доступ к ним возможен только посредством дескрипторов, возвращаемых при порождении канала, необходимым условием взаимодействия процессов через канал является передача этих дескрипторов по наследству при порождении процесса. Именованные каналы (FIFO-файлы) расширяют свою область применения за счет того, что подключиться к ним может любой процесс в любое время, в том числе и после создания канала. Это возможно благодаря наличию у них имен.
FIFO-файл представляет собой отдельный тип файла в файловой системе UNIX, который обладает всеми атрибутами файла, такими как имя владельца, права доступа и размер. Для его создания в UNIX System V.3 и ранее используется системный вызов mknod(), а в BSD UNIX и System V.4 – вызов mkfifo() (этот вызов поддерживается и стандартом POSIX):
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int mknod (char *pathname, mode_t mode, dev);
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo (char *pathname, mode_t mode);
В обоих вызовах первый аргумент представляет собой имя создаваемого канала, во втором указываются права доступа к нему для владельца, группы и прочих пользователей, и кроме того, устанавливается флаг, указывающий на то, что создаваемый объект является именно FIFO-файлом (в разных версиях ОС он может иметь разное символьное обозначение – S_IFIFO или I_FIFO). Третий аргумент вызова mknod() игнорируется.
После создания именованного канала любой процесс может установит с ним связь посредством системного вызова open(). При этом действуют следующие правила:
-
если процесс открывает FIFO-файл для чтения, он блокируется до тех пор, пока какой-либо процесс не откроет тот же канал на запись
-
если процесс открывает FIFO-файл на запись, он будет заблокирован до тех пор, пока какой-либо процесс не откроет тот же канал на чтение
-
процесс может избежать такого блокирования, указав в вызове open() специальный флаг (в разных версиях ОС он может иметь разное символьное обозначение – O_NONBLOCK или O_NDELAY). В этом случае в ситуациях, описанных выше, вызов open() сразу же вернет управление процессу
Правила работы с именованными каналами, в частности, особенности операций чтения-записи, полностью аналогичны неименованным каналам.
Ниже рассматривается пример, где один из процессов является сервером, предоставляющим некоторую услугу, другой же процесс, который хочет воспользоваться этой услугой, является клиентом. Клиент посылает серверу запросы на предоставление услуги, а сервер отвечает на эти запросы.
-
Модель «клиент-сервер».
Процесс-сервер запускается на выполнение первым, создает именованный канал, открывает его на чтение в неблокирующем режиме и входит в цикл, пытаясь прочесть что-либо. Затем запускается процесс-клиент, подключается к каналу с известным ему именем и записывает в него свой идентификатор. Сервер выходит из цикла, прочитав идентификатор клиента, и печатает его.
/* процесс-сервер*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int fd;
int pid;
mkfifo("fifo", S_IFIFO | 0666);
/*создали специальный файл FIFO с открытыми для всех правами доступа на чтение и запись*/
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");/*уничтожили именованный канал*/
return 0;
}
/* процесс-клиент*/
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc, char **argv)
{
int fd;
int pid = getpid( );
fd = open("fifo", O_RDWR);
write(fd, &pid, sizeof(int));
close(fd);
return 0;
}
Билет 39 Трассировка процессов.
Трассировка процессов.
Обзор форм межпроцессного взаимодействия в UNIX был бы не полон, если бы мы не рассмотрели простейшую форму взаимодействия, используемую для отладки — трассировку процессов. Принципиальное отличие трассировки от остальных видов межпроцессного взаимодействия в том, что она реализует модель «главный-подчиненный»: один процесс получает возможность управлять ходом выполнения, а также данными и кодом другого.
В UNIX трассировка возможна только между родственными процессами: процесс-родитель может вести трассировку только непосредственно порожденных им потомков, при этом трассировка начинается только после того, как процесс-потомок дает разрешение на это.
Далее схема взаимодействия процессов путем трассировки такова: выполнение отлаживаемого процесса-потомка приостанавливается всякий раз при получении им какого-либо сигнала, а также при выполнении вызова exec(). Если в это время отлаживающий процесс осуществляет системный вызов wait(), этот вызов немедленно возвращает управление. В то время, как трассируемый процесс находится в приостановленном состоянии, процесс-отладчик имеет возможность анализировать и изменять данные в адресном пространстве отлаживаемого процесса и в пользовательской составляющей его контекста. Далее, процесс-отладчик возобновляет выполнение трассируемого процесса до следующей приостановки (либо, при пошаговом выполнении, для выполнения одной инструкции).
Основной системный вызов, используемый при трассировке,– это ptrace(), прототип которого выглядит следующим образом:
#include <sys/ptrace.h>
int ptrace(int cmd, pid, addr, data);
где cmd – код выполняемой команды, pid – идентификатор процесса-потомка, addr – некоторый адрес в адресном пространстве процесса-потомка, data – слово информации.
Чтобы оценить уровень предоставляемых возможностей, рассмотрим основные коды - cmd операций этой функции.
cmd = PTRACE_TRACEME — ptrace() с таким кодом операции сыновний процесс вызывает в самом начале своей работы, позволяя тем самым трассировать себя. Все остальные обращения к вызову ptrace() осуществляет процесс-отладчик.
cmd = PTRACE_PEEKDATA — чтение слова из адресного пространства отлаживаемого процесса по адресу addr, ptrace() возвращает значение этого слова.
cmd = PTRACE_PEEKUSER — чтение слова из контекста процесса. Речь идет о доступе к пользовательской составляющей контекста данного процесса, сгруппированной в некоторую структуру, описанную в заголовочном файле <sys/user.h>. В этом случае параметр addr указывает смещение относительно начала этой структуры. В этой структуре размещена такая информация, как регистры, текущее состояние процесса, счетчик адреса и так далее. ptrace() возвращает значение считанного слова.
cmd = PTRACE_POKEDATA — запись данных, размещенных в параметре data, по адресу addr в адресном пространстве процесса-потомка.
cmd = PTRACE_POKEUSER — запись слова из data в контекст трассируемого процесса со смещением addr. Таким образом можно, например, изменить счетчик адреса трассируемого процесса, и при последующем возобновлении трассируемого процесса его выполнение начнется с инструкции, находящейся по заданному адресу.
cmd = PTRACE_GETREGS, PTRACE_GETFREGS — чтение регистров общего назначения (в т.ч. с плавающей точкой) трассируемого процесса и запись их значения по адресу data.
cmd = PTRACE_SETREGS, PTRACE_SETFREGS — запись в регистры общего назначения (в т.ч. с плавающей точкой) трассируемого процесса данных, расположенных по адресу data в трассирующем процессе.
cmd = PTRACE_CONT — возобновление выполнения трассируемого процесса. Отлаживаемый процесс будет выполняться до тех пор, пока не получит какой-либо сигнал, либо пока не завершится.
cmd = PTRACE_SYSCALL, PTRACE_SINGLESTEP — эта команда, аналогично PTRACE_CONT, возобновляет выполнение трассируемой программы, но при этом произойдет ее остановка после того, как выполнится одна инструкция. Таким образом, используя PTRACE_SINGLESTEP, можно организовать пошаговую отладку. С помощью команды PTRACE_SYSCALL возобновляется выполнение трассируемой программы вплоть до ближайшего входа или выхода из системного вызова. Идея использования PTRACE_SYSCALL в том, чтобы иметь возможность контролировать значения аргументов, переданных в системный вызов трассируемым процессом, и возвращаемое значение, переданное ему из системного вызова.
cmd = PTRACE_KILL — завершение выполнения трассируемого процесса.
Общая схема использования механизма трассировки.
Рассмотрим некоторый модельный пример, демонстрирующий общую схему построения отладочной программы (см. также Рис. 9):
...
if ((pid = fork()) == 0)
{
ptrace(PTRACE_TRACEME, 0, 0, 0);
/* сыновний процесс разрешает трассировать себя */
exec(“трассируемый процесс”, 0);