Особенности работы с каналом. Под хранение информации, передаваемой через канал, выделяется некоторый фиксированный объем оперативной памяти. В некоторых системах этот буфер может быть продолжен на внешнюю память. Что происходит, если процесс хочет записать информацию в канал, а буфер переполнен, или прочесть информацию из канала, а в буфере нет еще данных? В обоих случаях процесс приостанавливает свое выполнение и дожидается, пока не освободится место либо, соответственно, пока в канале не появится информация. Надо заметить, что в этих случаях работа процесса может изменяться в зависимости от установленных параметров, которые можно менять программно (и реакцией на эти ситуации может быть не ожидание, а возврат некоторого кода ответа).
Давайте посмотрим, как эти концепции реализуются в системе. Есть функция pipe. Аргументом этой функции должен быть указатель на массив двух целых переменных.
int pipe(pipes);
int pipes[2];
Нулевой элемент массива после обращения к функции pipeполучаетФД для чтения, первый элемент этого массива получает ФД для записи. Если нет свободных ФД, то эта функция возвращает -1. Признак конца файла для считывающего дескриптора не будет получен до тех пор, пока не закрыты все дескрипторы, связанные с записью в этот канал. Рассмотрим небольшой пример:
char *s = “Это пример”;
char b[80];
int pipes[2];
pipe(pipes);
write(pipes[1],s,strlen(s)+1);
read(pipes[0],s,strlen(s)+1);
Это пример копирования строки (понятно, что так копировать строки не надо, и вообще никто функцией pipe в пределах одного процессане пользуется). В этом примереи в последующих не обрабатываются случаи отказа. Теперь давайте рассмотрим более содержательный пример. Напишем пример программы, которая запустит и свяжет каналом два процесса:
main()
{
int fd[2];
pipe(fd);/* в отцовском процессе образуем два дескриптора канала */
if (fork())/* образуем процесс-сын, у которого будут те же дескрипторы */
{/* эта часть программы происходит в процессе-отце */
dup2(fd[1],1);/* заменяем стандартный вывод выводом в канал */
close(fd[1]);/* закрываем дескрипторы канала */
close(fd[0]);/* теперь весь вывод итак будет происходить в канал */
execl(“/bin/ls”,“ls”,(char*)0);/* заменяем тело отца на ls*/
}/* отсюда начинает работать процесс-сын */
dup2(fd[0],0);/* в процессе сыне все делаем аналогично */
close(fd[0]);
close(fd[1]);
execl(“/bin/wc”,“wc”,(char*)0);
}
Этот пример связывает конвейером две команды - lsиwc. Команда ls выводит содержимое каталога, а команда wc подсчитывает количество строк. Результатом выполнения нашей программы будет подсчет строк, выведенных командой ls.
В отцовском процессе запущен процесс ls. Всю выходную информацию lsзагружает в канал, потому что мы ассоциировали стандартное устройство вывода с каналом. Далее мы в сыне запустили процесс wc, у которого стандартное устройство ввода (т.е. то, откуда wc читает информацию)связано с дескриптором чтения из канала. Это означает, что все то, что будет писать ls в свое стандартное устройство вывода, будет поступать на стандартное устройство ввода команды wc.
Мы говорили о том, что для того чтобы канал работал корректно, и читающий дескриптор получил признак конца файла, должны быть закрыты все пишущие дескрипторы. Если бы в нашей программе не была бы указана выделенная строка, то процесс, связанный с wc завис бы, потому что в этом случае функция, читающая из канала, не дождется признака конца файла. Она будет ожидать его бесконечно долго. В процессе отце подчеркнутую строку можно было бы не указывать, т.к. дескриптор закрылся бы при завершении процесса, а в процессе сыне такая строка нужна. Т.е. вывод таков: перед завершением работы должны закрываться все дескрипторы каналов, связанные с записью.
Каналом можно связывать только родственные процессы. Технически можно связывать несколько процессов одним каналом, но могут возникнуть проблемы.
Лекция №14
Сигналы в системе UNIX
Рассмотрим взаимодействие между процессами с помощью приема-передачи сигналов. Мы уже говорили о том, что в системе UNIX можно построить аналогию механизма прерываний из некоторых событий, которые могут возникать при работе процессов.
Эти события так же, как и прерывания, однозначно определены для конкретной версии ОС, т.е. набор сигналов определен. Возникновение сигналов почти так же, как и возникновение прерываний, может происходить по следующим причинам:
некоторое событие внутри программы, например, деление на ноль или переполнение;
событие, связанное с приходом некоторой информации от устройства, например, событие, связанное с передачей от клавиатуры комбинации “Ctrl+C”;
событие, связанное с воздействием одного процесса на другой, например, “SIG_KILL”.
Система имеет фиксированный набор событий, которые могут возникать. Каждое событие имеет свое уникальное имя; эти имена обычно едины для всех версий UNIX. Такие имена называются сигналами.
Перечень сигналов находится в include-ôàéëå “signal.h”.
Есть сигналы, которые присутствуют практически во всех UNIX, но также есть сигналы, специфичные лишь для конкретной версии UNIX (FreeBSD, SCO UNIX, Linux, ...) Например, в версии BSD есть сигнал приостановки работы процесса, реакцией на который является замораживание процесса, а есть сигнал, который размораживает процесс. Это сигнал FreeBSD версии.
Прототип функции обработки сигнала:
void (* signal (sig, fun)) ()
int sig;
void (* fun) ();
При обращении к signal мы передаем:
sig - имя сигнала;
fun - указатель на функцию, которая будет обрабатывать событие, связанное с возникновением этого сигнала. Функция signal возвращает указатель на предыдущую функцию обработки данного сигнала.
Мы говорили о том, что событие, связанное с возникновением сигнала может быть обработано в системе тремя способами:
SIG_DEF - стандартная реакция на сигнал, которая предусмотрена системой;
SIG_IGN - игнорирование сигнала (следует отметить, что далеко не все сигналы можно игнорировать, например, SIG_KILL);
Некоторая пользовательская функция обработки сигнала.
Соответственно, указывая либо имена предопределенных констант, либо указатель на функцию, которую мы хотим определить как функцию-обработчик сигнала, можно предопределить реакцию на тот или иной сигнал. Установка обработки сигнала происходит одноразово, это означает то, что если мы установили некоторую обработку, то по этому правилу будет обработано только одно событие, связанное с появлением данного сигнала. И при входе в функцию-обработчика устанавливается стандартная реакция на сигнал. Возврат из функции-обработчика происходит в точку прерывания процесса.
Приведем пример программы “Будильник”. Средствами ОС мы будем “заводить” будильник. Функция alarm инициализирует появление сигнала SIG_ALRM.
main ()
{
char s[80];
signal(SIG_ALRM, alrm); /* установка режима связи с событием SIG_ALRM на функцию alrm */
alarm(5); /* заводим будильник */
printf(“Введите имя \n”);
for (;;)
{
printf(“имя:”);
if (gets(s,80) != NULL) break;
};
printf(“OK! \n”);
}
alrm ()
{
printf(“\n жду имя \n”);
alarm(5);
signal (SIG_ALRM,alrm);
}
В начале программы мы устанавливаем реакцию на сигнал SIG_ALRM - функцию alrm, далее мы заводим будильник, запрашиваем “Введите имя” и ожидаем ввода строки символов. Если ввод строки задерживается, то будет вызвана функция alrm, которая напомнит, что программа “ждет имя”, опять заведет будильник и поставит себя на обработку сигнала SIG_ALRM еще раз. И так будет до тех пор, пока не будет введена строка.
Здесь имеется один нюанс: если в момент выполнения системного вызова возникает событие, связанное с сигналом, то система прерывает выполнение системного вызова и возвращает код ответа, равный “-1”. Это мы можем также проанализировать по функции errno.
Надо отметить, что одноразово устанавливается только “свой” обработчик. Дефолтный обработчик или игнорирование устанавливается многоразово, то есть его не надо каждый раз подтверждать после обработки сигнала.
Еще две функции, которые необходимы нам для организации взаимодействия между процессами:.…
1) int kill(int pid, sig) - это функция передачи сигнала процессу. Она работает следующим образом: процессу с номером pid осуществляется попытка передачи сигнала, значение которого равно sig. Соответственно, сигнал может быть передан в рамках процессов, принадлежащих к одной группе. Код ответа: -1, если сигнал передать не удалось, пояснение опять же можно найти в errno. Функция kill может использоваться для проверки существования процесса с заданным идентификатором. Если функция выполняется с sig=0, то это тестовый сигнал, который определяет: можно или нет передать процессу сигнал; если можно, то код ответа kill отличен от “-1”. Если pid=0, то заданный сигнал передается всем процессам, входящим в группу.
2) int wait(int *wait_ret) - ожидание события в сыновнем процессе. Если сыновнего процесса нет, то управление возвращается сразу же с кодом ответа “-1” и расшифровкой в errno. Если в процессе-сыне возникло событие, то анализируются младшие 16 бит в значении wait_ret:
а) Если сын приостановлен (трассировка или получение сигнала), тогда старшие 8 бит wait_ret - , а младшие содержат код 0177.
б) Если сыновий процесс успешно завершился через обращение к функции exit. Тогда младшие 8 бит равны нулю, а старшие 8 бит равны коду, установленному функцией exit.
в) Если сын завершился из-за возникновения у него необрабатываемого сигнала, то старшие 8 бит равны нулю, а младшие - номер сигнала, который завершил процесс.
Функция wait возвращает идентификатор процесса в случае успешного выполнения и “-1” в противном случае. Если одно из перечисленных событий произошло до обращения к функции, то результат возвращается сразу же, то есть никакого ожидания не происходит, это говорит о том, что информация о событиях в процессе безвозвратно не теряется.
Давайте рассмотрим еще один пример. Наш будильник будет уже многопроцессный.