Смекни!
smekni.com

Теория вычислительных процессов и структур (стр. 7 из 8)

Следующий пример – программа 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 (“Родительский процесс получил сигнал #%d&bsol;n”, ++ntimes);

}

void c_action (int sig)

{

printf (“Дочерний процесс получил сигнал #%d&bsol;n”, ++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 (“%s&bsol;n”, 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 (“%s&bsol;n”, 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 (“%s&bsol;n”, inbuf);

}

wait (NULL);

}

exit (0);

}

В конечном итоге получится однонаправленный поток данных от дочернего процесса к родительскому. Эта упрощенная ситуация показана на рис. 5.3.

Рис. 5.3. Третий пример работы с каналами

Порядок выполнения работы

1. Изучить теоретическую часть лабораторной работы.

2. Организовать взаимодействие процессов следующей структуры:


Процессы «вопрос»(ы) посылают запросы процессу «ответ» по неименованным каналам и получают по ним ответы. Должны быть предусмотрены типы ответов, которые инициируют завершение процессов «вопрос», а также должны быть вопросы, которые инициируют порождение новых процессов.