учебники, программирование, основы, введение в,

 

Организация ввода-вывода в UNIX. Файлы устройств. Аппарат прерываний. Сигналы в UNIX
Понятие виртуальной файловой системы
Cеминары 11–12 были посвящены устройству файловой системы s5fs. Существуют и другие файловые системы, имеющие архитектуру, отличную от архитектуры s5fs (иные способы отображения файла на пространство физического носителя, иное построение директорий и т.д.). Современные версии UNIX-подобных операционных систем умеют работать с разнообразными файловыми системами, различающимися своей организацией. Такая возможность достигается с помощью разбиения каждой файловой системы на зависимую и независимую от конкретной реализации части, подобно тому, как в лекции 13, посвященной вопросам ввода-вывода, мы отделяли аппаратно-зависимые части для каждого устройства – драйверы – от общей базовой подсистемы ввода-вывода. Независимые части всех файловых систем одинаковы и представляют для всех остальных элементов ядра абстрактную файловую систему, которую принято называть виртуальной файловой системой. Зависимые части для различных файловых систем могут встраиваться в ядро на этапе компиляции, либо добавляться к нему динамически по мере необходимости, без перекомпиляции системы (как в системах с микроядерной архитектурой).
Рассмотрим схематично устройство виртуальной файловой системы. В файловой системе s5fs данные о физическом расположении и атрибутах каждого открытого файла представлялись в операционной системе структурой данных в таблице индексных узлов открытых файлов (см. семинар 11–12, раздел «Системные вызовы и команды для выполнения операций над файлами и директориями»), содержащей информацию из индексного узла файла во вторичной памяти. В виртуальной файловой системе, в отличие от s5fs, каждый файл характеризуется не индексным узлом inode, а некоторым виртуальным узлом vnode. Соответственно, вместо таблицы индексных узлов открытых файлов в операционной системе появляется таблица виртуальных узлов открытых файлов. При открытии файла в операционной системе для него заполняется (если, конечно, не был заполнен раньше) элемент таблицы виртуальных узлов открытых файлов, в котором хранятся, как минимум, тип файла, счетчик числа открытий файла, указатель на реальные физические данные файла и, обязательно, указатель на таблицу системных вызовов, совершающих операции над файлом, – таблицу операций. Реальные физические данные файла (равно как и способ расположения файла на диске и т.п.) и системные вызовы, реально выполняющие операции над файлом, уже не являются элементами виртуальной файловой системы. Они относятся к одной из зависимых частей файловой системы, так как определяются ее конкретной реализацией.
При выполнении операций над файлами по таблице операций, чей адрес содержится в vnode, определяется системный вызов, который будет на самом деле выполнен над реальными физическими данными файла, чей адрес также находится в vnode. В случае с s5fs данные, на которые ссылается vnode, – это как раз данные индексного узла, рассмотренные на семинарах 11–12 и на лекции 12. Заметим, что таблица операций является общей для всех файлов, принадлежащих одной и той же файловой системе.
Операции над файловыми системами. Монтирование файловых систем
В материалах семинаров 11–12 рассматривалась только одна файловая система, расположенная в одном разделе физического носителя. Как только мы переходим к сосуществованию нескольких файловых систем в рамках одной операционной системы, встает вопрос о логическом объединении структур этих файловых систем. При работе операционной системы нам изначально доступна лишь одна, так называемая корневая, файловая система. Прежде, чем приступить к работе с файлом, лежащим в некоторой другой файловой системе, мы должны встроить ее в уже существующий ациклический граф файлов. Эта операция – операция над файловой системой – называется монтированием файловой системы (mount).
Для монтирования файловой системы (см. лекцию 12, раздел «Монтирование файловых систем») в существующем графе должна быть найдена или создана некоторая пустая директория – точка монтирования, к которой и присоединится корень монтируемой файловой системы. При операции монтирования в ядре заводятся структуры данных, описывающие файловую систему, а в vnode для точки монтирования файловой системы помещается специальная информация.
Монтирование файловых систем обычно является прерогативой системного администратора и осуществляется командой операционной системы mount в ручном режиме, либо автоматически при старте операционной системы. Использование этой команды без параметров не требует специальных полномочий и позволяет пользователю получить информацию обо всех смонтированных файловых системах и соответствующих им физических устройствах. Для пользователя также обычно разрешается монтирование файловых систем, расположенных на гибких магнитных дисках. Для первого накопителя на гибких магнитных дисках такая команда в Linux будет выглядеть следующим образом:
mount /dev/fd0 <имя пустой директории>
где <имя пустой директории> описывает точку монтирования, а /dev/fd0 – специальный файл устройства, соответствующего этому накопителю (о специальных файлах устройств будет подробно рассказано в следующем разделе).



Синтаксис команды
mount [-hV]
mount [-rw] [-t fstype] device dir
Описание команды
Настоящее описание не является полным описанием команды mount, а описывает только те ее опции (очень малую часть), которые используются в данном курсе. Для получения полного описания следует обратиться к UNIX Manual.
Команда mount предназначена для выполнения операции монтирования файловой системы и получения информации об уже смонтированных файловых системах.
Опции -h, -V используются при вызове команды без параметров и служат для следующих целей:
  • -h – вывести краткую инструкцию по пользованию командой;
  • -V – вывести информацию о версии команды mount;

Команда mount без опций и без параметров выводит информацию обо всех уже смонтированных файловых системах.
Команда mount с параметрами служит для выполнения операции монтирования файловой системы.
Параметр device задает имя специального файла для устройства, содержащего файловую систему.
Параметр dir задает имя точки монтирования (имя некоторой уже существующей пустой директории). При монтировании могут использоваться следующие опции:

  • -r — смонтировать файловую систему только для чтения (read only);
  • -w — смонтировать файловую систему для чтения и для записи (read/write). Используется по умолчанию;
  • -t fstype — задать тип монтируемой файловой системы как fstype. Поддерживаемые типы файловых систем в операционной системе Linux: adfs, affs, autofs, coda, coherent, cramfs, devpts, efs, ext, ext2, ext3, hfs, hpfs, iso9660 (для CD), minix, msdos, ncpfs, nfs, ntfs, proc, qnx4, reiserfs, romfs, smbfs, sysv, udf, ufs, umsdos, vfat, xenix, xfs, xiafs. При отсутствии явно заданного типа команда для большинства типов файловых систем способна опознать его автоматически.

Если мы не собираемся использовать смонтированную файловую систему в дальнейшем (например, хотим вынуть ранее смонтированную дискету), нам необходимо выполнить операцию логического разъединения смонтированных файловых систем (umount). Для этой операции, которая тоже, как правило, является привилегией системного администратора, используется команда umount (может выполняться в ручном режиме или автоматически при завершении работы операционной системы). Для пользователя обычно доступна команда отмонтирования файловой системы на дискете в форме
umount <имя точки монтирования>
где <имя точки монтирования> – это <имя пустой директории>, использованное ранее в команде mount, или в форме
umount /dev/fd0
где /dev/fd0 – специальный файл устройства, соответствующего первому накопителю на гибких магнитных дисках.
Заметим, что для последующей корректной работы операционной системы при удалении физического носителя информации обязательно необходимо предварительное логическое разъединение файловых систем, если они перед этим были объединены.


Команда umount
Синтаксис команды
umount [-hV]
umount device
umount dir
Описание команды
Настоящее описание не является полным описанием команды umount, а описывает только те ее опции (очень малую часть), которые используются в данном курсе. Для получения полного описания следует обратиться к UNIX Manual (команда man).
Команда umount предназначена для выполнения операции логического разъединения ранее смонтированных файловых систем.
Опции -h, -V используются при вызове команды без параметров и служат для следующих целей:
  • -h – вывести краткую инструкцию по пользованию командой;
  • -V – вывести информацию о версии команды umount.

Команда umount с параметром служит для выполнения операции логического разъединения файловых систем. В качестве параметра может быть задано либо имя устройства, содержащего файловую систему – device, либо имя точки монтирования файловой системы (т.е. имя директории, которое указывалось в качестве параметра при вызове команды mount) – dir.
Заметим, что файловая система не может быть отмонтирована до тех пор, пока она находится в использовании (busy) – например, когда в ней существуют открытые файлы, какой-либо процесс имеет в качестве рабочей директории директорию в этой файловой системе и т.д.

Блочные, символьные устройства. Понятие драйвера. Блочные, символьные драйверы, драйверы низкого уровня. Файловый интерфейс
Обремененные знаниями об устройстве современных файловых систем в UNIX, мы можем, наконец, заняться вопросами реализации подсистемы ввода-вывода.
В лекции 13 (раздел «Структура системы ввода-вывода») речь шла о том, что все устройства ввода-вывода можно разделить на относительно небольшое число типов, в зависимости от набора операций, которые могут ими выполняться. Такое деление позволяет организовать «слоистую» структуру подсистемы ввода-вывода, вынеся все аппаратно-зависимые части в драйверы устройств, с которыми взаимодействует базовая подсистема ввода-вывода, осуществляющая стратегическое управление всеми устройствами.
В операционной системе UNIX принята упрощенная классификация устройств (см. лекцию 13, раздел «Систематизация внешних устройств и интерфейс между базовой подсистемой ввода-вывода и драйверами»): все устройства разделяются по способу передачи данных на символьные и блочные. Символьные устройства осуществляют передачу данных байт за байтом, в то время как блочные устройства передают блок байт как единое целое. Типичным примером символьного устройства является клавиатура, примером блочного устройства – жесткий диск. Непосредственное взаимодействие операционной системы с устройствами ввода-вывода обеспечивают их драйверы. Существует пять основных случаев, когда ядро обращается к драйверам:

  1. Автоконфигурация. Происходит в процессе инициализации операционной системы, когда ядро определяет наличие доступных устройств.
  2. Ввод-вывод. Обработка запроса ввода-вывода.
  3. Обработка прерываний. Ядро вызывает специальные функции драйвера для обработки прерывания, поступившего от устройства, в том числе, возможно, для планирования очередности запросов к нему.
  4. Специальные запросы. Например, изменение параметров драйвера или устройства.
  5. Повторная инициализация устройства или останов операционной системы.

Так же как устройства подразделяются на символьные и блочные, драйверы тоже существуют символьные и блочные. Особенностью блочных устройств является возможность организации на них файловой системы, поэтому блочные драйверы обычно используются файловой системой UNIX. При обращении к блочному устройству, не содержащему файловой системы, применяются специальные драйверы низкого уровня, как правило, представляющие собой интерфейс между ядром операционной системы и блочным драйвером устройства.
Для каждого из этих трех типов драйверов были выделены основные функции, которые базовая подсистема ввода-вывода может совершать над устройствами и драйверами: инициализация устройства или драйвера, временное завершение работы устройства, чтение, запись, обработка прерывания, опрос устройства и т.д. (об этих операциях уже говорилось в лекции 13, раздел «Систематизация внешних устройств и интерфейс между базовой подсистемой ввода-вывода и драйверами»). Эти функции были систематизированы и представляют собой интерфейс между драйверами и базовой подсистемой ввода-вывода.
Каждый драйвер определенного типа в операционной системе UNIX получает собственный номер, который по сути дела является индексом в массиве специальных структур данных операционной системы – коммутаторе устройств соответствующего типа. Этот индекс принято также называть старшим номером устройства, хотя на самом деле он относится не к устройству, а к драйверу. Несмотря на наличие трех типов драйверов, в операционной системе используется всего два коммутатора: для блочных и символьных драйверов. Драйверы низкого уровня распределяются между ними по преобладающему типу интерфейса (к какому типу ближе – в такой массив и заносятся). Каждый элемент коммутатора устройств обязательно содержит адреса (точки входа в драйвер), соответствующие стандартному набору функций интерфейса, которые и вызываются операционной системой для выполнения тех или иных действий над устройством и/или драйвером.
Помимо старшего номера устройства существует еще и младший номер устройства, который передается драйверу в качестве параметра и смысл которого определяется самим драйвером. Например, это может быть номер раздела на жестком диске (partition), доступ к которому должен обеспечить драйвер (надо отметить, что в операционной системе UNIX различные разделы физического носителя информации рассматриваются как различные устройства). В некоторых случаях младший номер устройства может не использоваться, но для единообразия он должен присутствовать. Таким образом, пара драйвер-устройство всегда однозначно определяется в операционной системе заданием пары номеров (старшего и младшего номеров устройства) и типа драйвера (символьный или блочный).
Для связи приложений с драйверами устройств операционная система UNIX использует файловый интерфейс. В числе типов файлов на предыдущем семинаре упоминались специальные файлы устройств. Так вот, каждой тройке тип-драйвер-устройство в файловой системе соответствует специальный файл устройства, который не занимает на диске никаких логических блоков, кроме индексного узла. В качестве атрибутов этого файла помимо обычных атрибутов используются соответствующие старший и младший номера устройства и тип драйвера (тип драйвера определяется по типу файла: ибо есть специальные файлы символьных устройств и специальные файлы блочных устройств, а номера устройств занимают место длины файла, скажем, для регулярных файлов). Когда открывается специальный файл устройства, операционная система, в числе прочих действий, заносит в соответствующий элемент таблицы открытых виртуальных узлов указатель на набор функций интерфейса из соответствующего элемента коммутатора устройств. Теперь при попытке чтения из файла устройства или записи в файл устройства виртуальная файловая система будет транслировать запросы на выполнение этих операций в соответствующие вызовы нужного драйвера.
Мы не будем останавливаться на практическом применении файлового интерфейса для работы с устройствами ввода-вывода, поскольку это выходит за пределы нашего курса, а вместо этого приступим к изложению концепции сигналов в UNIX, тесно связанных с понятиями аппаратного прерывания, исключения и программного прерывания.
Аппаратные прерывания (interrupt), исключения (exception), программные прерывания (trap, software interrupt). Их обработка
В лекции 13 (раздел «Опрос устройств и прерывания. Исключительные ситуации и системные вызовы») уже вводились понятия аппаратного прерывания, исключения и программного прерывания. Кратко напомним сказанное.
После выдачи запроса ввода-вывода у процессора существует два способа узнать о том, что обработка запроса устройством завершена. Первый способ заключается в регулярной проверке процессором бита занятости в регистре состояния контроллера соответствующего устройства (polling). Второй способ заключается в использовании прерываний. При втором способе процессор имеет специальный вход, на который устройства ввода-вывода, используя контроллер прерываний или непосредственно, выставляют сигнал запроса прерывания (interrupt request) при завершении операции ввода-вывода. При наличии такого сигнала процессор после выполнения текущей команды не выполняет следующую, а, сохранив состояние ряда регистров и, возможно, загрузив в часть регистров новые значения, переходит к выполнению команд, расположенных по некоторым фиксированным адресам. После окончания обработки прерывания можно восстановить состояние процессора и продолжить его работу с команды, выполнение которой было отложено.
Аналогичный механизм часто используется при обработке исключительных ситуаций (exception), возникающих при выполнении команды процессором (неправильный адрес в команде, защита памяти, деление на ноль и т.д.). В этом случае процессор не завершает выполнение команды, а поступает, как и при прерывании, сохраняя свое состояние до момента начала ее выполнения.
Этим же механизмом часто пользуются и для реализации так называемых программных прерываний (software interrupt, trap), применяемых, например, для переключения процессора из режима пользователя в режим ядра внутри системных вызовов. Для выполнения действий, аналогичных действиям по обработке прерывания, процессор в этом случае должен выполнить специальную команду.
Необходимо четко представлять себе разницу между этими тремя понятиями, для чего не лишним будет в очередной раз обратиться к лекциям (лекция 13, раздел «Опрос устройств и прерывания. Исключительные ситуации и системные вызовы»).

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

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

  1. hardware (при возникновении исключительной ситуации);
  2. другого процесса, выполнившего системный вызов передачи сигнала;
  3. операционной системы (при наступлении некоторых событий);
  4. терминала (при нажатии определенной комбинации клавиш);
  5. системы управления заданиями (при выполнении команды kill – мы рассмотрим ее позже).

Передачу сигналов процессу в случаях его генерации источниками 2, 3 и 5, т.е., в конечном счете, каким-либо другим процессом, можно рассматривать как реализацию в UNIX сигнальных средств связи, о которых рассказывалось в лекции 4.
Существует три варианта реакции процесса на сигнал:

  1. Принудительно проигнорировать сигнал.
  2. Произвести обработку по умолчанию: проигнорировать, остановить процесс (перевести в состояние ожидания до получения другого специального сигнала), либо завершить работу с образованием core файла или без него.
  3. Выполнить обработку сигнала, специфицированную пользователем.

Изменить реакцию процесса на сигнал можно с помощью специальных системных вызовов, которые мы рассмотрим позже. Реакция на некоторые сигналы не допускает изменения, и они могут быть обработаны только по умолчанию. Так, например, сигнал с номером 9 – SIGKILL обрабатывается только по умолчанию и всегда приводит к завершению процесса.
Важным вопросом при программировании с использованием сигналов является вопрос о сохранении реакции на них при порождении нового процесса или замене его пользовательского контекста. При системном вызове fork() все установленные реакции на сигналы наследуется порожденным процессом.
При системном вызове exec() сохраняются реакции только для тех сигналов, которые игнорировались или обрабатывались по умолчанию. Получение любого сигнала, который до вызова exec() обрабатывался пользователем, приведет к завершению процесса.
Прежде чем продолжить тему сигналов, нам придется подробнее остановиться на иерархии процессов в операционной системе.


Понятия группы процессов, сеанса, лидера группы, лидера сеанса, управляющего терминала сеанса. Системные вызовы getpgrp(), setpgrp(), getpgid(), setpgid(), getsid(), setsid()
В лекции 2, раздел «Одноразовые операции», уже говорилось, что все процессы в системе связаны родственными отношениями и образуют генеалогическое дерево или лес из таких деревьев, где в качестве узлов деревьев выступают сами процессы, а связями служат отношения родитель-ребенок. Все эти деревья принято разделять на группы процессов, или семьи.
Группа процессов включает в себя один или более процессов и существует, пока в группе присутствует хотя бы один процесс. Каждый процесс обязательно включен в какую-нибудь группу. При рождении нового процесса он попадает в ту же группу процессов, в которой находится его родитель. Процессы могут мигрировать из группы в группу по своему желанию или по желанию другого процесса (в зависимости от версии UNIX). Многие системные вызовы могут быть применены не к одному конкретному процессу, а ко всем процессам в некоторой группе. Поэтому то, как именно следует объединять процессы в группы, зависит от того, как предполагается их использовать. Чуть позже мы поговорим об использовании групп процессов для передачи сигналов.
В свою очередь, группы процессов объединяются в сеансы, образуя, с родственной точки зрения, некие кланы семей. Понятие сеанса изначально было введено в UNIX для логического объединения групп процессов, созданных в результате каждого входа и последующей работы пользователя в системе. С каждым сеансом, поэтому, может быть связан в системе терминал, называемый управляющим терминалом сеанса, через который обычно и общаются процессы сеанса с пользователем. Сеанс не может иметь более одного управляющего терминала, и один терминал не может быть управляющим для нескольких сеансов. В то же время могут существовать сеансы, вообще не имеющие управляющего терминала.
Каждая группа процессов в системе получает собственный уникальный номер. Узнать этот номер можно с помощью системного вызова getpgid(). Используя его, процесс может узнать номер группы для себя самого или для процесса из своего сеанса. К сожалению, не во всех версиях UNIX присутствует данный системный вызов. Здесь мы сталкиваемся с тяжелым наследием разделения линий UNIX'ов на линию BSD и линию System V, которое будет нас преследовать почти на всем протяжении данной темы. Вместо вызова getpgid() в таких системах существует системный вызов getpgrp(), который возвращает номер группы только для текущего процесса.

Системный вызов getpgid()
Прототип системного вызова
#include <sys/types.h>
#include <unistd.h>
pid_t getpgid(pid_t pid);
Описание системного вызова
Системный вызов возвращает идентификатор группы процессов для процесса с идентификатором pid.
Узнать номер группы процесс может только для себя самого или для процесса из своего сеанса. При других значениях pid системный вызов возвращает значение -1.
Тип данных pid_t является синонимом для одного из целочисленных типов языка C.

Системный вызов getpgrp()
Прототип системного вызова
#include <sys/types.h>
#include <unistd.h>
pid_t getpgrp(void);
Описание системного вызова
Системный вызов getpgrp возвращает идентификатор группы процессов для текущего процесса.
Тип данных pid_t является синонимом для одного из целочисленных типов языка C.

Для перевода процесса в другую группу процессов, возможно, с одновременным ее созданием, применяется системный вызов setpgid(). Перевести в другую группу процесс может либо самого себя (и то не во всякую и не всегда), либо свой процесс-ребенок, который не выполнял системный вызов exec(), т.е. не запускал на выполнение другую программу. При определенных значениях параметров системного вызова создается новая группа процессов с идентификатором, совпадающим с идентификатором переводимого процесса, состоящая первоначально только из одного этого процесса. Новая группа может быть создана только таким способом, поэтому идентификаторы групп в системе уникальны. Переход в другую группу без создания новой группы возможен лишь в пределах одного сеанса.
В некоторых разновидностях UNIX системный вызов setpgid() отсутствует, а вместо него используется системный вызов setpgrp(), способный только создавать новую группу процессов с идентификатором, совпадающим с идентификатором текущего процесса, и переводить в нее текущий процесс. (В ряде систем, где сосуществуют вызовы setpgrp() и setpgid(), например в Solaris, вызов setpgrp() ведет себя иначе – он аналогичен рассматриваемому ниже вызову setsid().)


Системный вызов setpgid()
Прототип системного вызова
#include <sys/types.h>
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
Описание системного вызова
Системный вызов setpgid служит для перевода процесса из одной группы процессов в другую, а также для создания новой группы процессов.
Параметр pid является идентификатором процесса, который нужно перевести в другую группу, а параметр pgid – идентификатором группы процессов, в которую предстоит перевести этот процесс.
Не все комбинации этих параметров разрешены. Перевести в другую группу процесс может либо самого себя (и то не во всякую, и не всегда), либо свой процесс-ребенок, который не выполнял системный вызов exec(), т.е. не запускал на выполнение другую программу.
  • Если параметр pid равен 0, то считается, что процесс переводит в другую группу самого себя.
  • Если параметр pgid равен 0, то в Linux считается, что процесс переводится в группу с идентификатором, совпадающим с идентификатором процесса, определяемого первым параметром.
  • Если значения, определяемые параметрами pid и pgid, равны, то создается новая группа с идентификатором, совпадающим с идентификатором переводимого процесса, состоящая первоначально только из этого процесса. Переход в другую группу без создания новой группы возможен только в пределах одного сеанса.

В новую группу не может перейти процесс, являющийся лидером группы, т.е. процесс, идентификатор которого совпадает с идентификатором его группы.
Тип данных pid_t является синонимом для одного из целочисленных типов языка C.
Возвращаемое значение
Системный вызов возвращает значение 0 при нормальном завершении и значение -1 при возникновении ошибки.

Системный вызов setpgrp()
Прототип системного вызова
#include <sys/types.h>
#include <unistd.h>
int setpgrp(void);
Описание системного вызова
Системный вызов setpgrp служит для перевода текущего процесса во вновь создаваемую группу процессов, идентификатор которой будет совпадать с идентификатором текущего процесса.
Возвращаемое значение
Системный вызов возвращает значение 0 при нормальном завершении и значение -1 при возникновении ошибки.

Процесс, идентификатор которого совпадает с идентификатором его группы, называется лидером группы. Одно из ограничений на применение вызовов setpgid() и setpgrp() состоит в том, что лидер группы не может перебраться в другую группу.
Каждый сеанс в системе также имеет собственный номер. Для того чтобы узнать его, можно воспользоваться системным вызовом getsid(). В разных версиях UNIX на него накладываются различные ограничения. В Linux такие ограничения отсутствуют.


Системный вызов getsid()
Прототип системного вызова
#include <sys/types.h>
#include <unistd.h>
pid_t getsid(pid_t pid);
Описание системного вызова
Системный вызов возвращает идентификатор сеанса для процесса с идентификатором pid. Если параметр pid равен 0, то возвращается идентификатор сеанса для данного процесса
Тип данных pid_t является синонимом для одного из целочисленных типов языка C.

Использование системного вызова setsid() приводит к созданию новой группы, состоящей только из процесса, который его выполнил (он становится лидером новой группы), и нового сеанса, идентификатор которого совпадает с идентификатором процесса, сделавшего вызов. Такой процесс называется лидером сеанса. Этот системный вызов может применять только процесс, не являющийся лидером группы.


Системный вызов setsid()
Прототип системного вызова
#include <sys/types.h>
#include <unistd.h>
int setsid(void);
Описание системных вызовов
Этот системный вызов может применять только процесс, не являющийся лидером группы, т.е. процесс, идентификатор которого не совпадает с идентификатором его группы. Использование системного вызова setsid приводит к созданию новой группы, состоящей только из процесса, который его выполнил (он становится лидером новой группы), и нового сеанса, идентификатор которого совпадает с идентификатором процесса, сделавшего вызов.
Возвращаемое значение
Системный вызов возвращает значение 0 при нормальном завершении и значение -1 при возникновении ошибки.

Если сеанс имеет управляющий терминал, то этот терминал обязательно приписывается к некоторой группе процессов, входящей в сеанс. Такая группа процессов называется текущей группой процессов для данного сеанса. Все процессы, входящие в текущую группу процессов, могут совершать операции ввода-вывода, используя управляющий терминал. Все остальные группы процессов сеанса называются фоновыми группами, а процессы, входящие в них – фоновыми процессами. При попытке ввода-вывода фонового процесса через управляющий терминал этот процесс получит сигналы, которые стандартно приводят к прекращению работы процесса. Передавать управляющий терминал от одной группы процессов к другой может только лидер сеанса. Заметим, что для сеансов, не имеющих управляющего терминала, все процессы являются фоновыми.
При завершении работы процесса – лидера сеанса все процессы из текущей группы сеанса получают сигнал SIGHUP, который при стандартной обработке приведет к их завершению. Таким образом, после завершения лидера сеанса в нормальной ситуации работу продолжат только фоновые процессы.
Процессы, входящие в текущую группу сеанса, могут получать сигналы, инициируемые нажатием определенных клавиш на терминале – SIGINT при нажатии клавиш <ctrl> и <c>, и SIGQUIT при нажатии клавиш <ctrl> и <4>. Стандартная реакция на эти сигналы – завершение процесса (с образованием core файла для сигнала SIGQUIT).
Необходимо ввести еще одно понятие, связанное с процессом, – эффективный идентификатор пользователя. В материалах первого семинара говорилось о том, что каждый пользователь в системе имеет собственный идентификатор – UID. Каждый процесс, запущенный пользователем, задействует этот UID для определения своих полномочий. Однако иногда, если у исполняемого файла были выставлены соответствующие атрибуты, процесс может выдать себя за процесс, запущенный другим пользователем. Идентификатор пользователя, от имени которого процесс пользуется полномочиями, и является эффективным идентификатором пользователя для процесса – EUID. За исключением выше оговоренного случая, эффективный идентификатор пользователя совпадает с идентификатором пользователя, создавшего процесс.

Системный вызов kill() и команда kill()
Из всех перечисленных ранее в разделе «Аппаратные прерывания (interrupt), исключения (exception), программные прерывания (trap, software interrupt). Их обработка» источников сигнала пользователю доступны только два – команда kill и посылка сигнала процессу с помощью системного вызова kill(). Команда kill обычно используется в следующей форме:
kill [-номер] pid
Здесь pid – это идентификатор процесса, которому посылается сигнал, а номер – номер сигнала, который посылается процессу. Послать сигнал (если у вас нет полномочий суперпользователя) можно только процессу, у которого эффективный идентификатор пользователя совпадает с идентификатором пользователя, посылающего сигнал. Если параметр –номер отсутствует, то посылается сигнал SIGTERM, обычно имеющий номер 15, и реакция на него по умолчанию – завершить работу процесса, который получил сигнал.


Команда kill
Синтаксис команды
kill [-signal] [--] pid
kill -l
Описание команды
Команда kill предназначена для передачи сигнала одному или нескольким специфицированным процессам в рамках полномочий пользователя.
Параметр pid определяет процесс или процессы, которым будут доставляться сигналы. Он может быть задан одним из следующих четырех способов:
  • Число n > 0 – определяет идентификатор процесса, которому будет доставлен сигнал.
  • Число 0 – сигнал будет доставлен всем процессам текущей группы для данного управляющего терминала.
  • Число -1 с предваряющей опцией -- – сигнал будет доставлен (если позволяют полномочия) всем процессам с идентификаторами, большими 1.
  • Число n < 0, где n не равно -1, с предваряющей опцией -- – сигнал будет доставлен всем процессам из группы процессов, идентификатор которой равен -n.

Параметр -signal определяет тип сигнала, который должен быть доставлен, и может задаваться в числовой или символьной форме, например -9 или -SIGKILL. Если этот параметр опущен, процессам по умолчанию посылается сигнал SIGTERM.
Послать сигнал (не имея полномочий суперпользователя) можно только процессу, у которого эффективный идентификатор пользователя совпадает с идентификатором пользователя, посылающего сигнал.
Опция -l используется для получения списка сигналов, существующих в системе в символьной и числовой формах.
Во многих операционных системах предусмотрены еще и дополнительные опции для команды kill.

При использовании системного вызова kill() послать сигнал (не имея полномочий суперпользователя) можно только процессу или процессам, у которых эффективный идентификатор пользователя совпадает с эффективным идентификатором пользователя процесса, посылающего сигнал.


Системный вызов kill()
Прототип системного вызова
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int signal);
Описание системного вызова
Системный вызов kill() предназначен для передачи сигнала одному или нескольким специфицированным процессам в рамках полномочий пользователя.
Послать сигнал (не имея полномочий суперпользователя) можно только процессу, у которого эффективный идентификатор пользователя совпадает с эффективным идентификатором пользователя для процесса, посылающего сигнал.
Аргумент pid описывает, кому посылается сигнал, а аргумент sig – какой сигнал посылается. Этот системный вызов умеет делать много разных вещей, в зависимости от значения аргументов:
  • Если pid > 0 и sig > 0, то сигнал номером sig (если позволяют привилегии) посылается процессу с идентификатором pid.
  • Если pid = 0, а sig > 0, то сигнал с номером sig посылается всем процессам в группе, к которой принадлежит посылающий процесс.
  • Если pid = -1, sig > 0 и посылающий процесс не является процессом суперпользователя, то сигнал посылается всем процессам в системе, для которых идентификатор пользователя совпадает с эффективным идентификатором пользователя процесса, посылающего сигнал.
  • Если pid = -1, sig > 0 и посылающий процесс является процессом суперпользователя, то сигнал посылается всем процессам в системе, за исключением системных процессов (обычно всем, кроме процессов с pid = 0 и pid = 1).
  • Если pid < 0, но не –1, sig > 0, то сигнал посылается всем процессам из группы, идентификатор которой равен абсолютному значению аргумента pid (если позволяют привилегии).
  • Если значение sig = 0, то производится проверка на ошибку, а сигнал не посылается, так как все сигналы имеют номера > 0. Это можно использовать для проверки правильности аргумента pid (есть ли в системе процесс или группа процессов с соответствующим идентификатором).

Возвращаемое значение
Системный вызов возвращает 0 при нормальном завершении и –1 при ошибке.

Изучение особенностей получения терминальных сигналов текущей и фоновой группой процессов
Возьмем тривиальную программу 13–14-1.c, в которой процесс порождает ребенка, и они оба зацикливаются, и на ее основе проиллюстрируем сказанное выше.
/* Тривиальная программа для иллюстрации
понятий группа процессов, сеанс, фоновая
группа и т.д. */
#include <unistd.h>
int main(void){
(void)fork();
while(1);
return 0;
}
Для этого будем использовать команду ps с опциями –e и j, которая позволяет получить информацию обо всех процессах в системе и узнать их идентификаторы, идентификаторы групп процессов и сеансов, управляющий терминал сеанса и к какой группе процессов он приписан. Набрав команду “ps –e j” (обратите внимание на наличие пробела между буквами e и j!!!) мы получим список всех процессов в системе. Колонка PID содержит идентификаторы процессов, колонка PGID – идентификаторы групп, к которым они принадлежат, колонка SID – идентификаторы сеансов, колонка TTY – номер соответствующего управляющего терминала, колонка TPGID (может присутствовать не во всех версиях UNIX, но в Linux есть) – к какой группе процессов приписан управляющий терминал.
Наберите тривиальную программу, откомпилируйте ее и запустите на исполнение (лучше всего из-под оболочки Midnight Commander – mc). Запустив команду “ps –e j“ с другого экрана, проанализируйте значения идентификаторов группы процессов, сеансов, прикрепления управляющего терминала, текущей и фоновой групп. Убедитесь, что тривиальные процессы относятся к текущей группе сеанса. Проверьте реакцию текущей группы на сигналы SIGINT – нажатие клавиш <CTRL> и <C> – и SIGQUIT – нажатие клавиш <CTRL> и <4>.
Запустите теперь тривиальную программу в фоновом режиме, например командой “a.out &“. Проанализируйте значения идентификаторов группы процессов, сеансов, прикрепления управляющего терминала, текущей и фоновой групп. Убедитесь, что тривиальные процессы относятся к фоновой группе сеанса. Проверьте реакцию фоновой группы на сигналы SIGINT – нажатие клавиш <CTRL> и <C> – и SIGQUIT – нажатие клавиш <CTRL> и <4>. Ликвидируйте тривиальные процессы с помощью команды kill.
Изучение получения сигнала SIGHUP процессами при завершении лидера сеанса
Возьмите снова тривиальную программу из предыдущего раздела и запустите ее на исполнение из-под Midnight Commander в текущей группе. Проанализировав значения идентификаторов группы процессов, сеансов, прикрепления управляющего терминала, текущей и фоновой групп, ликвидируйте лидера сеанса для тривиальных процессов. Убедитесь, что все процессы в текущей группе этого сеанса прекратили свою работу.
Запустите тривиальную программу в фоновом режиме. Снова удалите лидера сеанса для тривиальных процессов. Убедитесь, что фоновая группа продолжает работать. Ликвидируйте тривиальные процессы.
Системный вызов signal(). Установка собственного обработчика сигнала
Одним из способов изменения поведения процесса при получении сигнала в операционной системе UNIX является использование системного вызова signal().


Системный вызов signal()
Прототип системного вызова
#include <signal.h>
void (*signal (int sig,
void (*handler) (int)))(int);
Описание системного вызова
Системный вызов signal служит для изменения реакции процесса на какой-либо сигнал. Хотя прототип системного вызова выглядит довольно пугающе, ничего страшного в нем нет. Приведенное выше описание можно словесно изложить следующим образом:
функция signal, возвращающая указатель на функцию с одним параметром типа int, которая ничего не возвращает, и имеющая два параметра: параметр sig типа int и параметр handler, служащий указателем на ничего не возвращающую функцию с одним параметром типа int.
Параметр sig – это номер сигнала, обработку которого предстоит изменить.
Параметр handler описывает новый способ обработки сигнала – это может быть указатель на пользовательскую функцию – обработчик сигнала, специальное значение SIG_DFL или специальное значение SIG_IGN. Специальное значение SIG_IGN используется для того, чтобы процесс игнорировал поступившие сигналы с номером sig, специальное значение SIG_DFL – для восстановления реакции процесса на этот сигнал по умолчанию.
Возвращаемое значение
Системный вызов возвращает указатель на старый способ обработки сигнала, значение которого можно использовать для восстановления старого способа в случае необходимости.

Этот системный вызов имеет два параметра: один из них задает номер сигнала, реакцию процесса на который требуется изменить, а второй определяет, как именно мы собираемся ее менять. Для первого варианта реакции процесса на сигнал (см. раздел «Понятие сигнала. Способы возникновения сигналов и виды их обработки») – его игнорирования – применяется специальное значение этого параметра SIG_IGN. Например, если требуется игнорировать сигнал SIGINT, начиная с некоторого места работы программы, в этом месте программы мы должны употребить конструкцию
(void) signal(SIGINT, SIG_IGN);
Для второго варианта реакции процесса на сигнал – восстановления его обработки по умолчанию – применяется специальное значение этого параметра SIG_DFL. Для третьего варианта реакции процесса на сигнал в качестве значения параметра подставляется указатель на пользовательскую функцию обработки сигнала, которая должна иметь прототип вида
void *handler(int);
Ниже приведен пример скелета конструкции для пользовательской обработки сигнала SIGHUP.
void *my_handler(int nsig) {
    <обработка сигнала>
}
int main() {
    ...
    (void)signal(SIGHUP, my_handler);
    ...
}
В качестве значения параметра в пользовательскую функцию обработки сигнала (в нашем скелете – параметр nsig) передается номер возникшего сигнала, так что одна и та же функция может быть использована для обработки нескольких сигналов.
Прогон программы, игнорирующей сигнал SIGINT
Рассмотрим следующую программу – 13–14-2.c:
/* Программа, игнорирующая сигнал SIGINT */
#include <signal.h>
int main(void){
    /* Выставляем реакцию процесса на сигнал
       SIGINT на игнорирование */
    (void)signal(SIGINT, SIG_IGN);
    /*Начиная с этого места, процесс будет
      игнорировать возникновение сигнала
      SIGINT */
    while(1);
    return 0;
}
Эта программа не делает ничего полезного, кроме переустановки реакции на нажатие клавиш <CTRL> и <C> на игнорирование возникающего сигнала и своего бесконечного зацикливания. Наберите, откомпилируйте и запустите эту программу, убедитесь, что на нажатие клавиш <CTRL> и <C> она не реагирует, а реакция на нажатие клавиш <CTRL> и <4> осталась прежней.
Модификация предыдущей программы для игнорирования сигналов SIGINT и SIGQUIT
Модифицируйте программу из предыдущего раздела так, чтобы она перестала реагировать и на нажатие клавиш <CTRL> и <4>. Откомпилируйте и запустите ее, убедитесь в отсутствии ее реакций на внешние раздражители. Снимать программу придется теперь с другого терминала командой kill.


Прогон программы с пользовательской обработкой сигнала SIGINT
Рассмотрим теперь другую программу – 13–14-3.c:
/* Программа с пользовательской обработкой
сигнала SIGINT */
#include <signal.h>
#include <stdio.h>
/* Функция my_handler – пользовательский
обработчик сигнала */
void my_handler(int nsig){
printf("Receive signal %d,
CTRL-C pressed\n", nsig);
}
int main(void){
/* Выставляем реакцию процесса на
сигнал SIGINT */
(void)signal(SIGINT, my_handler);
/*Начиная с этого места, процесс будет
печатать сообщение о возникновении
сигнала SIGINT */
while(1);
return 0;
}
Эта программа отличается от программы из раздела «Прогон программы, игнорирующей сигнал SIGINT» тем, что в ней введена обработка сигнала SIGINT пользовательской функцией. Наберите, откомпилируйте и запустите эту программу, проверьте ее реакцию на нажатие клавиш <CTRL> и <C> и на нажатие клавиш <CTRL> и <4>.
Модификация предыдущей программы для пользовательской обработки сигналов SIGINT и SIGQUIT
Модифицируйте программу из предыдущего раздела так, чтобы она печатала сообщение и о нажатии клавиш <CTRL> и <4>. Используйте одну и ту же функцию для обработки сигналов SIGINT и SIGQUIT. Откомпилируйте и запустите ее, проверьте корректность работы. Снимать программу также придется с другого терминала командой kill.
Восстановление предыдущей реакции на сигнал
До сих пор в примерах мы игнорировали значение, возвращаемое системным вызовом signal(). На самом деле этот системный вызов возвращает указатель на предыдущий обработчик сигнала, что позволяет восстанавливать переопределенную реакцию на сигнал. Рассмотрим пример программы 13—14-4.c, возвращающей первоначальную реакцию на сигнал SIGINT после 5 пользовательских обработок сигнала.
Наберите, откомпилируйте программу и запустите ее на исполнение.
Сигналы SIGUSR1 и SIGUSR2. Использование сигналов для синхронизации процессов
В операционной системе UNIX существует два сигнала, источниками которых могут служить только системный вызов kill() или команда kill, – это сигналы SIGUSR1 и SIGUSR2. Обычно их применяют для передачи информации о происшедшем событии от одного пользовательского процесса другому в качестве сигнального средства связи.
В материалах семинара 5 (раздел «Написание, компиляция и запуск программы для организации двунаправленной связи между родственными процессами через pipe»), когда рассматривалась связь родственных процессов через pipe, речь шла о том, что pipe является однонаправленным каналом связи, и что для организации связи через один pipe в двух направлениях необходимо задействовать механизмы взаимной синхронизации процессов. Организуйте двустороннюю поочередную связь процесса-родителя и процесса-ребенка через pipe, используя для синхронизации сигналы SIGUSR1 и SIGUSR2, модифицировав программу из раздела. «Прогон программы для организации однонаправленной связи между родственными процессами через pipe» семинара 5.
Задача повышенной сложности: организуйте побитовую передачу целого числа между двумя процессами, используя для этого только сигналы SIGUSR1 и SIGUSR2.
При реализации нитей исполнения в операционной системе Linux (см. семинары 6–7, начиная с раздела «Понятие о нити исполнения (thread) в UNIX. Идентификатор нити исполнения. Функция pthread_self()») сигналы SIGUSR1 и SIGUSR2 используются для организации синхронизации между процессами, представляющими нити исполнения, и процессом-координатором в служебных целях. Поэтому пользовательские программы, применяющие в своей работе нити исполнения, не могут задействовать сигналы SIGUSR1 и SIGUSR2.
Завершение порожденного процесса. Системный вызов waitpid(). Сигнал SIGCHLD
В материалах семинаров 3–4 (раздел «Завершение процесса. Функция exit()») при изучении завершения процесса говорилось о том, что если процесс-ребенок завершает свою работу прежде процесса-родителя, и процесс-родитель явно не указал, что он не заинтересован в получении информации о статусе завершения процесса-ребенка, то завершившийся процесс не исчезает из системы окончательно, а остается в состоянии закончил исполнение (зомби-процесс) либо до завершения процесса-родителя, либо до того момента, когда родитель соблаговолит получить эту информацию.
Для получения такой информации процесс-родитель может воспользоваться системным вызовом waitpid() или его упрощенной формой wait(). Системный вызов waitpid() позволяет процессу-родителю синхронно получить данные о статусе завершившегося процесса-ребенка либо блокируя процесс-родитель до завершения процесса-ребенка, либо без блокировки при его периодическом вызове с опцией WNOHANG. Эти данные занимают 16 бит и в рамках нашего курса могут быть расшифрованы следующим образом:
  • Если процесс завершился при помощи явного или неявного вызова функции exit(), то данные выглядят так (старший бит находится слева)     :
  • Если процесс был завершен сигналом, то данные выглядят так (старший бит находится слева):

 

Каждый процесс-ребенок при завершении работы посылает своему процессу-родителю специальный сигнал SIGCHLD, на который у всех процессов по умолчанию установлена реакция «игнорировать сигнал». Наличие такого сигнала совместно с системным вызовом waitpid() позволяет организовать асинхронный сбор информации о статусе завершившихся порожденных процессов процессом-родителем.


Системные вызовы wait() и waitpid()
Прототипы системных вызовов
#include <sys/types.h>
#include <wait.h>
pid_t waitpid(pid_t pid, int *status,
int options);
pid_t wait(int *status);
Описание системных вызовов
Это описание не является полным описанием системных вызовов, а адаптировано применительно к нашему курсу. Для получения полного описания обращайтесь к UNIX Manual.
Системный вызов waitpid() блокирует выполнение текущего процесса до тех пор, пока либо не завершится порожденный им процесс, определяемый значением параметра pid, либо текущий процесс не получит сигнал, для которого установлена реакция по умолчанию «завершить процесс» или реакция обработки пользовательской функцией. Если порожденный процесс, заданный параметром pid, к моменту системного вызова находится в состоянии закончил исполнение, то системный вызов возвращается немедленно без блокирования текущего процесса.
Параметр pid определяет порожденный процесс, завершения которого дожидается процесс-родитель, следующим образом:
  • Если pid > 0 ожидаем завершения процесса с идентификатором pid.
  • Если pid = 0, то ожидаем завершения любого порожденного процесса в группе, к которой принадлежит процесс-родитель.
  • Если pid = -1, то ожидаем завершения любого порожденного процесса.
  • Если pid < 0, но не –1, то ожидаем завершения любого порожденного процесса из группы, идентификатор которой равен абсолютному значению параметра pid.

Параметр options в нашем курсе может принимать два значения: 0 и WNOHANG. Значение WNOHANG требует немедленного возврата из вызова без блокировки текущего процесса в любом случае.
Если системный вызов обнаружил завершившийся порожденный процесс, из числа специфицированных параметром pid, то этот процесс удаляется из вычислительной системы, а по адресу, указанному в параметре status, сохраняется информация о статусе его завершения. Параметр status может быть задан равным NULL, если эта информация не имеет для нас значения.
При обнаружении завершившегося процесса системный вызов возвращает его идентификатор. Если вызов был сделан с установленной опцией WNOHANG, и порожденный процесс, специфицированный параметром pid, существует, но еще не завершился, системный вызов вернет значение 0. Во всех остальных случаях он возвращает отрицательное значение. Возврат из вызова, связанный с возникновением обработанного пользователем сигнала, может быть в этом случае идентифицирован по значению системной переменной errno == EINTR, и вызов может быть сделан снова.
Системный вызов wait является синонимом для системного вызова waitpid со значениями параметров pid = -1, options = 0.

Используя системный вызов signal(), мы можем явно установить игнорирование этого сигнала (SIG_IGN), тем самым проинформировав систему, что нас не интересует, каким образом завершатся порожденные процессы. В этом случае зомби-процессов возникать не будет, но и применение системных вызовов wait() и waitpid() будет запрещено.
Прогон программы для иллюстрации обработки сигнала SIGCHLD
Для закрепления материала рассмотрим пример программы 13—14-5.c с асинхронным получением информации о статусе завершения порожденного процесса. В этой программе родитель порождает два процесса. Один из них завершается с кодом 200, а второй зацикливается. Перед порождением процессов родитель устанавливает обработчик прерывания для сигнала SIGCHLD, а после их порождения уходит в бесконечный цикл. В обработчике прерывания вызывается waitpid() для любого порожденного процесса. Так как в обработчик мы попадаем, когда какой-либо из процессов завершился, системный вызов не блокируется, и мы можем получить информацию об идентификаторе завершившегося процесса и причине его завершения. Откомпилируйте программу и запустите ее на исполнение. Второй порожденный процесс завершайте с помощью команды kill с каким-либо номером сигнала. Родительский процесс также будет необходимо завершать командой kill.

Возникновение сигнала SIGPIPE при попытке записи в pipe или FIFO, который никто не собирается читать
В материалах семинара 5 (раздел «Особенности поведения вызовов read() и write() для pip'а») при обсуждении работы с pip'ами и FIFO мы говорили, что для них системные вызовы read() и write() имеют определенные особенности поведения. Одной из таких особенностей является получение сигнала SIGPIPE процессом, который пытается записывать информацию в pipe или в FIFO в том случае, когда читать ее уже некому (нет ни одного процесса, который держит соответствующий pipe или FIFO открытым для чтения). Реакция по умолчанию на этот сигнал – прекратить работу процесса. Теперь мы уже можем написать корректную обработку этого сигнала пользователем, например, для элегантного прекращения работы пишущего процесса. Однако для полноты картины необходимо познакомиться с особенностями поведения некоторых системных вызовов при получении процессом сигналов во время их выполнения.
По ходу нашего курса мы представили читателям ряд системных вызовов, которые могут во время выполнения блокировать процесс. К их числу относятся системный вызов open() при открытии FIFO, системные вызовы read() и write() при работе с pip'ами и FIFO, системные вызовы msgsnd() и msgrcv() при работе с очередями сообщений, системный вызов semop() при работе с семафорами и т.д. Что произойдет с процессом, если он, выполняя один из этих системных вызовов, получит какой-либо сигнал? Дальнейшее поведение процесса зависит от установленной для него реакции на этот сигнал.

  • Если реакция на полученный сигнал была «игнорировать сигнал» (независимо от того, установлена она по умолчанию или пользователем с помощью системного вызова signal()), то поведение процесса не изменится.
  • Если реакция на полученный сигнал установлена по умолчанию и заключается в прекращении работы процесса, то процесс перейдет в состояние закончил исполнение.
  • Если реакция процесса на сигнал заключается в выполнении пользовательской функции, то процесс выполнит эту функцию (если он находился в состоянии ожидание, он попадет в состояние готовность и затем в состояние исполнение) и вернется из системного вызова с констатацией ошибочной ситуации (некоторые системные вызовы позволяют операционной системе после выполнения обработки сигнала вновь вернуть процесс в состояние ожидания). Отличить такой возврат от действительно ошибочной ситуации можно с помощью значения системной переменной errno, которая в этом случае примет значение EINTR (для вызова write и сигнала SIGPIPE соответствующее значение в порядке исключения будет EPIPE).

После этого краткого обсуждения становится до конца ясно, как корректно обработать ситуацию «никто не хотел прочитать» для системного вызова write(). Чтобы пришедший сигнал SIGPIPE не завершил работу нашего процесса по умолчанию, мы должны его обработать самостоятельно (функция-обработчик при этом может быть и пустой!). Но этого мало. Поскольку нормальный ход выполнения системного вызова был нарушен сигналом, мы вернемся из него с отрицательным значением, которое свидетельствует об ошибке. Проанализировав значение системной переменной errno на предмет совпадения со значением EPIPE, мы можем отличить возникновение сигнала SIGPIPE от других ошибочных ситуаций (неправильные значения параметров и т.д.) и грациозно продолжить работу программы.
Понятие о надежности сигналов. POSIX функции для работы с сигналами
Основным недостатком системного вызова signal() является его низкая надежность.
Во многих вариантах операционной системы UNIX установленная при его помощи обработка сигнала пользовательской функцией выполняется только один раз, после чего автоматически восстанавливается реакция на сигнал по умолчанию. Для постоянной пользовательской обработки сигнала необходимо каждый раз заново устанавливать реакцию на сигнал прямо внутри функции-обработчика.
В системных вызовах и пользовательских программах могут существовать критические участки, на которых процессу недопустимо отвлекаться на обработку сигналов. Мы можем выставить на этих участках реакцию «игнорировать сигнал» с последующим восстановлением предыдущей реакции, но если сигнал все-таки возникнет на критическом участке, то информация о его возникновении будет безвозвратно потеряна.
Наконец, последний недостаток связан с невозможностью определения количества сигналов одного и того же типа, поступивших процессу, пока он находился в состоянии готовность. Сигналы одного типа в очередь не ставятся! Процесс может узнать о том, что сигнал или сигналы определенного типа были ему переданы, но не может определить их количество. Этот недостаток мы можем проиллюстрировать, слегка изменив программу с асинхронным получением информации о статусе завершившихся процессов, рассмотренную нами ранее в разделе «Изучение особенностей получения терминальных сигналов текущей и фоновой группой процессов». Пусть в новой программе 13–14-6.c процесс-родитель порождает в цикле пять новых процессов, каждый из которых сразу же завершается со своим собственным кодом, после чего уходит в бесконечный цикл.
Листинг 13-14.6. Программа (13–14-6.c) для иллюстрации ненадежности сигналов.
Сколько сообщений о статусе завершившихся детей мы ожидаем получить? Пять! А сколько получим? It depends... Откомпилируйте, прогоните и посчитайте.
Последующие версии System Y и BSD пытались устранить эти недостатки собственными средствами. Единый способ более надежной обработки сигналов появился с введением POSIX стандарта на системные вызовы UNIX. Набор функций и системных вызовов для работы с сигналами был существенно расширен и построен таким образом, что позволял временно блокировать обработку определенных сигналов, не допуская их потери. Однако проблема, связанная с определением количества пришедших сигналов одного типа, по-прежнему остается актуальной. (Надо отметить, что подобная проблема существует на аппаратном уровне и для внешних прерываний. Процессор зачастую не может определить, какое количество внешних прерываний с одним номером возникло, пока он выполнял очередную команду.)
Рассмотрение POSIX сигналов выходит за рамки нашего курса. Желающие могут самостоятельно просмотреть описания функций и системных вызовов sigemptyset(), sigfillset(), sigaddset(), sigdelset(), sigismember(), sigaction(), sigprocmask(), sigpending(), sigsuspend() в UNIX Manual.
Задача повышенной сложности: модифицируйте обработку сигнала в программе из этого раздела, не применяя POSIX-сигналы, так, чтобы процесс-родитель все-таки сообщал о статусе всех завершившихся процессов-детей.

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика