Пример оспользования интервалов

#include <stdio.h>

#include <unistd.h> /* _SC_CLK_TCK */

#include <signal.h> /* SIGALRM */

#include <sys/time.h> /* не используется */

#include <sys/times.h> /* struct tms */

 

struct tms tms_stop, tms_start;

clock_t real_stop, real_start;

 

clock_t HZ; /* число ticks в секунде */

 

/* Засечь время момента старта процесса */

void hello(void){

       real_start = times (&tms_start);

}

/* Засечь время окончания процесса */

void bye(int n){

       real_stop = times (&tms_stop);

#ifdef CRONO

       /* Разность времен */

       tms_stop.tms_utime -= tms_start.tms_utime;

       tms_stop.tms_stime -= tms_start.tms_stime;

#endif

 

       /* Распечатать времена */

       printf("User time     = %g seconds [%lu ticks]\n",

         tms_stop.tms_utime / (double)HZ, tms_stop.tms_utime);

       printf("System time     = %g seconds [%lu ticks]\n",

         tms_stop.tms_stime / (double)HZ, tms_stop.tms_stime);

       printf("Children user time = %g seconds [%lu ticks]\n",

         tms_stop.tms_cutime / (double)HZ, tms_stop.tms_cutime);

       printf("Children system time = %g seconds [%lu ticks]\n",

         tms_stop.tms_cstime / (double)HZ, tms_stop.tms_cstime);

       printf("Real time       = %g seconds [%lu ticks]\n",

         (real_stop - real_start) / (double)HZ, real_stop - real_start);

       exit(n);

}

 

/* По сигналу SIGALRM - завершить процесс */

void onalarm(int nsig){

       printf("Выход #%d ================\n", getpid());

       bye(0);

}

/* Порожденный процесс */

void dochild(int n){

       hello();

       printf("Старт #%d ================\n", getpid());

       signal(SIGALRM, onalarm);

 

       /* Заказать сигнал SIGALRM через 1 + n*3 секунд */

       alarm(1 + n*3);

 

       for(;;){}  /* зациклиться в user mode */

}

 

#define NCHLD 4

int main(int ac, char *av[]){

       int i;

 

       /* Узнать число тиков в секунде */

       HZ = sysconf (_SC_CLK_TCK);

       setbuf(stdout, NULL);

 

       hello();

       for(i=0; i < NCHLD; i++)

               if(fork() == 0)

                       dochild(i);

       while(wait(NULL) > 0);

       printf("Выход MAIN =================\n");

       bye(0);

       return 0;

}

 

 

Сигналы.

Процессы в UNIX используют много разных механизмов взаимодействия. Одним из них являются сигналы.

Сигналы - это асинхронные события. Что это значит? Сначала объясним, что такое синхронные события: я два раза в день подхожу к почтовому ящику и проверяю - нет ли в нем почты (событий). Во-первых, я произвожу опрос - "нет ли для меня события?", в программе это выглядело бы как вызов функции опроса и, может быть, ожидания события. Во-вторых, я знаю, что почта может ко мне прийти, поскольку я подписался на какие-то газеты. То есть я предварительно заказывал эти события.

Схема с синхронными событиями очень распространена. Кассир сидит у кассы и ожидает, пока к нему в окошечко не заглянет клиент. Поезд периодически проезжает мимо светофора и останавливается, если горит красный. Функция Си пассивно "спит" до тех пор, пока ее не вызовут; однако она всегда готова выполнить свою работу (обслужить клиента). Такое ожидающее заказа (события) действующее лицо называется сервер. После выполнения заказа сервер вновь переходит в состояние ожидания вызова. Итак, если событие ожидается в специальном месте и в определенные моменты времени (издается некий вызов для ОПРОСА) - это синхронные события. Канонический пример - функция gets, которая задержит выполнение программы, пока с клавиатуры не будет введена строка. Большинство ожиданий внутри системных вызовов - синхронны. Ядро ОС выступает для программ пользователей в роли сервера, выполняющего сисвызовы (хотя и не только в этой роли - ядро иногда предпринимает и активные действия: передача процессора другому процессу через определенное время (режим разделения времени), убивание процесса при ошибке, и.т.п.).

Сигналы - это асинхронные события. Они приходят неожиданно, в любой момент времени - вроде телефонного звонка. Кроме того, их не требуется заказывать - сигнал процессу может поступить совсем без повода. Аналогия из жизни такова: человек сидит и пишет письмо. Вдруг его окликают посреди фразы - он отвлекается, отвечает на вопрос, и вновь продолжает прерванное занятие. Человек не ожидал этого оклика (быть может, он готов к нему, но он не озирался по сторонам специально). Кроме того, сигнал мог поступить когда он писал 5-ое предложение, а мог - когда 34-ое. Момент времени, в который произойдет прерывание, не фиксирован.

Сигналы имеют номера, причем их количество ограничено - есть определенный список допустимых сигналов. Номера и мнемонические имена сигналов перечислены в includeфайле < signal. h > и имеют вид SIG нечто. Допустимы сигналы с номерами 1.. NSIG -1, где NSIG определено в этом файле. При получении сигнала мы узнаем его номер, но не узнаем никакой иной информации: ни от кого поступил сигнал, ни что от нас хотят. Просто "звонит телефон". Чтобы получить дополнительную информацию, наш процесс должен взять ее из другого известного места; например - прочесть заказ из некоторого файла, об имени которого все наши программы заранее "договорились". Сигналы процессу могут поступать тремя путями:

· От другого процесса, который явно посылает его нам вызовом

    kill (pid, sig);

где pid - идентификатор (номер) процесса-получателя, а sig - номер сигнала. Послать сигнал можно только родственному процессу - запущенному тем же пользователем.

· От операционной системы. Система может посылать процессу ряд сигналов, сигнализирующих об ошибках, например при обращении программы по несуществующему адресу или при ошибочном номере системного вызова. Такие сигналы обычно прекращают наш процесс.

· От пользователя - с клавиатуры терминала можно нажимом некоторых клавиш послать сигналы SIGINT и SIGQUIT. Собственно, сигнал посылается драйвером терминала при получении им с клавиатуры определенных символов. Так можно прервать зациклившуюся или надоевшую программу.

Процесс-получатель должен как-то отреагировать на сигнал. Программа может:

· проигнорировать сигнал (не ответить на звонок);

· перехватить сигнал (снять трубку), выполнить какие-то действия, затем продолжить прерванное занятие;

· быть убитой сигналом (звонок был подкреплен броском гранаты в окно);

В большинстве случаев сигнал по умолчанию убивает процесс-получатель. Однако процесс может изменить это умолчание и задать свою реакцию явно. Это делается вызовом signal:

#include < signal. h >

void (* signal (int sig, void (* react)())) ();

Параметр react может иметь значение:

SIG _ IGN

сигнал sig будет отныне игнорироваться. Некоторые сигналы (например SIGKILL) невозможно перехватить или проигнорировать.

SIG _ DFL

восстановить реакцию по умолчанию (обычно - смерть получателя). имя _ функции Например

     void fr (gotsig){..... } /* обработчик */

    ... signal (sig, fr);... /* задание реакции */

Тогда при получении сигнала sig будет вызвана функция fr, в которую в качестве аргумента системой будет передан номер сигнала, действительно вызвавшего ее gotsig == sig. Это полезно, т.к. можно задать одну и ту же функцию в качестве реакции для нескольких сигналов:

    ... signal (sig1, fr); signal (sig2, fr);...

После возврата из функции fr () программа продолжится с прерванного места. Перед вызовом функции-обработчика реакция автоматически сбрасывается в реакцию по умолчанию SIG _ DFL, а после выхода из обработчика снова восстанавливается в fr. Это значит, что во время работы функции-обработчика может прийти сигнал, который убьет программу.

Приведем список некоторых сигналов; полное описание посмотрите в документации. Колонки таблицы: G - может быть перехвачен; D - по умолчанию убивает процесс (k), игнорируется (i); C - образуется дамп памяти процесса: файл core, который затем может быть исследован отладчиком adb; F - реакция на сигнал сбрасывается; S - посылается обычно системой, а не явно.

сигнал    G D C F S смысл

 

SIGTERM    + k - + - завершить процесс

SIGKILL    - k - + - убить процесс

SIGINT     + k - + - прерывание с клавиш

SIGQUIT    + k + + - прерывание с клавиш

SIGALRM    + k - + + будильник

SIGILL     + k + - + запрещенная команда

SIGBUS     + k + + + обращение по неверному

SIGSEGV    + k + + + адресу

SIGUSR1, USR2 + i - + - пользовательские

SIGCLD     + i - + + смерть потомка

· Сигнал SIGILL используется иногда для эмуляции команд с плавающей точкой, что происходит примерно так: при обнаружении "запрещенной" команды для отсутствующего процессора "плавающей" арифметики аппаратура дает прерывание и система посылает процессу сигнал SIGILL. По сигналу вызывается функция-эмулятор плавающей арифметики (подключаемая к выполняемому файлу автоматически), которая и обрабатывает требуемую команду. Это может происходить много раз, именно поэтому реакция на этот сигнал не сбрасывается.

· SIGALRM посылается в результате его заказа вызовом alarm () (см. ниже).

· Сигнал SIGCLD посылается процессу-родителю при выполнении процессом-потомком сисвызова exit (или при смерти вследствие получения сигнала). Обычно процессродитель при получении такого сигнала (если он его заказывал) реагирует, выполняя в обработчике сигнала вызов wait (см. ниже). По-умолчанию этот сигнал игнорируется.

· Реакция SIG _ IGN не сбрасывается в SIG _ DFL при приходе сигнала, т.е. сигнал игнорируется постоянно.

· Вызов signal возвращает старое значение реакции, которое может быть запомнено в переменную вида void (* f)(); а потом восстановлено.

· Синхронное ожидание (сисвызов) может иногда быть прервано асинхронным событием (сигналом), но об этом ниже.

 

Деления просесса

Системный вызов fork () (вилка) создает новый процесс: копию процесса, издавшего вызов. Отличие этих процессов состоит только в возвращаемом fork -ом значении:

0              - в новом процессе.

pid нового процесса - в исходном.

Вызов fork может завершиться неудачей если таблица процессов переполнена. Простейший способ сделать это:

main(){

     while(1)

       if(! fork ()) pause ();

}

Одно гнездо таблицы процессов зарезервировано - его может использовать только суперпользователь (в целях жизнеспособности системы: хотя бы для того, чтобы запустить программу, убивающую все эти процессы-варвары).

Пайпы и FIFO-файлы.

Процессы могут обмениваться между собой информацией через файлы. Существуют файлы с необычным поведением - так называемые FIFO -файлы (first in, first out), ведущие себя подобно очереди. У них указатели чтения и записи разделены. Работа с таким файлом напоминает проталкивание шаров через трубу - с одного конца мы вталкиваем данные, с другого конца - вынимаем их. Операция чтения из пустой "трубы" проиостановит вызов read (и издавший его процесс) до тех пор, пока кто-нибудь не запишет в FIFOфайл какие-нибудь данные. Операция позиционирования указателя - lseek () - неприме- нима к FIFO-файлам. FIFO-файл создается системным вызовом

#include < sys / types. h >

#include < sys / stat. h >

  mknod (имяФайла, S _ IFIFO | 0666, 0);

где 0666 - коды доступа к файлу. При помощи FIFO-файла могут общаться даже неродственные процессы.

Разновидностью FIFO-файла является безымянный FIFO-файл, предназначенный для обмена информацией между процессом-отцом и процессом-сыном. Такой файл - канал связи как раз и называется термином "труба" или pipe. Он создается вызовом pipe:

int conn [2]; pipe (conn);

Если бы файл-труба имел имя PIPEFILE, то вызов pipe можно было бы описать как

mknod (" PIPEFILE ", S _ IFIFO | 0600, 0);

conn [0] = open (" PIPEFILE ", O _ RDONLY);

conn [1] = open (" PIPEFILE ", O _ WRONLY);

unlink (" PIPEFILE ");

При вызове fork каждому из двух процессов достанется в наследство пара дескрипторов:

            pipe (conn);

              fork ();

 

conn [0]----<---- ----<----- conn [1]

               FIFO

conn [1]---->---- ---->----- conn [0]

процесс A            процесс B

Пусть процесс A будет посылать информацию в процесс B. Тогда процесс A сделает:

close (conn [0]);

// т.к. не собирается ничего читать

write (conn [1],...);

а процесс B

close (conn [1]);

// т.к. не собирается ничего писать

read (conn [0],...);

Получаем в итоге:

conn [1]---->----FIFO---->----- conn [0]

процесс A            процесс B

Обычно поступают еще более элегантно, перенаправляя стандартный вывод A в канал conn [1]

dup2 (conn [1], 1); close (conn [1]);

write (1,...); /* или printf */

а стандартный ввод B - из канала conn [0]

dup2 (conn [0], 0); close (conn [0]);

read (0,...); /* или gets */

Это соответствует конструкции

    $ A | B

записанной на языке СиШелл.

Файл, выделяемый под pipe, имеет ограниченный размер (и поэтому обычно целиком оседает в буферах в памяти машины). Как только он заполнен целиком - процесс, пишущий в трубу вызовом write, приостанавливается до появления свободного места в трубе. Это может привести к возникновению тупиковой ситуации, если писать программу неаккуратно. Пусть процесс A является сыном процесса B, и пусть процесс B издает вызов wait, не закрыв канал conn [0]. Процесс же A очень много пишет в трубу conn [1]. Мы получаем ситуацию, когда оба процесса спят:

A потому что труба переполнена, а процесс B ничего из нее не читает, так как ждет окончания A;

B потому что процесс-сын A не окончился, а он не может окончиться пока не допишет свое сообщение.

Решением служит запрет процессу B делать вызов wait до тех пор, пока он не прочитает ВСЮ информацию из трубы (не получит EOF). Только сделав после этого close (conn [0]); процесс B имеет право сделать wait.

Если процесс B закроет свою сторону трубы close (conn [0]) прежде, чем процесс A закончит запись в нее, то при вызове write в процессе A, система пришлет процессу A сигнал SIGPIPE - "запись в канал, из которого никто не читает".

Нелокальный переход.

Теперь поговорим про нелокальный переход. Стандартная функция setjmp позволяет установить в программе "контрольную точку"*, а функция longjmp осуществляет прыжок в эту точку, выполняя за один раз выход сразу из нескольких вызванных функций (если надо)*. Эти функции не являются системными вызовами, но поскольку они реализуются машинно-зависимым образом, а используются чаще всего как реакция на некоторый сигнал, речь о них идет в этом разделе. Вот как, например, выглядит рестарт программы по прерыванию с клавиатуры:

#include < signal. h >

#include < setjmp. h >

jmp _ buf jmp; /* контрольная точка */

 

/* прыгнуть в контрольную точку */

void onintr (nsig){ longjmp (jmp, nsig); }

 

main(){

  int n;

  n = setjmp (jmp); /* установить контрольную точку */

  if(n) printf("Рестарт после сигнала %d\n", n);

  signal (SIGINT, onintr); /* реакция на сигнал */

  printf("Начали\n");

 ...

}

setjmp возвращает 0 при запоминании контрольной точки. При прыжке в контрольную точку при помощи longjmp, мы оказываемся снова в функции setjmp, и эта функция возвращает нам значение второго аргумента longjmp, в этом примере - nsig.

Прыжок в контрольную точку очень удобно использовать в алгоритмах перебора с возвратом (backtracking): либо - если ответ найден - прыжок на печать ответа, либо если ветвь перебора зашла в тупик - прыжок в точку ветвления и выбор другой альтернативы. При этом можно делать прыжки и в рекурсивных вызовах одной и той же функции: с более высокого уровня рекурсии в вызов более низкого уровня (в этом случае jmp _ buf лучше делать автоматической переменной - своей для каждого уровня вызова функции).

Разделяемая память

shmget создает новый сегмент разделяемой памяти или находит существующий сегмент с тем же ключом shmat подключает сегмент с указанным дескриптором к виртуальной памяти обращающегося процесса shmdt отключает от виртуальной памяти ранее подключенный к ней сегмент с указанным виртуальным адресом начала shmctl служит для управления параметрами, связанными с существующим сегментом После подключения сегмента разделяемой памяти к виртуальной памяти процесса, он может обращаться к соответствующим элементам памяти с использованием обычных машинных команд чтения и записи

shmid = shmget(key, size, flag);

· size определяет желаемый размер сегмента в байтах

· если в таблице разделяемой памяти находится элемент, содержащий заданный ключ, и права доступа не противоречат текущим характеристикам процесса, то значением системного вызова является дескриптор существующего сегмента

· реальный размер сегмента можно узнать с помощью системного вызова shmctl

· иначе создается новый сегмент с размером не меньше установленного в системе минимального размера сегмента разделяемой памяти и не больше установленного максимального размера

· создание сегмента не означает немедленного выделения под него основной памяти

· откладывается до выполнения первого системного вызова подключения сегмента к виртуальной памяти некоторого процесса

· при выполнении последнего системного вызова отключения сегмента от виртуальной памяти соответствующая основная память освобождается

virtaddr = shmat(id, addr, flags);

· id - это ранее полученный дескриптор сегмента

· addr - желаемый процессом виртуальный адрес, который должен соответствовать началу сегмента в виртуальной памяти

· virtaddr - реальный виртуальный адрес начала сегмента не обязательно совпадает со значением прямого параметра addr

· если addr == 0, ядро выбирает наиболее удобный виртуальный адрес начала сегмента

shmdt(addr);

· addr - виртуальный адрес начала сегмента в виртуальной памяти, ранее полученный от системного вызова shmat

shmctl(id, cmd, shsstatbuf);

· cmd идентифицирует требуемое конкретное действие

· важна функция уничтожения сегмента разделяемой памяти

 

Семафоры

Обобщение классического механизма семафоров общего вида Диекстры

Целесообразность обобщения сомнительна

Обычно использовался облегченный вариант двоичных семафоров

Известен алгоритм реализации семафоров общего вида на основе двоичных

Семафор в ОС UNIX:

· значение семафора

· идентификатор процесса, который хронологически последним работал с семафором

· число процессов, ожидающих увеличения значения семафора

· число процессов, ожидающих нулевого значения семафора

Три системных вызова:

· semget для создания и получения доступа к набору семафоров

· semop для манипулирования значениями семафоров

· semctl для выполнения управляющих операций над набором семафоров

id = semget(key, count, flag);

· key, flag и id - обычный смысл

· count - число семафоров в наборе семафоров, обладающих одним и тем же ключом

· индивидуальный семафор идентифицируется дескриптором набора семафоров и номером семафора в наборе

· если набор семафоров с указанным ключом уже существует, то число семафоров в группе можно узнать с помощью системного вызова semctl

oldval = semop(id, oplist, count);

· id - дескриптор группы семафоров

· oplist - массив описателей операций над семафорами группы

· count - размер этого массива

· возвращается значение последнего обработанного семафора

Элемент массива oplist:

· номер семафора в указанном наборе семафоров

· операция

· флаги

Если проверка прав доступа проходит нормально

· указанные в массиве oplist номера семафоров не выходят за пределы общего размера набора семафоров

· для каждого элемента массива oplist значение семафора изменяется в соответствии со значением поля "операция"


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: