МИНИСТЕРСТВО ОБРАЗОВАНИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ
Московский Государственный институт электроники и математики
(Технический университет)
Кафедра Управление и информатика в технических системах
ПРОЦЕССЫ И СИГНАЛЫ ОС UNIX
Методические указания
к лабораторной работе № 3
по курсу "Системное программное обеспечение"
Москва 2002
Составители: канд. техн. наук В.Э. Карпов
ст. преподаватель И.П. Карпова
УДК 681.3
Процессы и сигналы ОС UNIX: Метод. указания к лабораторной работе №3 по курсу " Системное программное обеспечение " / Моск. гос. ин-т электроники и математики; Сост.: В.Э. Карпов, И. П. Карпова. М., 2002. – 27с.
Лабораторная работа № 3 посвящена изучению процессов и сигналов ОС UNIX как средства организации межпроцессного взаимодействия, приобретению знаний и навыков написания программ работы со взаимодействующими процессами. Приведены системные вызовы для работы с процессами и сигналами и примеры программ.
Лабораторная работа выполняется в объеме 3 часов.
Методические указания к лабораторным работам являются составной частью учебно-методического комплекса по дисциплине “Системное программное обеспечение”, изучаемой студентами специальности 21.01 “Управление и информатика в технических системах”.
ISBN
ОГЛАВЛЕНИЕ
1. ТЕОРЕТИЧЕСКИЕ СВЕДЕНИЯ
Цель лабораторной работы №3 состоит в изучении принципов организации работы ОС UNIX как многозадачной операционной системы и приобретении навыков написания программ работы со взаимодействующими процессами.
Работу ОС UNIX можно представить в виде функционирования множества взаимосвязанных процессов.
Процесс – это задание в ходе его выполнения. Он представляет собой исполняемый образ программы, включающий отображение в памяти исполняемого файла, полученного в ходе компиляции, стек, код и данные библиотек, а также ряд структур данных ядра, необходимых для управления процессом. Выполнение процесса заключается в точном следовании замкнутому набору инструкций, который не передает управление набору инструкций другого процесса; он считывает и записывает информацию в раздел данных и в стек, но ему недоступны данные и стеки других процессов.
ОС UNIX является многозадачной системой, поэтому в ней параллельно выполняется множество процессов, их выполнение планируется ядром. Несколько процессов могут быть экземплярами одной программы. Процессы взаимодействуют с другими процессами и с вычислительными ресурсами только посредством обращений к операционной системе, которая эффективно распределяет системные ресурсы между активными процессами.
Выполнение процесса осуществляется ядром системы. Подсистема управления процессами отвечает за синхронизацию процессов, взаимодействие процессов, распределение памяти и планирование выполнения процессов.
С практической точки зрения процесс в системе UNIX является объектом, создаваемым в результате выполнения системного вызова fork(). Каждый процесс, за исключением нулевого, порождается в результате запуска другим процессом операции fork(). Процесс, запустивший операцию fork(), называется родительским, а вновь созданный процесс – порожденным. Каждый процесс имеет одного родителя, но может породить много процессов. Ядро системы идентифицирует каждый процесс по его номеру, который называется идентификатором процесса (PID). Нулевой процесс является особенным процессом, который создается "вручную" в результате загрузки системы. Процесс 1, известный под именем init, является предком любого другого процесса в системе и связан с каждым процессом. Пользователь, транслируя исходный текст программы, создает исполняемый файл, который состоит из нескольких частей:
Ядро загружает исполняемый файл в память при выполнении системной операции exec, при этом загруженный процесс состоит по меньшей мере из трех частей, так называемых областей: текста, данных и стека. Области текста и данных соответствуют секциям текста и bss-данных исполняемого файла, а область стека создается автоматически и ее размер динамически устанавливается ядром системы во время выполнения.
Процесс в системе UNIX может выполняться в двух режимах – режиме ядра или режиме задачи. В режиме задачи процесс выполняет инструкции прикладной программы, системные структуры данных ему недоступны.
Когда процесс выполняет специальную инструкцию (системный вызов), он переключается в режим ядра. Каждой системной операции соответствует точка входа в библиотеке системных операций; библиотека системных операций написана на языке ассемблера и включает специальные команды прерывания, которые, выполняясь, порождают "прерывание", вызывающее переключение аппаратуры в режим ядра. Процесс ищет в библиотеке точку входа, соответствующую отдельной системной операции, подобно тому, как он вызывает любую из функций.
Соответственно и образ процесса состоит из двух частей: данных режима ядра и режима задачи. В режиме ядра образ процесса включает сегменты кода, данных, библиотек и других структур, к которым он может получить непосредственный доступ. Образ процесса в режиме ядра состоит из структур данных, недоступных процессу в режиме задачи (например, состояния регистров, таблицы для отображения памяти и т.п.).
Каждому процессу соответствует точка входа (запись) в таблице процессов ядра, кроме того, каждому процессу выделяется часть оперативной памяти, отведенной под задачи пользователей. Таблица процессов включает в себя указатели на промежуточную таблицу областей процессов, точки входа в которую служат в качестве указателей на собственно таблицу областей. Областью называется непрерывная зона адресного пространства, выделяемая процессу для размещения текста, данных и стека. Точки входа в таблицу областей описывают атрибуты области, как например, хранятся ли в области текст программы или данные, закрытая ли эта область или же совместно используемая, и где конкретно в памяти размещается содержимое области. Внешний уровень косвенной адресации (через промежуточную таблицу областей, используемых процессами, к собственно таблице областей) позволяет независимым процессам совместно использовать области.
Когда процесс запускает системную операцию exec, ядро системы выделяет области оперативной памяти (ОП) под новый процесс и освобождает области памяти вызывающего процесса. Если процесс запускает операцию fork, ядро удваивает размер адресного пространства процесса, позволяя новому и старому процессам совместно использовать области ОП, когда это возможно, и, с другой стороны, производя физическое копирование. Если процесс запускает операцию exit, ядро освобождает области, которые использовались процессом. На Рис.1 изображены информационные структуры, связанные с запуском процесса позволяющие процессам разделять области ОП.
Таблица процессов ссылается на промежуточную таблицу областей, используемых процессом, в которой содержатся указатели на записи в собственно таблице областей, соответствующие областям для текста, данных и стека процесса.
UNIX является системой разделения времени. Это означает, что каждому процессу вычислительные ресурсы выделяются на ограниченный промежуток времени, после чего они предоставляются другому процессу. Максимальный временной интервал, на который процесс может захватить процессор, называется временным квантом. Таким образом, создается иллюзия того, что процессы работают одновременно, хотя в действительности на однопроцессорной машине одновременно может выполняться только один процесс.
Процессы предъявляют различные требования к системе с точки зрения их планирования и общей производительности. Можно выделить три основных класса приложений:
В основе планирования выполнения процессов лежат два понятия: квант времени и приоритет. Каждый процесс имеет два атрибута приоритета: текущий (на основании которого осуществляется планирование) и относительный, который задается при порождении процесса и влияет на текущий. Номера приоритетов разбиваются на несколько групп: для процессов в режиме задачи, в режиме ядра, для процессов реального времени (группы указаны в соответствии с повышением приоритета).
Обработчик прерываний от таймера, в частности, проверяет истечение временного кванта для процессов и пересчитывает приоритеты процессов: чем дольше процесс занимает процессор, тем ниже (в пределах группы процессов) становится его приоритет.
Выполнение процесса может быть прервано:
В режиме ядра приоритет процесса повышается для того, чтобы его выполнение не могло быть прервано, так как это может привести к нарушению целостности структур данных ядра. Таким образом, выполнение системных вызовов осуществляется в непрерывном режиме (за исключением некоторых аппаратных прерываний).
Под контекстом процесса понимается вся информация, необходимая для описания процесса. Контекст процесса состоит из нескольких частей:
Контекстом процесса является его состояние, определяемое текстом, значениями глобальных переменных пользователя и информационными структурами, значениями используемых машинных регистров, значениями, хранимыми в таблице процессов и в адресном пространстве задачи, а также содержимым стеков задачи и ядра, относящихся к процессу. Текст операций системы и ее глобальные информационные структуры совместно используются всеми процессами, но не являются составной частью контекста процесса.
Принято говорить, что при запуске процесса система исполняется в контексте процесса. Когда ядро системы решает запустить другой процесс, оно выполняет переключение контекста с тем, чтобы система исполнялась в контексте другого процесса. Ядро осуществляет переключение контекста только при определенных условиях (см. п.1.3). Выполняя переключение контекста, ядро сохраняет информацию, достаточную для того, чтобы позднее переключиться вновь на прерванный процесс и возобновить его выполнение.
Аналогичным образом, при переходе из режима задачи в режим ядра, ядро системы сохраняет информацию, достаточную для того, чтобы позднее вернуться в режим задачи и продолжить выполнение с прерванного места. Однако, переход из режима задачи в режим ядра является сменой режима, но не переключением контекста. Ядро меняет режим выполнения с режима задачи на режим ядра и наоборот, оставаясь в контексте одного процесса.
Ядро обрабатывает прерывания в контексте прерванного процесса, пусть даже оно и не вызывало никакого прерывания. Прерванный процесс мог при этом выполняться как в режиме задачи, так и в режиме ядра. Ядро сохраняет информацию, достаточную для того, чтобы можно было позже возобновить выполнение прерванного процесса, и обрабатывает прерывание в режиме ядра. Ядро не порождает и не планирует порождение какого-то особого процесса по обработке прерываний.
Время жизни процесса можно разделить на несколько состояний, каждое из которых имеет определенные характеристики, описывающие процесс. На рис. 2 показаны состояния, в которых процесс может находиться с момента создания до завершения выполнения:
Поскольку процессор в каждый момент времени выполняет только один процесс, в состояниях 1 и 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. Каждому сигналу соответствует адрес функции, которая будет вызвана для обработки данного сигнала.
Ядро обрабатывает сигналы в контексте того процесса, который получает их, поэтому чтобы обработать сигналы, нужно запустить процесс.
Существует три способа обработки сигналов:
Реакцией по умолчанию со стороны процесса, исполняемого в режиме ядра, обычно является вызов функции exit(), т.е. завершение процесса. Но вместе с тем реакция процесса на принимаемый сигнал зависит от того, как сам процесс определил свое поведение в случае приема данного сигнала: процесс может проигнорировать сигнал, вызвать на выполнение другой процесс и т.д. При этом способ обработки сигналов одного типа не влияет на обработку сигналов других типов.
Обрабатывая сигнал, ядро определяет тип сигнала и очищает (гасит) разряд в записи таблицы процессов, соответствующий данному типу сигнала и установленный в момент получения сигнала процессом. Таким образом, когда процесс получает любой неигнорируемый им сигнал (за исключением SIGILL и SIGTRAP), ОС UNIX автоматически восстанавливает реакцию “по умолчанию” на всякое последующее получение этого сигнала.
Замечание 1: Если необходима многократная обработка одного и того же сигнала, процесс должен каждый раз осуществлять системный вызов signal для установления требуемой реакции на данный сигнал.
Замечание 2: Процесс не в состоянии узнать, сколько однотипных сигналов им было получено. В том случае, если процесс не успевает обработать все поступившие сигналы, происходит потеря информации.
Если функции обработки сигнала присвоено значение по умолчанию, ядро в отдельных случаях перед завершением процесса сбрасывает на внешний носитель (дампирует) образ процесса в памяти. Дампирование удобно для программистов тем, что позволяет установить причину завершения процесса и посредством этого вести отладку программ. Ядро дампирует состояние памяти при поступлении сигналов, которые сообщают о каких-нибудь ошибках в выполнении процессов, как например, попытка исполнения запрещенной команды или обращение к адресу, находящемуся за пределами виртуального адресного пространства процесса. Ядро не дампирует состояние памяти, если сигнал не связан с программной ошибкой, за исключением внешнего сигнала о выходе (quit), обычно вызываемого одновременным нажатием клавиш Ctrl+|.
Если процесс получает сигнал SIGINT, который было решено игнорировать (signal(SIGINT,SIG_IGN)), выполнение процесса продолжается так, словно сигнала и не было. Поскольку ядро не сбрасывает значение соответствующего поля, свидетельствующего о необходимости игнорирования сигнала данного типа, то когда сигнал поступит вновь, процесс опять не обратит на него внимание.
В том случае, если процесс получает сигнал, реагирование на который установлено системным вызовом signal, сразу по возвращении процесса в режим задачи выполняется заранее условленное действие, описанное в вызове signal. После выполнения функции обработки сигнала управление будет передано на то место в программе пользователя, где было произведено обращение к системной функции или произошло прерывание.
Если во время исполнения системной функции приходит сигнал, а процесс приостановлен с допускающим прерывания приоритетом, этот сигнал побуждает процесс выйти из приостанова, вернуться в режим задачи и вызвать функцию обработки сигнала. Когда функция обработки сигнала завершается, процесс выходит из системной функции с ошибкой, сообщающей о прерывании ее выполнения. После этого пользователь может запустить системную функцию повторно.
Несмотря на то, что в системе UNIX процессы идентифицируются уникальным кодом (PID), системе иногда приходится использовать для идентификации процессов номер “группы”, в которую они входят. Например, процессы, имеющие общего предка в лице регистрационного интерпретатора shell, взаимосвязаны, и поэтому когда пользователь нажимает клавиши “delete” или “break”, или когда терминальная линия “зависает”, все эти процессы получают соответствующие сигналы. Ядро использует код группы процессов для идентификации группы взаимосвязанных процессов, которые при наступлении определенных событий должны получать общий сигнал. Код группы запоминается в таблице процессов. При выполнении функции fork процесс-потомок наследует код группы своего родителя.
Для того, чтобы образовать новую группу процессов, следует воспользоваться системной функцией setpgrp:
grp = setpgrp()
где grp - новый код группы процессов, равный его коду идентификации процесса, осуществившего вызов setpgrp().
3.1. Системные вызовы для работы с процессами
Рассмотрим системные вызовы, используемые при работе с процессами в ОС UNIX и описанные в библиотеке <fcntl.h>.
Системный вызов fork создает новый процесс, копируя вызывающий, вызов exit завершает выполнение процесса, wait дает возможность родительскому процессу синхронизировать свое продолжение с завершением порожденного процесса, а sleep приостанавливает на определенное время выполнение процесса. Системный вызов exec дает процессу возможность запускать на выполнение другую программу.
FORK Создание нового процесса:
int fork(void)
pid = fork();
В ходе выполнения функции ядро производит следующую последовательность действий:
В результате выполнения функции 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 будет отменена.
Пример 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 2 1
2) 1 2 1 3 1 3 3
3) 3 1 2 1 2 3
Учебное издание
ПРОЦЕССЫ И СИГНАЛЫ ОС UNIX
Составители: КАРПОВ Валерий Эдуардович
КАРПОВА Ирина Петровна
Редактор
Технический редактор
Подписано в печать .02. Формат 60´ 84/16. Бумага офсетная № 2.
Ризография. Усл.-печ.л.__. Уч.-изд.л.__. Изд. № __. Тираж 50 экз.
Заказ.
Московский государственный институт электроники и математики.
109028, Москва, Б. Трехсвятительский пер. 3/12.
Отдел оперативной полиграфии Московского государственного
института электроники и математики. 113054,
ул. М. Пионерская, 12.