МИНИСТЕРСТВО ОБРАЗОВАНИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ

Московский Государственный институт электроники и математики

(Технический университет)

 

 

 

Кафедра Управление и информатика в технических системах

 

 

 

ПРОЦЕССЫ И СИГНАЛЫ ОС UNIX

 

 

 

Методические указания

к лабораторной работе № 3

по курсу "Системное программное обеспечение"

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Москва 2002

 

Составители:       канд. техн. наук В.Э. Карпов

ст. преподаватель И.П. Карпова

 

 

 

 

 

УДК 681.3

Процессы и сигналы ОС UNIX: Метод. указания к лабораторной работе №3 по курсу " Системное программное обеспечение " / Моск. гос. ин-т электроники и математики; Сост.: В.Э. Карпов, И. П. Карпова. М., 2002. – 27с.

 

 

 

 

Лабораторная работа № 3 посвящена изучению процессов и сигналов ОС UNIX как средства организации межпроцессного взаимодействия, приобретению знаний и навыков написания программ работы со взаимодействующими процессами. Приведены системные вызовы для работы с процессами и сигналами и примеры программ.

Лабораторная работа выполняется в объеме 3 часов.

Методические указания к лабораторным работам являются составной частью учебно-методического комплекса по дисциплине “Системное программное обеспечение”, изучаемой студентами специальности 21.01 “Управление и информатика в технических системах”.

 

 

 

 

 

ISBN

ОГЛАВЛЕНИЕ

1. ТЕОРЕТИЧЕСКИЕ СВЕДЕНИЯ 1.1. Выполнение процесса
1.2. Контекст процесса
1.3. Состояния процесса
2. СИГНАЛЫ
2.1. Причины возникновения сигналов
2.2. Обработка сигналов
2.3. Группы процессов
3. СИСТЕМНЫЕ ВЫЗОВЫ
3.1. Системные вызовы для работы с процессами
3.2. Системные вызовы для работы с сигналами
4. ПРИМЕРЫ ПРОГРАММ
5. ВЫПОЛНЕНИЕ ЛАБОРАТОРНОЙ РАБОТЫ №3
6. ЗАДАНИЯ НА ЛАБОРАТОРНУЮ РАБОТУ №3
БИБЛИОГРАФИЧЕСКИЙ СПИСОК

 

Цель лабораторной работы №3 состоит в изучении принципов организации работы ОС UNIX как многозадачной операционной системы и приобретении навыков написания программ работы со взаимодействующими процессами.

1. ТЕОРЕТИЧЕСКИЕ СВЕДЕНИЯ

Работу ОС UNIX можно представить в виде функционирования множества взаимосвязанных процессов.

Процесс – это задание в ходе его выполнения. Он представляет собой исполняемый образ программы, включающий отображение в памяти исполняемого файла, полученного в ходе компиляции, стек, код и данные библиотек, а также ряд структур данных ядра, необходимых для управления процессом. Выполнение процесса заключается в точном следовании замкнутому набору инструкций, который не передает управление набору инструкций другого процесса; он считывает и записывает информацию в раздел данных и в стек, но ему недоступны данные и стеки других процессов.

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

1.1. Выполнение процесса

Выполнение процесса осуществляется ядром системы. Подсистема управления процессами отвечает за синхронизацию процессов, взаимодействие процессов, распределение памяти и планирование выполнения процессов.

С практической точки зрения процесс в системе UNIX является объектом, создаваемым в результате выполнения системного вызова fork(). Каждый процесс, за исключением нулевого, порождается в результате запуска другим процессом операции fork(). Процесс, запустивший операцию fork(), называется родительским, а вновь созданный процесс – порожденным. Каждый процесс имеет одного родителя, но может породить много процессов. Ядро системы идентифицирует каждый процесс по его номеру, который называется идентификатором процесса (PID). Нулевой процесс является особенным процессом, который создается "вручную" в результате загрузки системы. Процесс 1, известный под именем init, является предком любого другого процесса в системе и связан с каждым процессом. Пользователь, транслируя исходный текст программы, создает исполняемый файл, который состоит из нескольких частей:

Ядро загружает исполняемый файл в память при выполнении системной операции exec, при этом загруженный процесс состоит по меньшей мере из трех частей, так называемых областей: текста, данных и стека. Области текста и данных соответствуют секциям текста и bss-данных исполняемого файла, а область стека создается автоматически и ее размер динамически устанавливается ядром системы во время выполнения.

Процесс в системе UNIX может выполняться в двух режимах – режиме ядра или режиме задачи. В режиме задачи процесс выполняет инструкции прикладной программы, системные структуры данных ему недоступны.

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

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

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

Когда процесс запускает системную операцию exec, ядро системы выделяет области оперативной памяти (ОП) под новый процесс и освобождает области памяти вызывающего процесса. Если процесс запускает операцию fork, ядро удваивает размер адресного пространства процесса, позволяя новому и старому процессам совместно использовать области ОП, когда это возможно, и, с другой стороны, производя физическое копирование. Если процесс запускает операцию exit, ядро освобождает области, которые использовались процессом. На Рис.1 изображены информационные структуры, связанные с запуском процесса позволяющие процессам разделять области ОП.

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

Рис. 1. Системные структуры для разделения областей памяти между процессами

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

Процессы предъявляют различные требования к системе с точки зрения их планирования и общей производительности. Можно выделить три основных класса приложений:

В основе планирования выполнения процессов лежат два понятия: квант времени и приоритет. Каждый процесс имеет два атрибута приоритета: текущий (на основании которого осуществляется планирование) и относительный, который задается при порождении процесса и влияет на текущий. Номера приоритетов разбиваются на несколько групп: для процессов в режиме задачи, в режиме ядра, для процессов реального времени (группы указаны в соответствии с повышением приоритета).

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

Выполнение процесса может быть прервано:

  1. планировщиком процессов по истечении временного кванта или в том случае, если в очереди готовых к исполнению процессов есть процесс с более высоким приоритетом.
  2. ядром системы, если процесс ожидает недоступного ресурса или окончания длительной операции ввода/вывода.

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

1.2. Контекст процесса

Под контекстом процесса понимается вся информация, необходимая для описания процесса. Контекст процесса состоит из нескольких частей:

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

Принято говорить, что при запуске процесса система исполняется в контексте процесса. Когда ядро системы решает запустить другой процесс, оно выполняет переключение контекста с тем, чтобы система исполнялась в контексте другого процесса. Ядро осуществляет переключение контекста только при определенных условиях (см. п.1.3). Выполняя переключение контекста, ядро сохраняет информацию, достаточную для того, чтобы позднее переключиться вновь на прерванный процесс и возобновить его выполнение.

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

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

1.3. Состояния процесса

Время жизни процесса можно разделить на несколько состояний, каждое из которых имеет определенные характеристики, описывающие процесс. На рис. 2 показаны состояния, в которых процесс может находиться с момента создания до завершения выполнения:

  1. Процесс выполняется в режиме задачи. Процессор выполняет прикладные инструкции данного процесса.
  2. Процесс выполняется в режиме ядра. Процессор выполняет системные инструкции ядра ОС от имени процесса.
  3. Процесс не выполняется, но готов к выполнению, находится в очереди готовых к исполнению процессов и ждет, когда планировщик выберет его. Естественно, в этом состоянии может находиться много процессов, и алгоритм планирования устанавливает, какой из процессов будет выполняться следующим.
  4. Процесс приостановлен ("спит"). Процесс "впадает в сон", когда он не может больше продолжать выполнение, например, когда ждет завершения ввода-вывода или освобождения занятого ресурса.
  5. Процесс возвращается из режима ядра в режим задачи, но ядро прерывает его и производит переключение контекста для запуска более высокоприоритетного процесса.
  6. Процесс только что создан вызовом fork и находится в переходном состоянии: он существует, но не готов к запуску и не находится в состоянии сна.
  7. Процесс выполнил системный вызов exit и перешел в состояние "зомби". Как такового процесса не существует, но осталась запись с системной таблице процессов, содержащая код возврата и временную статистику, доступные для родительского процесса.

Поскольку процессор в каждый момент времени выполняет только один процесс, в состояниях 1 и 2 на однопроцессорной машине может находиться самое большее один процесс.

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

Рис. 2. Состояния процесса и переходы между ними

В режиме разделения времени может выполняться одновременно несколько процессов, и все они могут одновременно работать в режиме ядра. Если им разрешить свободно выполняться в режиме ядра, то они могут испортить глобальные информационные структуры, принадлежащие ядру. Запрещая произвольное переключение контекста и управляя возникновением событий, ядро защищает свою целостность. Ядро разрешает переключение контекста только тогда, когда процесс переходит из состояния "запуск в режиме ядра" в состояние "сна в памяти". Процессы, запущенные в режиме ядра, не могут быть выгружены другими процессами; поэтому иногда говорят, что ядро невыгружаемо, при этом процессы, находящиеся в режиме задачи, могут выгружаться системой. Ядро поддерживает целостность своих информационных структур, поскольку оно невыгружаемо, таким образом решая проблему "взаимного исключения" – обеспечения того, что критические секции программы выполняются в каждый момент времени в рамках самое большее одного процесса.

2. СИГНАЛЫ

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

Сигналы описаны в файле <signal.h>, каждому из них ставится в соответствие мнемоническое обозначение. Количество и семантика сигналов зависят от версии ОС UNIX.

В UNIX System V сигналы имеют номера от 1 до 19:

#define NSIG            20

#define SIGHUP      1      /* разрыв связи */

#define SIGINT      2      /* прерывание */

#define SIGQUIT      3      /* аварийный выход */

#define SIGILL      4      /* неверная машинная инструкция */

#define SIGTRAP      5      /* прерывание-ловушка */

#define SIGIOT      6      /* прерывание ввода-вывода */

#define SIGEMT      7      /* программное прерывание EMT */

#define SIGFPE      8      /* авария при выполнении операции с

/* плавающей точкой */

#define SIGKILL      9      /* уничтожение процесса */

#define SIGBUS      10      /* ошибка шины */

#define SIGSEGV      11      /* нарушение сегментации */

#define SIGSYS      12      /* ошибка выполнения системного вызова */

#define SIGPIPE      13      /* запись в канал есть, чтения нет */

#define SIGALRM      14      /* прерывание от таймера */

#define SIGTERM      15      /* программ. сигнал завершения от kill */

#define SIGUSR1      16      /* определяется пользователем */

#define SIGUSR2      17      /* определяется пользователем */

#define SIGCLD      18      /* процесс-потомок завершился */

#define SIGPWR      19      /* авария питания */

#define SIG_DFL      (int(*)())0      /* все установки “по умолчанию” */

#define SIG_IGN      (int(*)())1      /* игнорировать этот сигнал */

В BSD UNIX сигналов больше и описываются они следующим образом:

#define SIGHUP      1      /* разрыв связи */

#define SIGINT      2      /* прерывание */

#define SIGQUIT      3      /* аварийный выход */

#define SIGILL      4      /* неверная машинная инструкция */

#define SIGTRAP      5      /* прерывание-ловушка */

#define SIGIOT      6      /* прерывание ввода-вывода */

#define SIGABRT      6      /* используется как ABORT */

#define SIGEMT      7      /* программное прерывание EMT */

#define SIGFPE      8      /* авария при выполнении операции с плав. точкой */

#define SIGKILL      9      /* уничтожение процесса (не может быть

перехвачен или проигнорирован */

#define SIGBUS      10      /* ошибка шины */

#define SIGSEGV      11      /* нарушение сегментации */

#define SIGSYS      12      /* неправильный аргумент системного вызова */

#define SIGPIPE      13      /* запись в канал есть, чтения нет */

#define SIGALRM      14      /* прерывание от таймера */

#define SIGTERM      15      /* программ. сигнал завершения от kill */

#define SIGUSR1      16      /* определяется пользователем */

#define SIGUSR2      17      /* определяется пользователем */

#define SIGCLD      18      /* изменение статуса потомка

(завершение процесса-потомка) */

#define SIGCHLD      18      /* альтернатива для SIGCLD (POSIX) */

#define SIGPWR      19      /* авария питания */

#define SIGWINCH      20      /* изменение размера окна */

#define SIGURG      21      /* urgent socket condition */

#define SIGPOLL      22      /* pollable event occured */

#define SIGIO            SIGPOLL      /* socket I/O possible (SIGPOLL alias) */

#define SIGSTOP      23      /* стоп (не может быть перехвачен или проигнорирован)*/

#define SIGTSTP      24      /* требование остановки от терминала */

#define SIGCONT      25      /* остановить процесс с возможностью продолжения */

#define SIGTTIN      26      /* скрытая попытка чтения с терминала */

#define SIGTTOU      27      /* скрытая попытка записи на терминал */

#define SIGVTALRM      28      /* виртуальное время истекло */

#define SIGPROF      29      /* время конфигурирования истекло */

#define SIGXCPU      30      /* превышен лимит ЦП */

#define SIGXFSZ      31      /* превышен лимит размера файла */

#define SIGWAITING 32      /* process's lwps заблокирован */

#define SIGLWP      33      /* спецсигнал (used by thread library) */

#define SIGFREEZE      34      /* спецсигнал, используемый процессором*/

#define SIGTHAW      35            /* спецсигнал, используемый процессором*/

#define _SIGRTMIN      36      /* первый (с высшим приоритетом)

сигнал реального времени */

#define _SIGRTMAX      43      /* последний (с низшим приоритетом)

сигнал реального времени */

#define SIG_DFL      (void(*)())0      /* все установки “по умолчанию” */

#define SIG_IGN      (void(*)())0      /* игнорировать этот сигнал */

Примечание: причины возникновения сигналов для различных версий могут отличаться; первоначально они были обусловлены архитектурными особенностями ЭВМ PDP-11.

2.1. Причины возникновения сигналов

В версии System V UNIX возникновение сигналов можно классифицировать следующим образом:

Посылка сигналов производится процессами - друг другу, с помощью функции kill, - или ядром. Для каждого процесса определен бинарный вектор, длина которого равна количеству сигналов в системе. При получении процессом сигнала I соответствующий i-й разряд этого вектора становится равным 1. Каждому сигналу соответствует адрес функции, которая будет вызвана для обработки данного сигнала.

2.2. Обработка сигналов

Ядро обрабатывает сигналы в контексте того процесса, который получает их, поэтому чтобы обработать сигналы, нужно запустить процесс.

Существует три способа обработки сигналов:

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

Обрабатывая сигнал, ядро определяет тип сигнала и очищает (гасит) разряд в записи таблицы процессов, соответствующий данному типу сигнала и установленный в момент получения сигнала процессом. Таким образом, когда процесс получает любой неигнорируемый им сигнал (за исключением SIGILL и SIGTRAP), ОС UNIX автоматически восстанавливает реакцию “по умолчанию” на всякое последующее получение этого сигнала.

Замечание 1: Если необходима многократная обработка одного и того же сигнала, процесс должен каждый раз осуществлять системный вызов signal для установления требуемой реакции на данный сигнал.

Замечание 2: Процесс не в состоянии узнать, сколько однотипных сигналов им было получено. В том случае, если процесс не успевает обработать все поступившие сигналы, происходит потеря информации.

Если функции обработки сигнала присвоено значение по умолчанию, ядро в отдельных случаях перед завершением процесса сбрасывает на внешний носитель (дампирует) образ процесса в памяти. Дампирование удобно для программистов тем, что позволяет установить причину завершения процесса и посредством этого вести отладку программ. Ядро дампирует состояние памяти при поступлении сигналов, которые сообщают о каких-нибудь ошибках в выполнении процессов, как например, попытка исполнения запрещенной команды или обращение к адресу, находящемуся за пределами виртуального адресного пространства процесса. Ядро не дампирует состояние памяти, если сигнал не связан с программной ошибкой, за исключением внешнего сигнала о выходе (quit), обычно вызываемого одновременным нажатием клавиш Ctrl+|.

Если процесс получает сигнал SIGINT, который было решено игнорировать (signal(SIGINT,SIG_IGN)), выполнение процесса продолжается так, словно сигнала и не было. Поскольку ядро не сбрасывает значение соответствующего поля, свидетельствующего о необходимости игнорирования сигнала данного типа, то когда сигнал поступит вновь, процесс опять не обратит на него внимание.

В том случае, если процесс получает сигнал, реагирование на который установлено системным вызовом signal, сразу по возвращении процесса в режим задачи выполняется заранее условленное действие, описанное в вызове signal. После выполнения функции обработки сигнала управление будет передано на то место в программе пользователя, где было произведено обращение к системной функции или произошло прерывание.

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

2.3. Группы процессов

Несмотря на то, что в системе UNIX процессы идентифицируются уникальным кодом (PID), системе иногда приходится использовать для идентификации процессов номер “группы”, в которую они входят. Например, процессы, имеющие общего предка в лице регистрационного интерпретатора shell, взаимосвязаны, и поэтому когда пользователь нажимает клавиши “delete” или “break”, или когда терминальная линия “зависает”, все эти процессы получают соответствующие сигналы. Ядро использует код группы процессов для идентификации группы взаимосвязанных процессов, которые при наступлении определенных событий должны получать общий сигнал. Код группы запоминается в таблице процессов. При выполнении функции fork процесс-потомок наследует код группы своего родителя.

Для того, чтобы образовать новую группу процессов, следует воспользоваться системной функцией setpgrp:

grp = setpgrp()

где grp - новый код группы процессов, равный его коду идентификации процесса, осуществившего вызов setpgrp().

3. СИСТЕМНЫЕ ВЫЗОВЫ

3.1. Системные вызовы для работы с процессами

Рассмотрим системные вызовы, используемые при работе с процессами в ОС UNIX и описанные в библиотеке <fcntl.h>.

Системный вызов fork создает новый процесс, копируя вызывающий, вызов exit завершает выполнение процесса, wait дает возможность родительскому процессу синхронизировать свое продолжение с завершением порожденного процесса, а sleep приостанавливает на определенное время выполнение процесса. Системный вызов exec дает процессу возможность запускать на выполнение другую программу.

FORK      Создание нового процесса:

int fork(void)

pid = fork();

В ходе выполнения функции ядро производит следующую последовательность действий:

  1. Отводит место в таблице процессов под новый процесс.
  2. Присваивает порождаемому процессу уникальный код идентификации.
  3. Создает логическую копию контекста родительского процесса. Поскольку те или иные составляющие процесса, такие как область команд, могут разделяться другими процессами, ядро может иногда вместо копирования области в новый физический участок памяти просто увеличить значение счетчика ссылок на область.
  4. Увеличивает значения счетчиков числа файлов, связанных с процессом как в таблице файлов, так и в таблице индексов.
  5. Возвращает родительскому процессу код идентификации порожденного процесса, а порожденному процессу – 0.

В результате выполнения функции fork пользовательский контекст и того, и другого процессов совпадает во всем, кроме возвращаемого значения функции fork. Если процесс не может быть порожден, функция возвращает отрицательный код ошибки.

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

EXIT            Завершение выполнения процесса:

void exit(int status)

где status – значение, возвращаемое функцией родительскому процессу. Процессы могут вызывать функцию exit как в явном, так и в неявном виде (по окончании выполнения программы функция exit вызывается автоматически с параметром 0). Также ядро может вызывать функцию exit по своей инициативе, если процесс завершается по сигналу. В этом случае значение параметра status равно номеру сигнала.

Выполнение вызова exit приводит к "прекращению существования" процесса, освобождению ресурсов и ликвидации контекста. Система не накладывает никакого ограничения на продолжительность выполнения процесса.

WAIT      Ожидание завершения выполнения процесса-потомка:

int wait(int *stat)

pid = wait(stat_addr);

где pid – значение кода идентификации (PID) завершившегося потомка, stat_addr – адрес переменной целого типа, в которую будет помещено возвращаемое функцией exit значение.

С помощью этой функции процесс синхронизирует продолжение своего выполнения с моментом завершения потомка. Ядро ведет поиск потомков процесса, прекративших существование, и в случае их отсутствия возвращает ошибку. Если потомок, прекративший существование, обнаружен, ядро передает его код идентификации и значение, возвращаемое через параметр функции exit, процессу, вызвавшему функцию wait. Таким образом, через параметр функции exit (status) завершающийся процесс может передавать различные значения, кодирующие информацию о причине завершения процесса, однако на практике этот параметр используется по назначению довольно редко. Если процесс, выполняющий функцию wait, имеет потомков, продолжающих существование, он приостанавливается до получения ожидаемого сигнала. Ядро не возобновляет по своей инициативе процесс, приостановившийся с помощью функции wait: такой процесс может возобновиться только в случае получения сигнала о "гибели потомка".

SLEEP      Приостанов работы процесса на определенное время:

void sleep(unsigned seconds)

где seconds – количество секунд, на которое требуется приостановить работу процесса.

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

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

EXEC      Запуск программы.

Системный вызов exec осуществляется несколькими библиотечными функциями – execl, execv, execle и др.. Приведем формат одной из них:

int execv(char *path, char *argv[])

res = execv(path, argv);

где path – имя исполняемого файла, argv – указатель на массив параметров, которые передаются вызываемой программе. Этот массив аналогичен параметру argv командной строки функции main.

Список argv должен содержать минимум два параметра: первый – имя программы, подлежащей выполнению (отображается в argv[0] функции main новой программы), второй – NULL (завершающий список аргументов). Системный вызов exec дает возможность процессу запускать другую программу, при этом соответствующий этой программе исполняемый файл будет располагаться в пространстве памяти процесса. Содержимое пользовательского контекста после вызова функции становится недоступным, за исключением передаваемых функции параметров, которые переписываются ядром из старого адресного пространства в новое.

Вызов exec возвращает 0 при успешном завершении и -1 при аварийном. В последнем случае управление возвращается в вызывающую программу.

В качестве примера использования этого вызова можно привести работу командного интерпретатора shell: при выполнении команды он сначала порождает свою копию (fork), а затем запускает соответствующую указанной команде программу с помощью exec.

3.2. Системные вызовы для работы с сигналами

Рассмотрим наиболее часто используемые системные вызовы при работе с сигналами в ОС UNIX, описанные в библиотеке <signal.h>.

KILL            Посылка всем или некоторым процессам любого сигнала:

int kill(pid_t pid, int sig)

где sig - номер сигнала, pid - идентификатор процесса, определяющий группу родственных процессов, которым будет послан данный сигнал:

Вызов kill возвращает 0 при успешном завершении и -1 при аварийном (например, спецификация несуществующего в ОС UNIX сигнала или несуществующего процесса).

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

Посылка сигнала может сопровождать возникновение любого события. Сигналы SIGUSR1, SIGUSR2 и SIGKILL могут быть посланы только с помощью системного вызова kill.

SIGNAL      Позволяет процессу самому определить свою реакцию на получение того или иного сигнала:

void (*signal(int signum, void (*handler)(int)))(int)

После определения реакции на сигнал signal при получении процессом этого сигнала будет автоматически вызываться функция handler(тип ее - void handler(int)), которая, естественно, должна быть описана или объявлена прежде, чем будет осуществлен системный вызов signal.

При многократной обработке одного и того же сигнала, процесс должен каждый раз осуществлять системный вызов signal для установления требуемой реакции на данный сигнал. Использование констант SIG_DFL и SIG_IGN позволяет упростить реализацию двух часто встречающихся реакций процесса на сигнал:

signal(SIGINT,SIG_IGN) игнорирование сигнала;

signal(SIGINT,SIG_DFL) восстановление стандартной реакции на сигнал.

Аргументом функции-обработчика является целое число – номер обрабатываемого сигнала. Значение его устанавливается ядром.

PAUSE      Приостанавливает функционирование процесса до получения им некоторого сигнала:

void pause()

Этот системный вызов не имеет параметров. Работа процесса возобновляется после получения им любого сигнала, кроме тех, которые игнорируются этим процессом.

ALARM      Посылка процессу сигнала побудки SIGALARM:

unsigned alarm(unsigned secs)

Этим системным вызовом процесс информирует ядро ОС о том, что ядро должно послать этому процессу сигнал побудки через secs секунд. Вызов alarm возвращает число секунд, заданное при предыдущем осуществлении этого системного вызова.

Если secs равно 0, то специфицированная ранее посылка процессу сигнала SIGALARM будет отменена.

4. ПРИМЕРЫ ПРОГРАММ

Пример 1. Порождение процессов. Программа в результате выполнения породит три процесса (процесс-предок 1 и процессы-потомки 2 и 3).

#include <sys/types.h>

#include <fcntl.h>

#include <stdio.h>

void main(void)

{      int pid2, pid3, st; /* process 1 */

printf("Process 1, pid = %d:\n", getpid());

pid2 = fork();

if (pid2 == 0) /* process 2 */

{      printf("Process 2, pid = %d:\n", getpid());

            pid3 = fork();

            if (pid3 == 0) /* process 3 */

            {      printf("Process 3, pid = %d:\n", getpid());

                  sleep(2);

                  printf("Process 3: end\n");

            } /* process 2 */

            if (pid3 < 0) printf("Cann't create process 3: error %d\n", pid3);

            wait(&st);

            printf("Process 2: end\n");

}

else /* process 1 */

{      if (pid2 < 0) printf("Cann't create process 2: error %d\n", pid2);

            wait(&st);

            printf("Process 1: end\n");

}

exit(0);

}

В соответствии с программой первоначально будет создан процесс 1 (как потомок интерпретатора shell), он сообщит о начале своей работы и породит процесс 2. После этого работа процесса 1 приостановится и начнет выполняться процесс 2 как более приоритетный. Он также сообщит о начале своей работы и породит процесс 3. Далее начнет выполняться процесс 3, он сообщит о начале работы и "заснет". После этого возобновит свое выполнение либо процесс 1, либо процесс 2 в зависимости от величин приоритетов и от того, насколько процессор загружен другими процессами. Так как ни один из процессов не выполняет никакой работы, они, вероятнее всего, успеют завершится до возобновления процесса 3, который в этом случае завершится последним.

Пример 2. Порождение процессов и их синхронизация. Данная программа в результате выполнения породит два процесса причем процесс-предок закончится после процесса-потомка.

#include <sys/types.h>

#include <fcntl.h>

#include <stdio.h>

void main(void)

{ int pid2, pid3, st; /* proc 1 */

printf("Process 1, pid = %d: begin\n", getpid());

pid2 = fork();

if (pid2 < 0) printf("Cann't create process 2: error %d\n", pid2);

else

{      if (pid2 == 0) /* process 2 */

      {      printf("Process 2, pid = %d: begin\n", getpid());

      sleep(1);

            printf("Process 2: end\n");

      }

      else /* process 1 */

      {      wait(&st);

            printf("Process 1: end\n");

      }

}

exit(0);

}

От предыдущей данная программа отличается наличием синхронизации выполнения процессов: процесс-предок ожидает завершения процесса-потомка (функция wait). Сначала выполняется процесс 1, который порождает процесс 2. После начинает выполняться процесс 2, который после выдачи сообщения "заснет". Тогда возобновит выполнение процесс 1, который приостановится до получения сигнала об окончании процесса 2. По истечение указанного периода процесс 2 возобновит свое выполнение, выдаст сообщение и завершится. После этого будет возобновлен процесс 1, который также выдаст сообщение и завершится.

Пример 3. Синхронизация работы процессов.

Программа породит три процесса (процесс-предок 0 и процессы-потомки 1 и 2). Процессы 1 и 2 будут обмениваться сигналами и выдавать соответствующие сообщения на экран, а процесс 0 через определенное количество секунд отправит процессам 1 и 2 сигнал завершения и сам прекратит свое функционирование.

#include <sys/types.h>

#include <fcntl.h>

#include <stdio.h>

#include <signal.h>

#include <unistd.h>

#define TIMEOUT 10

int f1(int), f2(int), f3(int);

int pid0, pid1, pid2;

void main(void)

{      setpgrp();

pid0 = getpid();

pid1 = fork();

if (pid1 == 0) /* process 1 */

{      signal(SIGUSR1, f1);

            pid1 = getpid();

            pid2 = fork();

            if (pid2 < 0 ) puts("Fork error");

            if (pid2 > 0) for(;;);

            else /* process 2 */

            {      signal(SIGUSR2, f2);

                  pid2 = getpid();

                  kill(pid1,SIGUSR1);

                  for (;;);

            }

}

else /* process 0 */

{      signal(SIGALRM, f3);

            alarm(TIMEOUT);

            pause();

}

exit(0);

}

int f1(int signum)

{      signal(SIGUSR1, f1);

printf("Process 1 (%d) has got a signal from process 2 (%d)\n",pid1,pid2);

sleep(1);

kill(pid2, SIGUSR2);

return 0;

}

int f2(int signum)

{      signal(SIGUSR2, f2);

printf("Process 2 (%d) has got a signal from process 1 (%d)\n",pid2,pid1);

sleep(1);

kill(pid1, SIGUSR1);

return 0;

}

int f3(int signum)

{      printf("End of job - %d\n", pid0);

kill(0, SIGKILL);

return 0;

}

5. ВЫПОЛНЕНИЕ ЛАБОРАТОРНОЙ РАБОТЫ №3

Выполнение работы заключается в написании и отладке программы по одному из вариантов задания (п.6). Ввод текста программы и его редактирование производится с помощью любого редактора UNIX (vi, ed и др.). Компиляция программы осуществляется с помощью следующего вызова:

$ cc имя_программы.с

На выходе получается исполняемый файл "a.out" или список сообщений об ошибках. Расширение указывать обязательно. Если запустить компилятор с опицией -o, можно указать произвольное имя исполняемого файла:

$ cc -o имя_исполняемого_файла имя_программы.c

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

6. ЗАДАНИЯ НА ЛАБОРАТОРНУЮ РАБОТУ №3

  1. Процесс 1 порождает потомка 2, который в свою очередь порождает потомка 3. Добиться того, чтобы эти процессы гарантированно заканчивались в порядке 3, 2, 1.
  2. Процесс 1 открывает файл и после этого порождает потомка 2. Процесс 1 пишет в файл, процесс 2 читает из файла и выводит прочитанное на экран. Организовать чтение после записи без использования сигналов.
  3. Процесс 1 открывает файл и порождает потомков 2 и 3. Потомки пишут в файл по N байт и завершают работу. После этого процесс 1 считывает данные из файла и выводит на экран. Объяснить полученный результат.
  4. Процесс 1 открывает файл и порождает потомка 2. Оба процесса после этого пишут в файл по очереди по N байт. Организовать M циклов записи с помощью сигналов.
  5. Процесс 1 открывает файл и порождает потомка 2, после этого пишет в файл N байт, закрывает его и завершается. Потом процесс 2 пишет N байт в файл, закрывает файл и завершается. Для определения очередности записи использовать функцию fstat.
  6. Процесс 1 открывает файл и порождает потомка 2, который в свою очередь порождает потомка 3. Процессы 2 и 3 пишут в общий файл, причем процесс 3 не может начать писать раньше, чем процесс 2. Организовать очередность без использования сигналов.
  7. Процесс 1 порождает процесс-потомок, ждет его завершения, после этого порождает еще один процесс-потомок и так N раз. Каждый процесс выдает на экран сообщения о начале и завершении работы.
  8. Процесс 1 открывает существующий файл и порождает потомков 2 и 3. Все процессы читают из файла данные по одному байту и выводят их на экран с пометками, какой именно процесс выводит данные. Объяснить полученный результат.
  9. Процесс 1 открывает файл и порождает потомка 2. Процесс 2 пишет N байт в файл по одному байту, процесс 1, не дожидаясь окончания процесса-потомка, начинает читать из файла по одному байту и выводит их на экран. Процесс 1 завершается, когда достигнут конец файла. Объяснить полученный результат.
  10. Программа порождает три процесса, каждый из которых записывает в общий файл число, равное своему номеру. Манипулируя схемой порождения процессов и используя сигналы обеспечить следующее содержимое файла:
  11. 1) 1 2 3 2 1

    2) 1 2 1 3 1 3 3

    3) 3 1 2 1 2 3

  12. Процесс 1 открывает файл и после этого порождает потомка 2. Процесс 2 начинает запись в файл после получения сигнала SIG1 от процесса 1 и прекращает ее после получения от процесса 1 сигнала SIG2, который посылается через N секунд после SIG1. Затем процесс 1 читает данные из файла и выводит их на экран.
  13. Процесс 1 открывает файл и после этого порождает потомка 2. Один процесс пишет в файл один байт, посылает другому процессу сигнал, другой читает из файла один байт, выводит прочитанное на экран и посылает сигнал первому процессу. Организовать N циклов запись/чтение.
  14. Процесс 1 открывает файл и порождает потомков 2 и 3. Потомки после сигнала от предка пишут в файл по N байт, посылают сигналы процессу 1 и завершают работу. После этого процесс 1 считывает данные из файла и выводит на экран.
  15. Процесс 1 открывает файл и после этого порождает потомка 2, который в свою очередь порождает потомка 3. Процесс 2 пишет N байт в общий файл, посылает сигнал процессу 3, который тоже пишет N байт в файл и посылает сигнал процессу 1, который считывает данные из файла и выводит их на экран.
  16. Процесс 1 открывает файл и порождает потомка 2, после этого пишет в файл N байт и посылает сигнал процессу 2. Процесс 2 пишет N байт в файл, посылает сигнал процессу 1 и завершается. Процесс 1, получив сигнал, считывает данные из файла, выводит их на экран и завершается.
  17. Процесс 1 открывает файл и порождает потомков 2 и 3. Потомки пишут в файл по очереди по N байт (M циклов записи, организовать с помощью сигналов) и завершаются. Последний из них посылает сигнал процессу 1, который читает данные и выводит их на экран.
  18. Процесс 1 открывает файл, порождает потомка 2, пишет в файл N байт и посылает сигнал SIG1 процессу 2. Процесс 2 по сигналу SIG1 читает данные, выводит их на экран и завершается. Если сигнал от процесса 1 не поступит в течение M секунд, процесс 2 начинает считывать данные по сигналу SIGALRM.
  19. Процесс 1 открывает файл и порождает потомка 2, который в свою очередь порождает потомка 3. Процессы 2 и 3 пишут в общий файл, причем процесс 3 не может начать писать раньше, чем процесс 2. Организовать очередность с помощью сигналов. Как только размер файла превысит N байт, процесс 1 посылает потомкам сигнал SIG2 о завершении работы, считывает данные из файла и выводит их на экран.
  20. Процесс 1 открывает файл и порождает потомка 2. Процесс 1 с интервалом в 1 секунду (через alarm) посылает M сигналов SIG1 процессу 2, который по каждому сигналу пишет в общий файл по N чисел. Потом процесс 1 посылает процессу 2 сигнал SIG2, процесс 2 завершается. Процесс 1 считывает данные из файла и выводит их на экран.
  21. Процесс 1 открывает файл и порождает потомков 2 и 3. Процесс 1 с интервалом в 1 секунду (через alarm) по очереди посылает N сигналов SIG1 процессам 2 и 3, которые по каждому сигналу пишут в общий файл по M чисел. Потом процесс 1 посылает потомкам сигнал SIG2, процессы 2 и 3 завершаются. Процесс 1 считывает данные из файла и выводит их на экран.
  22. Процесс 1 открывает два файла и порождает потомков 2 и 3. Процессы 2 и 3 с интервалом в 1 секунду (через alarm) посылают по N сигналов процессу 1, который по каждому сигналу пишет в соответствующий файл по M чисел. Потом процессы 2 и 3 считывают данные из файлов, выводят их на экран и завершаются. Процесс 1 завершается последним.
  23. Процесс 1 открывает два файла и порождает потомков 2 и 3. Процессы 2 и 3 посылают по одному сигналу процессу 1, который по каждому сигналу пишет в соответствующий файл M чисел. Процессы 2 и 3 считывают данные из файлов и выводят их на экран. С помощью сигналов организовать непересекающийся вывод данных.
  24. Процесс 1 открывает файл и порождает потомка 2. Процесс 2 по сигналу SIG1 от процесса 1 начинает писать в файл, по сигналу SIG2 прекращает запись, а потом по сигналу SIG1 завершается. Сигнал SIG1 поступает с задержкой в 1 секунду относительно первого сигнала SIG2.
  25. Процесс 1 открывает файл и порождает потомка 2. Процесс 1 читает данные из файла, процесс 2 пишет данные в файл следующим образом: по сигналу SIG1 от процесса 1 процесс 2 записывает в файл N чисел и посылает процессу 1 сигнал окончания записи; процесс 1 по этому сигналу считывает данные и посылает процессу 2 сигнал SIG1. Процесс 2 всегда пишет данные в начало файла. Организовать M циклов записи/чтения. Прочитанные данные выводятся на экран.
  26. Процесс 1 открывает существующий файл и порождает потомка 2. Процесс 1 считывает N байт из файла, выводит их на экран и посылает сигнал SIG1 процессу 2. Процесс 2 также считывает N байт из файла, выводит их на экран и посылает сигнал SIG1 процессу 1. Если одному из процессов встретился конец файла, то он посылает другому процессу сигнал SIG2 и они оба завершаются.

БИБЛИОГРАФИЧЕСКИЙ СПИСОК

  1. Дансмур М., Дейвис Г. Операционная система UNIX и программирование на языке Си: Пер. с англ. – М.: Радио и связь, 1989. – 192 с.
  2. Керниган Б.В., Пайк Р. UNIX – универсальная среда программирования: Пер. с англ. – М.: Финансы и статистика, 1992. – 304 с.
  3. Робачевский А.М. Операционная система UNIX. – СПб.: BHV – Санкт-Петербург, 1997. – 528 с.

 

 

Учебное издание

 

 

 

ПРОЦЕССЫ И СИГНАЛЫ ОС UNIX

Составители: КАРПОВ Валерий Эдуардович

КАРПОВА Ирина Петровна

 

 

 

 

 

 

Редактор

Технический редактор

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Подписано в печать .02. Формат 60´ 84/16. Бумага офсетная № 2.

Ризография. Усл.-печ.л.__. Уч.-изд.л.__. Изд. № __. Тираж 50 экз.

Заказ.

Московский государственный институт электроники и математики.

109028, Москва, Б. Трехсвятительский пер. 3/12.

Отдел оперативной полиграфии Московского государственного

института электроники и математики. 113054,

ул. М. Пионерская, 12.