2.2. Задание по условию 2.1, но отец отслеживает момент завершения какого-нибудь из сыновей и при обнаружении этого факта запускает новый процесс-сын.
3. Исследовать взаимодействие процессов, когда они используют общий указатель на файл, открываемый до размножения процессов, и когда процессы открывают свои указатели.
Лабораторная работа №4
Сигналы
Цель работы – изучение механизма взаимодействия процессов с использованием сигналов.
Теоретическая часть
Сигналы не могут непосредственно переносить информацию, что ограничивает их применимость в качестве общего механизма межпроцессного взаимодействия. Тем не менее, каждому типу сигналов присвоено мнемоническое имя (например, SIGINT), которое указывает, для чего обычно используется сигнал этого типа. Имена сигналов определены в стандартном заголовочном файле <signal.h> при помощи директивы препроцессора #define. Как и следовало ожидать, эти имена соответствуют небольшим положительным целым числам.
Большинство типов сигналов UNIX предназначены для использования ядром, хотя есть несколько сигналов, которые посылаются от процесса к процессу:
SIGABRT – сигнал прерывания процесса (process abort signal). Посылается процессу при вызове им функции abort. В результате сигнала произойдет аварийное завершение. Следствием этого в реализациях UNIX является сброс образа памяти с выводом сообщения Quit – core dumped;
SIGALRM – сигнал таймера (alarm clock). Посылается процессу ядром при срабатывании таймера. Каждый процесс может устанавливать не менее трех таймеров. Первый из них измеряет прошедшее реальное время. Этот таймер устанавливается самим процессом при помощи системного вызова alarm;
SIGBUS – сигнал ошибки на шине (bus error). Этот сигнал посылается при возникновении некоторой аппаратной ошибки и вызывает аварийное завершение;
SIGCHLD – сигнал останова или завершения дочернего процесса (child process terminated or stopped). Если дочерний процесс останавливается или завершается, то ядро сообщит об этом родительскому процессу, послав ему данный сигнал. По умолчанию родительский процесс игнорирует этот сигнал, поэтому, если в родительском процессе необходимо получать сведения о завершении дочерних процессов, то нужно перехватывать этот сигнал;
SIGCONT – продолжение работы остановленного процесса (continue executing if stopped). Это сигнал управления процессом, который продолжит выполнение процесса, если он был остановлен; в противном случае процесс будет игнорировать этот сигнал. Данный сигнал обратный сигналу SIGSTOP;
SIGHUP – сигнал освобождения линии (hangup signal). Посылается ядром всем процессам, подключенным к управляющему терминалу (control terminal) при отключении терминала. Он также посылается всем членам сеанса, если завершает работу лидер сеанса (обычно процесс командного интерпретатора), связанного с управляющим терминалом;
SIGIIL – недопустимая команда процессора (illegal instruction). Посылается операционной системой, если процесс попытается выполнить недопустимую машинную команду;
SIGINT – сигнал прерывания программы (interrupt). Посылается ядром всем процессам сеанса, связанного с терминалом, когда пользователь нажимает клавишу прерывания. Это также обычный способ остановки выполняющейся программы;
SIGKILL – сигнал уничтожения процесса (kill). Это довольно специфический сигнал, который посылается от одного процесса к другому и приводит к немедленному прекращению работы получающего сигнал процесса;
SIGPIPE – сигнал о попытке записи в канал или сокет, для которых принимающий процесс уже завершил работу (write on a pipe or socket when recipent is terminated);
SIGPOLL – сигнал о возникновении одного из опрашиваемых событий (pollable event). Этот сигнал генерируется ядром, когда некоторый открытый дескриптор файла становится готовым для ввода или вывода;
SIGPROF – сигнал профилирующего таймера (profiling time expired). Как было упомянуто для сигнала SIGALRM, любой процесс может установить не менее трех таймеров. Второй из этих таймеров может использоваться для измерения времени выполнения процесса в пользовательском и системном режимах. Этот сигнал генерируется, когда истекает время, установленное в этом таймере, и поэтому может быть использован средством профилирования программы;
SIGQUIT – сигнал о выходе (quit). Очень похожий на сигнал SIGINT, этот сигнал посылается ядром, когда пользователь нажимает клавишу выхода используемого терминала. В отличие от SIGINT, этот сигнал приводит к аварийному завершению и сбросу образа памяти;
SIGSEGV – обращение к некорректному адресу памяти (invalid memory reference). Сокращение SEGV в названии сигнала означает нарушение границ сегментов памяти (segmentation violation). Сигнал генерируется, если процесс пытается обратиться к неверному адресу памяти;
SIGSTOP – сигнал останова (stop executing). Это сигнал управления заданиями, который останавливает процесс. Его, как и сигнал SIGKILL, нельзя проигнорировать или перехватить;
SIGSYS – некорректный системный вызов (invalid system call). Посылается ядром, если процесс пытается выполнить некорректный системный вызов;
SIGTERM – программный сигнал завершения (software termination signal). Программист может использовать этот сигнал для того, чтобы дать процессу время для «наведения порядка», прежде чем посылать ему сигнал SIGKILL;
SIGTRAP – сигнал трассировочного прерывания (trace trap). Это особый сигнал, который в сочетании с системным вызовом ptrace используется отладчиками, такими как sdb, adb, gdb;
SIGTSTP – терминальный сигнал остановки (terminal stop signal). Он формируется при нажатии специальной клавиши останова;
SIGTTIN – сигнал о попытке ввода с терминала фоновым процессом (background process attempting read). Если процесс выполняется в фоновом режиме и пытается выполнить чтение с управляющего терминала, то ему посылается этот сигнал. Действие сигнала по умолчанию – остановка процесса;
SIGTTOU – сигнал о попытке вывода на терминал фоновым процессом (background process attempting write). Аналогичен сигналу SIGTTIN, но генерируется, если фоновый процесс пытается выполнить запись в управляющий терминал. Действие сигнала по умолчанию – остановка процесса;
SIGURG – сигнал о поступлении в буфер сокета срочных данных (high bandwidth data is available at a socket). Он сообщает процессу, что по сетевому соединению получены срочные внеочередные данные;
SIGUSR1 и SIGUSR2 – пользовательские сигналы (user defined signals 1 and 2). Так же, как и сигнал SIGTERM, эти сигналы никогда не посылаются ядром и могут использоваться для любых целей по выбору пользователя;
SIGVTALRM – сигнал виртуального таймера (virtual timer expired). Третий таймер можно установить так, чтобы он измерял время, которое процесс выполняет в пользовательском режиме;
SIGXCPU – сигнал о превышении лимита процессорного времени (CPU time limit exceeded). Он посылается процессу, если суммарное процессорное время, занятое его работой, превысило установленный предел. Действие по умолчанию – аварийное завершение;
SIGXFSZ – сигнал о превышении предела на размер файла (file size limit exceeded). Он генерируется, если процесс превысит максимально допустимый размер файла.
При получении сигнала процесс может выполнить одно из трех действий. Первое – действие по умолчанию. Оно заключается в прекращении выполнения процесса, а для некоторых сигналов – в игнорировании сигнала либо в остановке процесса. Второе действие – игнорировать сигнал и продолжать выполнение. Третье – выполнить определенное пользователем действие.
Наборы сигналов являются одним из основных параметров, передаваемых работающим с сигналами системным вызовам. Они просто задают список сигналов, которые необходимо передать системному вызову.
Наборы сигналов определяются при помощи типа sigset_t, который определен в заголовочном файле <signal.h>. Размер типа задан так, чтобы в нем мог поместиться весь набор определенных в системе сигналов. Выбрать определенные сигналы можно, начав либо с полного набора сигналов и удалив ненужные сигналы, либо с пустого набора, включив в него нужные. Инициализация пустого и полного набора сигналов выполняется при помощи процедур sigemptyset и sigfillset соответственно. После инициализации с наборами сигналов можно оперировать при помощи процедур sigaddset и sigdelset, соответственно добавляющих и удаляющих указанные вами сигналы.
Описание данных процедур:
#include <signal.h>
/* Инициализация*/
int sigemptyset (sigset_t *set);
int sigfillset (sigset_t *set);
/*Добавление и удаление сигналов*/
int sigaddset (sigset_t *set, int signo);
int sigdelset (sigset_t *set, int signo);
Процедуры sigemptyset и sigfillset имеют единственный параметр – указатель на переменную типа sigset_t. Вызов sigemptyset инициализирует набор set, исключив из него все сигналы. И наоборот, вызов sigfillset инициализирует набор, на который указывает set, включив в него все сигналы. Приложения должны вызывать sigemptyset или sigfillset хотя бы один раз для каждой переменной типа sigset_t.
Процедуры sigaddset и sigdelset принимают в качестве параметров указатель на инициализированный набор сигналов и номер сигнала, который должен быть добавлен или удален. Второй параметр, signo, может быть символическим именем константы, таким как SIGINT, или настоящим номером сигнала, но в последнем случае программа окажется системно-зависимой.
После определения списка сигналов можно задать определенный метод обработки сигнала при помощи процедуры sigaction:
#include <signal.h>
int sigaction (int signo, const struct sigaction *act,
struct sigaction *oact);
Первый параметр signo задает отдельный сигнал, для которого нужно определить действие. Чтобы это действие выполнялось, процедура sigaction должна быть вызвана до получения сигнала типа signo. Значение переменной signo может быть любое из ранее определенных имен сигналов, за исключением SIGSTOP и SIGKILL, которые предназначены только для остановки или завершения процесса и не могут обрабатываться по-другому.
Второй параметр, act, определяет обработчика сигнала signo. Третий параметр, oact, если не равен NULL, указывает на структуру, куда будет помещено описание старого метода обработки сигнала. Рассмотрим структуру sigaction, определенную в файле <signal.h>:
struct sigaction {
void (*sa_handler) (int); /*Функция обработчика*/
sigset_t sa_mask, /*Сигналы, которые блокируются
во время обработки сигнала*/
int sa_flags; /*Флаги, влияющие на поведение сигнала*/
void (*sa_sigaction) (int, siginfo_t *, void *);
/*Указатель на обработчик сигналов*/
};
Первое поле, sa_handler, задает обработчик сигнала signo. Это поле может иметь три вида значений. Первое – SIG_DFL – константа, сообщающая, что нужно восстановить обработку сигнала по умолчанию. Второе – SIG_IGN – константа, означающая, что нужно игнорировать данный сигнал. Не может использоваться для сигналов SIGSTOP и SIGKILL. Третье – адрес функции, принимающей аргумент типа int. Если функция объявлена в тексте программы до заполнения sigaction, то полю sa_handler можно просто присвоить имя функции. Компилятор поймет, что имелся в виду ее адрес. Эта функция будет выполняться при получении сигнала signo, а само значение signo будет передано в качестве аргумента вызываемой функции. Управление будет передано функции, как только процесс получит сигнал, какой бы участок программы при этом ни выполнялся. После возврата из функции управление будет снова передано процессу и продолжится с точки, в которой выполнение процесса было прервано.
Второе поле, sa_mask, демонстрирует первое практическое использование набора сигналов. Сигналы, заданные в этом поле, будут блокироваться во время выполнения функции, заданной полем sa_handler. Это не означает, что эти сигналы будут игнорироваться, просто их обработка будет отложена до завершения функции. При входе в функцию перехваченный сигнал также будет неявно добавлен к текущей маске сигналов.
Поле sa_flags может использоваться для изменения характера реакции на сигнал signo.
Пример перехвата сигнала SIGINT демонстрирует, как можно перехватить сигнал, а также проясняет лежащий в его основе механизм сигналов. Программа sigex просто связывает с сигналом SIGINT функцию catchint, а затем выполняет набор операторов sleep и printf. В данном примере определена структура act типа sigaction как static, поэтому при инициализации структуры все поля, в частности поле sa_flags, обнуляются:
#include <signal.h>
main()
{
static struct sigaction act;
/*Определение процедуры обработчика сигнала catchint*/
void catchint (int);
/*Задание действия при получении сигнала SIGINT*/
act.sa_handler = catchint;
/*Создать маску, включающую все сигналы*/
sigfillset (& (act.sa_mask));
/*До вызова процедуры sigaction сигнал SIGINT*/
/*приводил к завершению процесса (действие по умолчанию).*/
sigaction (SIGINT, &act, NULL);
/*При получении сигнала SIGINT управление*/
/*будет передаваться процедуре catchint*/
printf (“Вызов sleep номер 1n”);
sleep (1);
printf (“Вызов sleep номер 2n”);
sleep (1);
printf (“Вызов sleep номер 3n”);
sleep (1);
printf (“Вызов sleep номер 4n”);
sleep (1);
printf (“Выходn”);
exit (0);
}
/*Простая функция для обработки сигнала SIGINT*/
void catchint (int signal)
{
printf (“nСигнал CATCHINT: signo = %dn”, signo);
printf (“Сигнал CATCHINT: возвратnn”);
}
Сеанс обычного запуска sigex будет выглядеть так:
$ sigex
Вызов sleep номер 1
Вызов sleep номер 2
Вызов sleep номер 3
Вызов sleep номер 4
Выход
Пользователь может прервать выполнение данной программы, нажав клавишу прерывания задания. Если она была нажата до того, как в программе была выполнена процедура sigaction, то процесс просто завершит работу. Если же нажать на клавишу прерывания после вызова, то управление будет передано функции catchint:
$ sigex
Вызов sleep номер 1
<прерывание> (пользователь нажимает на клавишу прерывания)
Сигнал CATCHINT : signo =2
Сигнал CATCHINT : возврат
Вызов sleep номер 2
Вызов sleep номер 3
Вызов sleep номер 4
Выход
Обратите внимание на то, как передается управление из тела программы в процедуру catchint. После завершения этой процедуры, управление продолжится с точки, в которой программа была прервана. Можно попробовать прервать программу и в другом месте:
$ sigex
Вызов sleep номер 1
Вызов sleep номер 2
<прерывание> (пользователь нажимает на клавишу прерывания)
Сигнал CATCHINT : signo =2
Сигнал CATCHINT : возврат
Вызов sleep номер 3
Вызов sleep номер 4
Выход
Для того чтобы процесс игнорировал сигнал прерывания SIGINT, нужно заменить строку в программе:
act.sa_handler = catchint;
на
act.sa_handler = SIG_IGN;
После выполнения этого оператора нажатие клавиши прерывания будет безрезультатным. Снова разрешить прерывание можно так:
act.sa_handler = SIG_IGN;
sigaction (SIGINT, &act, NULL);
sigaction (SIGQUIT, &act, NULL);
При этом игнорируются оба сигнала SIGINT и SIGQUIT. Это может быть использовано в программах, которые не должны прерываться с клавиатуры.
Как упоминалось выше, в структуре sigaction может быть заполнен третий параметр oact. Это позволяет сохранять и восстанавливать прежнее состояние обработчика сигнала, как показано в следующем примере:
#include <signal.h>
static struct sigaction act, oact;
/*Сохранить старый обработчик сигнала SIGTERM*/
sigaction (SIGTERM, NULL, &oact);
/*Определить новый обработчик сигнала SIGTERM*/
act.sa_handler = SIG_IGN;
sigaction (SIGTERM, &act, NULL);
/*Выполнить какие-либо действия*/
/*Восстановить старый обработчик*/
sigaction (SIGTERM, &oact, NULL);
Предположим, что программа использует временный рабочий файл. Следующая простая процедура удаляет файл:
/*Аккуратный выход из программы*/
#include <stdio.h>
#include <stdlib.h>
void g_exit (int s)
{
unlink (“tempfile”);
fprintf (stderr, “Прерывание – выход из программыn”);
exit (1);
}
Можно связать эту процедуру с определенным сигналом:
extern void g_exit (int);
...
static struct sigaction act;
act.sa_handler = g_exit;
sigaction (SIGINT, &act, NULL);
Если после вызова пользователь нажмет клавишу прерывания, то управление будет автоматически передано процедуре g_exit. Можно дополнить процедуру g_exit другими необходимыми для завершения операциями.
Следующий пример – программа synchro создает два процесса, которые будут поочередно печатать сообщения на стандартный вывод. Они синхронизируют свою работу, посылая друг другу сигнал SIGUSR1 при помощи вызова kill:
#include <unistd.h>
#include <signal.h>
int ntimes = 0;
main ()
{
pid_t pid, ppid;
void p_action (int), c_action (int);
static struct sigaction pact, cact;
/*Задаем обработчик сигнала SIGUSR1 в родительском процессе*/
pact.sa_handler = p_action;
sigaction (SIGUSR1, &pact, NULL);
switch (pid = fork ()) {
case -1: /*Ошибка*/
perrror (“synchro”);
exit (1);
case 0: /*Дочерний процесс*/
/*Задаем обработчик в дочернем процессе*/
cact.sa_handler = c_action;
sigaction (SIGUSR1, &cact, NULL);
/*Получаем идентификатор родительского процесса*/
ppid = getppid ();
/*Бесконечный цикл*/
for (;;)
{
sleep (1);
kill (ppid, SIGUSR1);
pause ();
}
default: /*Родительский процесс*/
/*Бесконечный цикл*/
for (;;)
{
pause ();
sleep (1);
kill (pid, SIGUSR1);
}
}
}
void p_action (int sig)
{
printf (“Родительский процесс получил сигнал #%dn”, ++ntimes);
}
void c_action (int sig)
{
printf (“Дочерний процесс получил сигнал #%dn”, ++ntimes);
}
Оба процесса выполняют бесконечный цикл, приостанавливая работу до получения сигнала от другого процесса. Они используют для этого системный вызов pause, который просто приостанавливает работу до получения сигнала. Затем каждый из процессов выводит сообщение и, в свою очередь, посылает сигнал при помощи вызова kill. Дочерний процесс начинает вывод сообщений. Оба процесса завершают работу, когда пользователь нажимает клавишу прерывания. Диалог с программой может выглядеть примерно так:
$ synchro
Родительский процесс получил сигнал #1
Дочерний процесс получил сигнал #1
Родительский процесс получил сигнал #2
Дочерний процесс получил сигнал #2
<прерывание> (пользователь нажал на клавишу прерывания)
$
Порядок выполнения работы
1. Изучить теоретическую часть лабораторной работы.
2. Организовать функционирование процессов следующей структуры:
Процессы определяют свою работу выводом сообщений вида : N pid (N – текущий номер сообщения) на экран. “Отец” периодически, по очереди, посылает сигнал SIGUSR1 “сыновьям”. “Сыновья” периодически посылают сигнал SIGUSR2 “отцу”. Написать функции-обработчики сигналов, которые при получении сигнала выводят сообщение о получении сигнала на экран. При получении/посылке сигнала они выводят соответствующее сообщение: N pid сын n get/put SIGUSRm.
Предусмотреть механизм для определения “отцом”, от кого из “сыновей” получен сигнал.
3. Для процессов написать функции-обработчики сигналов от клавиатуры, которые запрашивали бы подтверждение на завершение работы при получении такого сигнала.
Лабораторная работа №5
Использование каналов
Цель работы - изучение механизма взаимодействия процессов с использованием каналов.
Теоретическая часть
Каналы являются одной из самых сильных и характерных особенностей ОС UNIX, доступных даже с уровня командного интерпретатора. Они позволяют легко соединять между собой произвольные последовательности команд. Поэтому программы UNIX могут разрабатываться как простые инструменты, осуществляющие чтение из стандартного ввода, запись в стандартный вывод и выполняющие одну, четко определенную задачу. При помощи каналов из этих основных блоков могут быть построены более сложные командные строки.
Каналы создаются в программе при помощи системного вызова pipe. В случае удачного завершения вызов сообщает два дескриптора файла: один – для записи в канал, а другой – для чтения из него. Вызов pipe определяется следующим образом:
#include <unistd.h>
int pipe (int filedes[2]);
Переменная filedes является массивом из двух целых чисел, который будет содержать дескрипторы файлов, обозначающие канал. После успешного вызова filedes[0] будет открыт для чтения из канала, а filedes[1] – для записи в канал.
В случае неудачи вызов pipe вернет значение -1. Это может произойти, если в момент вызова произойдет превышение максимально возможного числа дескрипторов файлов, которые могут быть одновременно открыты процессами пользователя (в этом случае переменная errno будет содержать значение EMFILE), или если произойдет переполнение таблицы открытых файлов в ядре (в этом случае переменная errno будет содержать значение ENFILE).
После создания канала с ним можно работать просто при помощи вызовов read и write. Следующий пример демонстрирует это: он создает канал, записывает в него три сообщения, а затем считывает их из канала:
#include <unistd.h>
#include <stdio.h>
/*Эти строки заканчиваются нулевым символом*/
#define MSGSIZE 16
char *msg1 = “hello, world #1”;
char *msg2 = “hello, world #2”;
char *msg3 = “hello, world #3”;
main ()
{
char inbuf [MSGSIZE];
int p [2], j;
/*Открыть канал*/
if (pipe (p) == -1) {
perror (“Ошибка вызова pipe”);
exit (1);
}
/*Запись в канал*/
write (p[1], msg1, MSGSIZE);
write (p[1], msg2, MSGSIZE);
write (p[1], msg3, MSGSIZE);
/*Чтение из канала*/
for (j=0; j<3; j++)
{
read (p[0], inbuf, MSGSIZE);
printf (“%sn”, inbuf);
}
exit (0);
}
На выходе программы получим:
hello, world #1
hello, world #2
hello, world #3
Каналы обращаются с данными в порядке «первый вошел – первым вышел» (FIFO). Этот порядок нельзя изменить, поскольку вызов lseek не работает с каналами.
Размеры блоков при записи в канал и чтении из него необязательно должны быть одинаковыми, хотя в нашем примере это и было так. Можно, например, писать в канал блоками по 512 байт, а затем считывать из него по 1 символу, так же как и в случае обычного файла. Тем не менее, использование блоков фиксированного размера дает определенные преимущества.
Работа примера показана графически на рис. 5.1. Эта диаграмма позволяет более ясно представить, что процесс только посылает данные сам себе, используя канал в качестве некой разновидности механизма обратной связи. Это может показаться бессмысленным, поскольку процесс общается только сам с собой.
Рис. 5.1. Первый пример работы с каналами
Настоящее значение каналов проявляется при использовании вместе с системным вызовом fork, тогда можно воспользоваться тем фактом, что файловые дескрипторы остаются открытыми в обоих процессах. Следующий пример демонстрирует это – он создает канал и вызывает fork, затем дочерний процесс обменивается несколькими сообщениями с родительским:
#include <unistd.h>
#include <stdio.h>
#define MSGSIZE 16
char *msg1 = “hello, world #1”;
char *msg2 = “hello, world #2”;
char *msg3 = “hello, world #3”;
main ()
{
char inbuf [MSGSIZE];
int p [2], j;
pid_t pid;
/*Открыть канал*/
if (pipe (p) == -1) {
perror (“Ошибка вызова pipe”);
exit (1);
}
switch (pid = fork ()) {
case -1:
perror (“Ошибка вызова fork”);
exit (2);
case 0:
/*Это дочерний процесс, выполнить запись в канал*/
write (p[1], msg1, MSGSIZE);
write (p[1], msg2, MSGSIZE);
write (p[1], msg3, MSGSIZE);
break;
default:
/*Это родительский процесс, выполнить чтение из канала*/
for (j=0; j<3; j++)
{
read (p[0], inbuf, MSGSIZE);
printf (“%sn”, inbuf);
}
wait (NULL);
}
exit (0);
}
Этот пример представлен графически на рис. 5.2. На нем показано, как канал соединяет два процесса. Здесь видно, что и в родительском, и в дочернем процессах открыто по два дескриптора файла, позволяя выполнять запись в канал и чтение из него. Поэтому любой из процессов может выполнять запись в файл с дескриптором p[1] и чтение из файла с дескриптором p[0]. Это создает определенную проблему – каналы предназначены для использования в качестве однонаправленного средства связи. Если оба процесса будут одновременно выполнять чтение из канала и запись в него, то это приведет к путанице.
Рис. 5.2. Второй пример работы с каналами.
Чтобы избежать этого, каждый процесс должен выполнять либо чтение из канала, либо запись в него и закрывать дескриптор файла, как только он стал не нужен. Фактически программа должна выполнять это для того, чтобы избежать неприятностей, если посылающий данные процесс закроет дескриптор файла, открытого на запись. Приведенные до сих пор примеры работают только потому, что принимающий процесс в точности знает, какое количество данных он может ожидать. Следующий пример представляет собой законченное решение:
#include <unistd.h>
#include <stdio.h>
#define MSGSIZE 16
char *msg1 = “hello, world #1”;
char *msg2 = “hello, world #2”;
char *msg3 = “hello, world #3”;
main ()
{
char inbuf [MSGSIZE];
int p [2], j;
pid_t pid;
/*Открыть канал*/
if (pipe (p) == -1) {
perror (“Ошибка вызова pipe”);
exit (1);
}
switch (pid = fork ()) {
case -1:
perror (“Ошибка вызова fork”);
exit (2);
case 0:
/*Дочерний процесс, закрывает дескриптор файла,*/
/*открытого для чтения, и выполняет запись в канал*/
close (p[0]);
write (p[1], msg1, MSGSIZE);
write (p[1], msg2, MSGSIZE);
write (p[1], msg3, MSGSIZE);
break;
default:
/*Родительский процесс, закрывает дескриптор файла,*/
/*открытого для записи, и выполняет чтение из канала*/
close (p[1]);
for (j=0; j<3; j++)
{
read (p[0], inbuf, MSGSIZE);
printf (“%sn”, inbuf);
}
wait (NULL);
}
exit (0);
}
В конечном итоге получится однонаправленный поток данных от дочернего процесса к родительскому. Эта упрощенная ситуация показана на рис. 5.3.
Рис. 5.3. Третий пример работы с каналами
Порядок выполнения работы
1. Изучить теоретическую часть лабораторной работы.
2. Организовать взаимодействие процессов следующей структуры:
Процессы «вопрос»(ы) посылают запросы процессу «ответ» по неименованным каналам и получают по ним ответы. Должны быть предусмотрены типы ответов, которые инициируют завершение процессов «вопрос», а также должны быть вопросы, которые инициируют порождение новых процессов.
3. Организовать взаимодействие процессов следующей структуры:
Процессы «работники» по неименованным каналам обмениваются между собой данными. Неименованные каналы существуют также между процессом «Управление» и процессами «работниками». Процесс «Управление» инициирует завершение процессов «работников».
Лабораторная работа №6
Работа с несколькими каналами
Цель работы – организация работы процессов с несколькими каналами и их взаимодействие.
Теоретическая часть
Для простых приложений применение неблокирующих операций чтения и записи работает прекрасно. Для работы с множеством каналов одновременно существует другое решение, которое заключается в использовании системного вызова select.
Возможна ситуация, когда родительский процесс выступает в качестве серверного процесса и может иметь произвольное число связанных с ним клиентских (дочерних) процессов, как показано на рис. 6.1.
Рис. 6.1. Клиент/сервер с использованием каналов
В этом случае серверный процесс должен как-то справляться с ситуацией, когда одновременно в нескольких каналах может находиться информация, ожидающая обработки. Кроме того, если ни в одном из каналов нет ожидающих данных, то может иметь смысл приостановить работу серверного процесса до их появления, а не опрашивать постоянно каналы. Если информация поступает более чем по одному каналу, то серверный процесс должен знать обо всех таких каналах для того, чтобы работать с ними в правильном порядке (например, согласно их приоритету).
Это можно сделать при помощи системного вызова select (существует также аналогичный вызов poll). Системный вызов select используется не только для каналов, но и для обычных файлов, терминальных устройств, именованных каналов и сокетов. Системный вызов select показывает, какие дескрипторы файлов из заданных наборов готовы для чтения, записи или ожидают обработки ошибок. Иногда серверный процесс не должен совсем прекращать работу, даже если не происходит никаких событий, поэтому в вызове select также можно задать предельное время ожидания. Описание данного вызова:
#include <sys/time.h>
int select (int nfds, fd_set *readfds, fd_set *writefds,
fd_set *errorfds, struct timeval *timeout);
Первый параметр nfds задает число дескрипторов файлов, которые могут представлять интерес для сервера. Программист может определять это значение самостоятельно или воспользоваться постоянной FD_SETSIZE, которая определена в файле <sys/time.h>. Значение постоянной равно максимальному числу дескрипторов файлов, которые могут быть использованы вызовом select.
Второй, третий и четвертый параметры вызова являются указателями на битовые маски, в которых каждый бит соответствует дескриптору файла. Если бит включен, то это обозначает интерес к соответствующему дескриптору файла. Набор readfds определяет дескрипторы, для которых сервер ожидает возможности чтения; набор writefds – дескрипторы, для которых сервер ожидает возможности выполнить запись; набор errorfds – дескрипторы, для которых сервер ожидает появление ошибки или исключительной ситуации. Так как работа с битами довольно неприятна и приводит к немобильности программ, существуют абстрактный тип данных fd_set, а также макросы или функции для работы с объектами этого типа:
#include <sys/time.h>
/*Инициализация битовой маски, на которую указывает fdset*/
void FD_ZERO (fd_set *fdset);
/*Установка бита fd в маске, на которую указывает fdset*/
void FD_SET (int fd, fd_set *fdset);
/*Установлен ли бит fd в маске, на которую указывает fdset?*/
int FD_ISSET (int fd, fd_set *fdset);
/*Сбросить бит fd в маске, на которую указывает fdset*/
void FD_GLR (int fd, fd_set *fdset);
Следующий пример демонстрирует, как отслеживать состояние двух открытых дескрипторов файлов:
#include <sys/time.h>
#include <sys/types.h>
#include <fcntl.h>
...
int fd1, fd2;
fd_set readset;
fd1 = open (“file1”, O_RDONLY);
fd2 = open (“file2”, O_RDONLY);
FD_ZERO (& readset);
FD_SET (fd1, &readset);
FD_SET (fd2, &readset);
switch (select (5, &readset, NULL, NULL, NULL))
{
/*Обработка ввода*/
}
Пятый параметр вызова select является указателем на следующую структуру timeval:
#include <sys/time.h>
struct timeval {
long tv_sec; /*Секунды*/
long tv_usec; /*и микросекунды*/
};
Если указатель является нулевым, как в этом примере, то вызов select будет заблокирован, пока не произойдет “интересующее” процесс событие. Если в этой структуре задано нулевое время, то вызов завершится немедленно. Если структура содержит ненулевое значение, то возврат из вызова произойдет через заданное время, когда файловые дескрипторы неактивны.
Возвращаемое вызовом select значение равно -1 в случае ошибки, нулю – после истечения временного интервала или целому числу, равному числу «интересующих» программу дескрипторов файлов. Необходимо сохранять копию исходных масок.
Пример, в котором используются три канала, связанные с тремя дочерними процессами. Родительский процесс должен отслеживать стандартный ввод:
#include <sys/time.h>
#include <sys/wait.h>
#define MSGSIZE 6
char *msg1 = “hello”;
char *msg2 = “bye”;
void parent (int [] []);
int child (int []);
main()
{
int pip [3] [2];
int i;
/*Создает три канала связи и порождает три процесса*/
for (i = 0; i < 3; i++)
{
if (pipe (pip [i]) == -1)
fatal (“Ошибка вызова pipe”);
switch (fork ()) {
case -1: /*Ошибка*/
fatal (“Ошибка вызова fork”);
case 0: /*Дочерний процесс*/
child (pip [i]);
}
}
parent (pip);
exit (0);
}
/*Родительский процесс ожидает сигнала в трех каналах*/
void parent (int p [3] [2]) /*Код родительского процесса*/
{
char buf [MSGSIZE], ch;
fd_set set, master;
int i;
/*Закрывает все ненужные дескрипторы, открытые для записи*/
for (i = 0; i < 3; i++)
close (p [i] [1]);
/*Задает битовые маски для системного вызова select*/
FD_ZERO (&master);
FD_SET (0, &master);
for (i = 0; i < 3; i++)
FD_SET (p [i] [0], &master);
/*Лимит времени для вызова select не задан, поэтому он будет*/
/*заблокирован, пока не произойдет событие*/
while (set = master, select (p [2] [0] + 1, &set, NULL, NULL, NULL) > 0)
{
/*Нельзя забывать и про стандартный ввод,*/
/* то есть дескриптор файла fd = 0*/
if (FD_ISSET (0, &set))
{
printf (“Из стандартного ввода…”);
read (0, &ch, 1);
printf (“%cn”, ch);
}
for (i = 0; i < 3; i++)
{
if (FD_ISSET (p [i] [0], &set))
{
if (read (p [i] [0], buf, MSGSIZE) > 0)
{
printf (“Сообщение от потомка %dn”, i);
printf (“MSG=%sn”, buf);
}
}
}
/*Если все дочерние процессы прекратили работу,*/
/*то сервер вернется в основную программу*/
if (waitpid (-1, NULL, WNOHANG) == -1)
return;
}
}
int child (int p [2])
{
int count;
close (p [0]);
for (count = 0; count < 2; count++)
{
write (p [1], msg1, MSGSIZE);
/*Пауза в течение случайно выбранного времени*/
sleep (getpid () % 4);
}
/*Посылает последнее сообщение*/
write (p [1], msg2, MSGSIZE);
exit (0);
}
Результат данной программы может быть таким:
Сообщение от потомка 0
MSG=hello
Сообщение от потомка 1
MSG=hello
Сообщение от потомка 2
MSG=hello
d (пользователь нажимает клавишу d, а затем клавишу Return)
Из стандартного ввода d (повторение символа d )
Из стандартного ввода d (повторение символа Return )
Сообщение от потомка 0
MSG=hello
Сообщение от потомка 1
MSG=hello
Сообщение от потомка 2
MSG=hello
Сообщение от потомка 0
MSG=bye
Сообщение от потомка 1
MSG= bye
Сообщение от потомка 2
MSG= bye
Обратите внимание, что в этом примере пользователь нажимает клавишу d, а затем символ перевода строки (Enter или Return), и это отслеживается в стандартном вводе в вызове select.
Порядок выполнения работы
1. Изучить теоретическую часть лабораторной работы.
2. На двух машинах запустить процессы и организовать между ними взаимодействие посредством канала. Один из процессов является главным, а второй подчинённым. Главный процесс может инициировать завершение подчинённого процесса.
Лабораторная работа №7
Работа с использованием неименованных каналов
Цель работы - изучение работы системы “производители-потребители” с использованием неименованных каналов.
Порядок выполнения работы
Смоделировать посредством неименованного канала работу системы «производители-потребители». Создать структуру:
Производители посылают сообщения переменной длины, потребители читают эти сообщения. При записи и чтении данных в канал решить задачу взаимного исключения. Формат порции записи
Хэвиленд К., Грэй Д., Салама Б. Системное программирование в UNIX: Руководство программиста по разработке ПО. – М.: ДМК “Пресс”, 2000.
2. WWW ресурс www.opennet
... Министерство образования Российской Федерации Саратовский государственный технический университет Формульный компилятор методические указания к выполнению лабораторной работы по курсу «Теория вычислительных процессов и структур для студентов специальности ПВС Составил доцент кафедры ПВС Сайкин А.И. ...
... рехэширования с помощью произведения; б) – Блок-схема функции поиска идентификатора; в) – Блок-схема функции добавления идентификатора 2 Проектирование лексического анализатора 2.1 Назначение лексического анализатора Лексический анализатор (или сканер) – это часть-компилятора, которая читает литеры программы на исходном языке и строит из них слова (лексемы) исходного языка. На вход ...
... приведенные ниже: Из последнего выражения видно, что: Иногда в практике целесообразно использовать зависимости: где - естественная составляющая коэффициента - длительности переходных процессов соответственно реальная и нормированная. 3. Метод случайного поиска В задачах модального формирования динамических свойств системы управления в экстремальных условиях на первое ...
... , его стали называть арифметико-логическим. Оно стало основным устройством современных компьютеров. Таким образом, два гения XVII века, установили первые вехи в истории развития цифровой вычислительной техники. Заслуги В. Лейбница, однако, не ограничиваются созданием "арифметического прибора". Начиная со студенческих лет и до конца жизни он занимался исследованием свойств двоичной системы ...
0 комментариев