Добавлен: 29.10.2018
Просмотров: 48089
Скачиваний: 190
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 соединяется со стандартным
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. Обсуждение проще
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, то в этом случае
системный вызов ожидает завершения любого дочернего процесса. Второй параметр
представляет собой адрес переменной, в которую записывается статус завершения до-
чернего процесса (нормальное или ненормальное завершение, а также возвращаемое
на выходе значение). Это позволяет родителю знать о судьбе своего ребенка. Третий
10.3. Процессы в системе Linux
809
параметр определяет, будет ли вызывающая сторона блокирована или сразу получит
управление обратно (если ни один потомок не завершен).
В случае использования оболочки дочерний процесс должен выполнить введенную
пользователем команду. Он делает это при помощи системного вызова exec, который за-
меняет весь образ памяти содержимым файла, указанного в первом параметре. Крайне
упрощенный вариант оболочки, иллюстрирующей использование системных вызовов
fork, waitpid и exec, показан в листинге 10.2.
В самом общем случае у системного вызова exec три параметра: имя исполняемого
файла, указатель на массив аргументов и указатель на массив строк окружения. Скоро
мы все это опишем. Различные библиотечные процедуры, такие как execl, execv, execle
и 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».
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 может использоваться также для игнорирования сигнала
или чтобы восстановить действие по умолчанию, заключающееся в уничтожении про-
цесса.