Околения компьютеров
Скачать 1.67 Mb.
|
Рис. 8 Пример обмена данными между процессами через канал. 5. Схема взаимодействия процессов с использованием канала. #include #include { int fd[2]; pipe(fd); if (fork()) {/*процесс-родитель*/ close(fd[0]); /* закрываем ненужный дескриптор */ write (fd[1], …); … close(fd[1]); … } else {/*процесс-потомок*/ close(fd[1]); /* закрываем ненужный дескриптор */ while(read (fd[0], …)) { … } … } } Аналогичным образом может быть организован обмен через канал между двумя потомками одного порождающего процесса и вообще между любыми родственными процессами, единственным требованием здесь, как уже говорилось, является необходимость создавать канал в порождающем процессе прежде, чем его дескрипторы будут унаследованы порожденными процессами. Как правило, канал используется как однонаправленное средство передачи данных, т.е. только один из двух взаимодействующих процессов осуществляет запись в него, а другой процесс осуществляет чтение, при этом каждый из процессов закрывает не используемый им дескриптор. Это особенно важно для неиспользуемого дескриптора записи в канал, так как именно при закрытии пишущей стороны канала в него помещается символ конца файла. Если, к примеру, в рассмотренном 5 процесс-потомок не закроет свой дескриптор записи в канал, то при последующем чтении из канала, исчерпав все данные из него, он будет заблокирован, так как записывающая сторона канала будет открыта, и следовательно, читающий процесс будет ожидать очередной порции данных. 6. Реализация конвейера. Пример реализации конвейера print|wc – вывод программы print будет подаваться на вход программы wc.Программа print печатает некоторый текст. Программа wc считает количество прочитанных строк, слов и символов. #include #include #include { int fd[2]; pipe(fd); /*организован канал*/ if (fork()) { /*процесс-родитель*/ 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 */ } 7. Совместное использование сигналов и каналов – «пинг-понг». Пример программы с использованием каналов и сигналов для осуществления связи между процессами – весьма типичной ситуации в системе. При этом на канал возлагается роль среды двусторонней передачи информации, а на сигналы – роль системы синхронизации при передаче информации. Процессы посылают друг другу целое число, всякий раз увеличивая его на 1. Когда число достигнет некоего максимума, оба процесса завершаются. #include #include #include #include #include #include #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(;;); /* бесконечный цикл */ } } Билет 31 Именованные каналы (FIFO) Рассмотренные выше программные каналы имеют важное ограничение: так как доступ к ним возможен только посредством дескрипторов, возвращаемых при порождении канала, необходимым условием взаимодействия процессов через канал является передача этих дескрипторов по наследству при порождении процесса. Именованные каналы (FIFO-файлы) расширяют свою область применения за счет того, что подключиться к ним может любой процесс в любое время, в том числе и после создания канала. Это возможно благодаря наличию у них имен. FIFO-файл представляет собой отдельный тип файла в файловой системе UNIX, который обладает всеми атрибутами файла, такими как имя владельца, права доступа и размер. Для его создания в UNIX System V.3 и ранее используется системный вызов mknod(),а в BSD UNIX и System V.4 – вызов mkfifo() (этот вызов поддерживается и стандартом POSIX): #include #include #include #include INT MKNOD (CHAR *PATHNAME, MODE_T MODE, DEV); #include #include INT MKFIFO (CHAR *PATHNAME, MODE_T MODE); В обоих вызовах первый аргумент представляет собой имя создаваемого канала, во втором указываются права доступа к нему для владельца, группы и прочих пользователей, и кроме того, устанавливается флаг, указывающий на то, что создаваемый объект является именно FIFO-файлом (в разных версиях ОС он может иметь разное символьное обозначение – S_IFIFO или I_FIFO). Третий аргумент вызова mknod() игнорируется. После создания именованного канала любой процесс может установит с ним связь посредством системного вызова open(). При этом действуют следующие правила: - если процесс открывает FIFO-файл для чтения, он блокируется до тех пор, пока какой-либо процесс не откроет тот же канал на запись - если процесс открывает FIFO-файл на запись, он будет заблокирован до тех пор, пока какой-либо процесс не откроет тот же канал на чтение - процесс может избежать такого блокирования, указав в вызове open() специальный флаг (в разных версиях ОС он может иметь разное символьное обозначение – O_NONBLOCK или O_NDELAY). В этом случае в ситуациях, описанных выше, вызов open() сразу же вернет управление процессу Правила работы с именованными каналами, в частности, особенности операций чтения-записи, полностью аналогичны неименованным каналам. Ниже рассматривается пример, где один из процессов является сервером, предоставляющим некоторую услугу, другой же процесс, который хочет воспользоваться этой услугой, является клиентом. Клиент посылает серверу запросы на предоставление услуги, а сервер отвечает на эти запросы. 8. Модель «клиент-сервер». Процесс-сервер запускается на выполнение первым, создает именованный канал, открывает его на чтение в неблокирующем режиме и входит в цикл, пытаясь прочесть что-либо. Затем запускается процесс-клиент, подключается к каналу с известным ему именем и записывает в него свой идентификатор. Сервер выходит из цикла, прочитав идентификатор клиента, и печатает его. /* процесс-сервер*/ #include #include #include #include #include { 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 #include #include #include { int fd; int pid = getpid( ); fd = open("fifo", O_RDWR); write(fd, &pid, sizeof(int)); close(fd); return 0; } Билет 32 Трассировка процессов. Трассировка процессов. Обзор форм межпроцессного взаимодействия в UNIX был бы не полон, если бы мы не рассмотрели простейшую форму взаимодействия, используемую для отладки — трассировку процессов. Принципиальное отличие трассировки от остальных видов межпроцессного взаимодействия в том, что она реализует модель «главный- подчиненный»: один процесс получает возможность управлять ходом выполнения, а также данными и кодом другого. В UNIX трассировка возможна только между родственными процессами: процесс- родитель может вести трассировку только непосредственно порожденных им потомков, при этом трассировка начинается только после того, как процесс- потомок дает разрешение на это. Далее схема взаимодействия процессов путем трассировки такова: выполнение отлаживаемого процесса-потомка приостанавливается всякий раз при получении им какого-либо сигнала, а также при выполнении вызова exec(). Если в это время отлаживающий процесс осуществляет системный вызов wait(), этот вызов немедленно возвращает управление. В то время, как трассируемый процесс находится в приостановленном состоянии, процесс-отладчик имеет возможность анализировать и изменять данные в адресном пространстве отлаживаемого процесса и в пользовательской составляющей его контекста. Далее, процесс- отладчик возобновляет выполнение трассируемого процесса до следующей приостановки (либо, при пошаговом выполнении, для выполнения одной инструкции). Основной системный вызов, используемый при трассировке,– это ptrace(), прототип которого выглядит следующим образом: #include addr – некоторый адрес в адресном пространстве процесса-потомка, data – слово информации. Чтобы оценить уровень предоставляемых возможностей, рассмотрим основные коды - cmd операций этой функции. cmd = PTRACE_TRACEME — ptrace() с таким кодом операции сыновний процесс вызывает в самом начале своей работы, позволяя тем самым трассировать себя. Все остальные обращения к вызову ptrace() осуществляет процесс- отладчик. cmd = PTRACE_PEEKDATA — чтение слова из адресного пространства отлаживаемого процесса по адресу addr, ptrace() возвращает значение этого слова. cmd = PTRACE_PEEKUSER — чтение слова из контекста процесса. Речь идет о доступе к пользовательской составляющей контекста данного процесса, сгруппированной в некоторую структуру, описанную в заголовочном файле 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); /* замещается телом процесса, который необходимо трассировать */ } else { /* это процесс, управляющий трассировкой */ wait((int ) 0); /* процесс приостанавливается до тех пор, пока от трассируемого процесса не придет сообщение о том, что он приостановился */ for(;;) { ptrace(PTRACE_SINGLESTEP, pid, 0, 0); /* возобновляем выполнение трассируемой программы */ wait((int ) 0); /* процесс приостанавливается до тех пор, пока от трассируемого процесса не придет сообщение о том, что он приостановился */ … ptrace(cmd, pid, addr, data); /* теперь выполняются любые действия над трассируемым процессом */ … } } Процесс-потомок ptrace(PTRACE_TRACEME, 0, 0, 0); exec(…); Процесс-предок wait(…); for(;;) { … ptrace(PTRACE_SINGLESTEP , …); … wait(…); … } cигнал SIGTRAP cигнал SIGTRAP § Рис. 9 Общая схема трассировки процессов Предназначение процесса-потомка — разрешить трассировку себя. После вызова ptrace(PTRACE_TRACEME, 0, 0, 0) ядро устанавливает для этого процесса бит трассировки. Сразу же после этого можно заместить код процесса-потомка кодом программы, которую необходимоотладить.Отметим, что при выполнении системного вызова exec(), если для данного процесса ранее был установлен бит трассировки, ядро перед передачей управления в новую программу посылает процессу сигнал SIGTRAP. При получении данного сигнала трассируемый процесс приостанавливается, и ядро передает управление процессу-отладчику, выводя его из ожидания в вызове wait(). Процесс-родитель вызывает wait() и переходит в состояние ожидания до того момента, пока потомок неперейдет в состояние трассировки. Проснувшись, управляющий процесс, выполняяфункцию ptrace(cmd, pid, addr, data) с различными кодами операций, может производить любое действие с трассируемой программой, в частности, читать и записывать данные в адресном пространстве трассируемого процесса, а также разрешать дальнейшее выполнение трассируемого процесса или производить его пошаговое выполнение. Схема пошаговой отладки показана в примере выше и на рисунке: на каждом шаге процесс-отладчик разрешает выполнение очередной инструкции отлаживаемого процесса и затем вызывает wait() и погружается в состояние ожидания, а ядро возобновляет выполнение трассируемого потомка, исполняет трассируемую команду и вновь передает управление отладчику, выводя его из ожидания . 9. Трассировка процессов. /* Процесс-сын: */ int main(int argc, char **argv) { /* деление на ноль – здесь процессу будет послан сигнал SIGFPE – floating point exception */ return argc/0; } Процесс-родитель: #include #include #include #include #include #include #include { pid_t pid; int status; struct user_regs_struct REG; if ((pid = fork()) == 0) { /*находимся в процессе-потомке, разрешаем трассировку */ ptrace(PTRACE_TRACEME, 0, 0, 0); execl(“son”, ”son”, 0); /* замещаем тело процесса */ /* здесь процесс-потомок будет остановлен с сигналом SIG_TRAP, ожидая команды продолжения выполнения от управляющего процесса*/ } /* в процессе-родителе */ while (1) { /* ждем, когда отлаживаемый процесс приостановится */ wait(&status); /*читаем содержимое регистров отлаживаемого процесса */ ptrace(PTRACE_GETREGS, pid, ®, ®); /* выводим статус отлаживаемого процесса, номер сигнала, который его остановил и значения прочитанных регистров */ printf("signal = %d, status = %#x, EIP=%#x ESP=%#x\n", WSTOPSIG(status), status, REG.eip, REG.esp); if (WSTOPSIG(status) != SIGTRAP) { if (!WIFEXITED(status)) { /* завершаем выполнение трассируемого процесса */ ptrace (PTRACE_KILL, pid, 0, 0); } break; } /* разрешаем выполнение трассируемому процессу */ ptrace (PTRACE_CONT, pid, 0, 0); } } ДЛЯ БИЛЕТОВ 33-35 ОБЩАЯ ЧАСТЬ Именование разделяемых объектов. Для всех средств IPC приняты общие правила именования объектов, позволяющие процессу получить доступ к такому объекту. Для именования объекта IPC используется ключ, представляющий собой целое число. Ключи являются уникальными во всей UNIX-системе идентификаторами объектов IPC, и зная ключ для некоторого объекта, процесс может получить к нему доступ. При этом процессу возвращается дескриптор объекта, который в дальнейшем используется для всех операций с ним. Проведя аналогию с файловой системой, можно сказать, что ключ аналогичен имени файла, а получаемый по ключу дескриптор – файловому дескриптору, получаемому во время операции открытия файла. Ключ для каждого объекта IPC задается в момент его создания тем процессом, который его порождает, а все процессы, желающие получить в дальнейшем доступ к этому объекту, должны указывать тот же самый ключ. Итак, все процессы, которые хотят работать с одним и тем же IPC-ресурсом, должны знать некий целочисленный ключ, по которому можно получить к нему доступ. В принципе, программист, пишущий программы для работы с разделяемым ресурсом, может просто жестко указать в программе некоторое константное значение ключа для именования разделяемого ресурса. Однако, возможна ситуация, когда к моменту запуска такой программы в системе уже существует разделяемый ресурс с таким значением ключа, и в виду того, что ключи должны быть уникальными во всей системе, попытка породить второй ресурс с таким же ключом закончится неудачей (подробнее этот момент будет рассмотрен ниже). Генерация ключей: функция ftok(). Как видно, встает проблема именования разделяемого ресурса: необходим некий механизм получения заведомо уникального ключа для именования ресурса, но вместе с тем нужно, чтобы этот механизм позволял всем процессам, желающим работать с одним ресурсом, получить одно и то же значение ключа. Для решения этой задачи служит функция |