Файл: Debian Таненбаум Бос.pdf

ВУЗ: Не указан

Категория: Книга

Дисциплина: Операционные системы

Добавлен: 29.10.2018

Просмотров: 48089

Скачиваний: 190

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
background image

806  

 Глава 10. Изучение конкретных примеров: Unix, Linux и Android 

разы памяти. Если родительский процесс впоследствии изменяет какие-либо свои пере-
менные, то эти изменения остаются невидимыми для дочернего процесса (и наоборот).

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

Тот факт, что образы памяти, переменные, регистры и все остальное у родительского 
и дочернего процессов идентичны, приводит к небольшому затруднению: как процес-
сам узнать, какой из них должен исполнять родительский код, а какой — дочерний? 
Секрет в том, что системный вызов fork возвращает дочернему процессу число 0, 
а родительскому — отличный от нуля PID  (Process IDentifier — идентификатор про-
цесса ) дочернего процесса. Оба процесса обычно проверяют возвращаемое значение 
и действуют так, как показано в листинге 10.1.

Листинг 10.1. Создание процесса в системе Linux

pid = fork( );             /* если fork завершился успешно, pid > 0 в 
                              родительском процессе */
if (pid < 0) {
    handle_error();        /* fork потерпел неудачу (например, память 
                              или какая-либо таблица переполнена) */
} else if (pid > 0) {
                           /* здесь располагается родительский код */
} else {
                           /* здесь располагается дочерний код */
}

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

В системе Linux процессы могут общаться друг с другом с помощью некой формы 
передачи сообщений. Можно создать канал между двумя процессами, в который один 
процесс сможет писать поток байтов, а другой процесс сможет его читать. Эти каналы 
иногда называют трубами (pipes) . Синхронизация процессов достигается путем бло-
кирования процесса при попытке прочитать данные из пустого канала. Когда данные 
появляются в канале, процесс разблокируется.

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

sort <f | head

она создает два процесса, sort и head, а также устанавливает между ними канал таким 
образом, что стандартный поток вывода программы sort соединяется со стандартным 


background image

10.3. Процессы в системе Linux   

807

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

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

Сигналы используются и для других целей. Например, если процесс выполняет вы-
числения с плавающей точкой и непреднамеренно делит на 0 (делает то, что осуждается 
математиками), то он получает сигнал SIGFPE (Floating-Point Exception SIGnal — сиг-
нал исключения при выполнении операции с плавающей точкой). Сигналы, требуемые 
стандартом POSIX, перечислены в табл. 10.2. В большинстве систем Linux имеются 
также дополнительные сигналы, но использующие их программы могут оказаться не-
переносимыми на другие версии Linux и UNIX.

Таблица 10.2. Сигналы, требуемые стандартом POSIX

Сигнал

Причина

SIGABRT

Посылается, чтобы прервать процесс и создать дамп памяти

SIGALRM

Истекло время будильника

SIGFPE

Произошла ошибка при выполнении операции с плавающей точкой (например, 
деление на 0)

SIGHUP

На телефонной линии, использовавшейся процессом, была повешена трубка

SIGILL

Пользователь нажал клавишу Del, чтобы прервать процесс

SIGQUIT

Пользователь нажал клавишу, требующую выполнения дампа памяти

SIGKILL

Посылается, чтобы уничтожить процесс (не может игнорироваться или перехва-
тываться)

SIGPIPE

Процесс пишет в канал, из которого никто не читает

SIGSEGV

Процесс обратился к неверному адресу памяти

SIGTERM

Вежливая просьба к процессу завершить свою работу

SIGUSR1

Может быть определен приложением

SIGUSR2

Может быть определен приложением

10.3.2. Системные вызовы управления процессами в Linux

Рассмотрим теперь системные вызовы Linux  , предназначенные для управления про-
цессами. Основные системные вызовы перечислены в табл. 10.3. Обсуждение проще 


background image

808  

 Глава 10. Изучение конкретных примеров: Unix, Linux и Android 

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

Таблица 10.3. Некоторые системные вызовы, относящиеся к процессам. 
Код возврата s в случае ошибки равен –1; pid — это идентификатор процесса; 
residual — остаток времени от предыдущего сигнала. Смысл параметров понятен 
по их названиям

Системный вызов

Описание

pid=fork( )

Создать дочерний процесс, идентичный родительскому

pid=waitpid(pid, &statloc, opts)

Ждать завершения дочернего процесса

s=execve(name, argv, envp)

Заменить образ памяти процесса

exit(status)

Завершить выполнение процесса и вернуть статус

s=sigaction(sig, &act, &oldact)

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

s=sigreturn(&context)

Вернуть управление после обработки сигнала

s=sigprocmask(how, &set, &old)

Исследовать или изменить маску сигнала

s=sigpending(set)

Получить набор блокированных сигналов

s=sigsuspend(sigmask)

Заменить маску сигнала и приостановить процесс

s=kill(pid, sig)

Послать сигнал процессу

residual=alarm(seconds)

Установить будильник

s=pause( )

Приостановить выполнение вызывающей стороны до сле-
дующего сигнала

В большинстве случаев после системного вызова fork дочернему процессу требуется 
выполнить отличающийся от родительского процесса код. Рассмотрим работу обо-
лочки. Она считывает команду с терминала, с помощью системного вызова fork создает 
дочерний процесс, ждет выполнения введенной команды дочерним процессом, после 
чего считывает следующую команду (после завершения дочернего процесса). Для 
ожидания завершения дочернего процесса родительский процесс делает системный 
вызов waitpid, который ждет завершения потомка (любого потомка, если их несколько). 
У этого системного вызова три параметра. Первый параметр позволяет вызывающей 
стороне ждать конкретного потомка. Если этот параметр равен –1, то в этом случае 
системный вызов ожидает завершения любого дочернего процесса. Второй параметр 
представляет собой адрес переменной, в которую записывается статус завершения до-
чернего процесса (нормальное или ненормальное завершение, а также возвращаемое 
на выходе значение). Это позволяет родителю знать о судьбе своего ребенка. Третий 


background image

10.3. Процессы в системе Linux   

809

параметр определяет, будет ли вызывающая сторона блокирована или сразу получит 
управление обратно (если ни один потомок не завершен).

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

В самом общем случае у системного вызова exec три параметра: имя исполняемого 
файла, указатель на массив аргументов и указатель на массив строк окружения. Скоро 
мы все это опишем. Различные библиотечные процедуры, такие как execlexecvexecle 
и execve, позволяют опускать некоторые параметры или указывать их иными спосо-
бами. Все эти процедуры обращаются к одному и тому же системному вызову. Хотя 
сам системный вызов называется exec, библиотечной процедуры с таким именем нет 
(необходимо использовать одну из вышеупомянутых).

Листинг 10.2. Сильно упрощенная оболочка

while (TRUE) { /* бесконечный цикл */
    type_prompt( );                /* вывести приглашение к вводу */
    read_command(command, params); /* прочитать с клавиатуры строку ввода*/
    pid = fork( );                 /* ответвить дочерний процесс */
    if (pid < 0) {
        printf("Создать процесс невозможно"); /* ошибка */
        continue;                  /* повторить цикл */
    }
    if (pid != 0) {
        waitpid (-1, &status, 0);  /* родительский процесс ждет дочерний 
                                      процесс */
    } else {
        execve(command, params, 0); /* дочерний процесс выполняет работу */
    }
}

Рассмотрим случай выполнения оболочкой команды

cp file1 file2

используемой для копирования файла 

file1

 в файл 

file2

. После того как оболочка со-

здает дочерний процесс, тот обнаруживает и исполняет файл 

cp

 и передает ему инфор-

мацию о копируемых файлах.

Главная программа файла 

cp

 (как и многие другие программы) содержит объявление 

функции

main(argc, argv, envp)

где argc — счетчик количества элементов командной строки, включая имя программы. 
Для приведенного примера значение argc равно 3.

Второй параметр argv представляет собой указатель на массив. i-й элемент этого мас-
сива является указателем на i-й элемент командной строки. В нашем примере элемент 
argv[0] указывает на двухсимвольную строку «cp». Соответственно элемент argv[1] 
указывает на пятисимвольную строку «file1», а элемент argv[2] — на пятисимвольную 
строку «file2».


background image

810  

 Глава 10. Изучение конкретных примеров: Unix, Linux и Android 

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

Если системный вызов exec показался вам слишком мудреным, не отчаивайтесь — это 
самый сложный системный вызов. Все остальные значительно проще. В качестве 
примера простого системного вызова рассмотрим exit, который процессы должны ис-
пользовать при завершении исполнения. У него есть один параметр — статус выхода 
(от 0 до 255), возвращаемый родительскому процессу в переменной status системного 
вызова waitpid. Младший байт переменной status содержит статус завершения (равный 
нулю при нормальном завершении или коду ошибки — при аварийном). Старший 
байт содержит статус выхода потомка (от 0 до 255), указанный в вызове завершения 
потомка. Например, если родительский процесс выполняет оператор

n = waitpid(-1, &status, 0);

то он будет приостановлен до тех пор, пока не завершится какой-либо дочерний про-
цесс. Если, например, дочерний процесс завершится со значением статуса 4 (в пара-
метре библиотечной процедуры exit), то родительский процесс будет разбужен со 
значением n, равным PID дочернего процесса, и значением статуса 0x0400 (префикс 
0x означает в программах на языке C шестнадцатеричное число). Младший байт пере-
менной status относится к сигналам, старший байт представляет собой значение, зада-
ваемое дочерним процессом в виде параметра при обращении к системному вызову exit.

Если процесс уже завершился, а родительский процесс не ожидает этого события, то 
дочерний процесс переводится в так называемое состояние зомби (zombie state) — 
живого мертвеца , то есть приостанавливается. Когда родительский процесс, наконец, 
обращается к библиотечной процедуре waitpid, дочерний процесс завершается.

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

Del

 или 

Ctrl+C

), в результате чего редакто-

ру посылается сигнал. Редактор перехватывает сигнал и останавливает вывод.

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

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

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