Рефераты. Теория вычислительных процессов и структур

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

    }

  void c_action (int sig)

    {

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

    }

  wait (NULL);

  }

 exit (0);

}

Этот пример представлен графически на рис. 5.2. На нем показано, как канал соединяет два процесса. Здесь видно, что и в родительском, и в дочернем процессах открыто по два дескриптора файла, позволяя выполнять запись в канал и чтение из него. Поэтому любой из процессов может выполнять запись в файл с дескриптором p[1] и чтение из файла с дескриптором p[0]. Это создает определенную проблему – каналы предназначены для использования в качестве однонаправленного средства связи. Если оба процесса будут одновременно выполнять  чтение   из  канала  и  запись  в  него,  то  это  приведет  к  путанице.



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

 

Чтобы избежать этого, каждый процесс должен выполнять либо чтение из канала, либо запись в него и закрывать дескриптор файла, как только он стал не нужен. Фактически программа должна выполнять это для того, чтобы избежать неприятностей, если посылающий данные процесс закроет дескриптор файла, открытого на запись. Приведенные до сих пор примеры работают только потому, что принимающий процесс в точности знает, какое количество данных он может ожидать. Следующий пример представляет собой законченное решение:

Страницы: 1, 2, 3, 4, 5, 6, 7, 8, 9



2012 © Все права защищены
При использовании материалов активная ссылка на источник обязательна.