2011. Машбук (1114722), страница 31
Текст из файла (страница 31)
При неудачном завершении возвращается -1, а в переменной errnoустанавливается код ошибки.Ниже представлены прототипы функций семейства exec():#include <unistd.h>int execl(const char *path, char *arg0,…);int execlp(const char *file, char *arg0,…);int execle(const char *path, char *arg0,…, const char **env);int execv(const char *path, const char **arg);int execvp(const char *file, const char **arg);int execve(const char *path, const char **arg, const char**env);Первый параметр во всех вызовах задает имяфайла программы, подлежащейисполнению. Этот файл должен быть исполняемым файлом и пользователь-владелецпроцесса должен иметь право на исполнение данного файла.
Для функций с суффиксом«p» в названии имя файла может быть кратким, при этом при поиске нужного файла будетиспользоваться переменная окружения PATH. Далее передаются аргументы команднойстроки для вновь запускаемой программы, которые отобразятся в ее массив argv — в видесписка аргументов переменной длины для функций с суффиксом «l» либо в виде векторастрок для функций с суффиксом «v». В любом случае, в списке аргументов должноприсутствовать как минимум 2 аргумента: имя программы, которое отобразится в элементargv[0], и значение NULL, завершающее список.
В функциях с суффиксом «e» имеетсятакже дополнительный аргумент, описывающий переменные окружения для вновьзапускаемой программы – это массив строк вида name=value, завершенный значениемNULL.Концептуально все системные вызовы семейства exec() работают следующимобразом. Через параметры вызова передается указание на имя некоторого исполняемогофайла, а также набор аргументов, которые передаются внутрь при запуске этогоисполняемого файла. При выполнении данных системных вызовов происходит заменатела текущего процесса на тело, образованное в результате загрузки исполняемого файла,и управление передается на точку входа в новое тело.Рассмотрим небольшой пример (Рис.
85). Запускается процесс (ему ставится всоответствие идентификатор 2757), который обращается к системному вызову execl(), длясмены своего тела телом команды ls -l, которая отображает содержимое текущегокаталога. Реализация данной команды хранится, соответственно, в файле /bin/ls. Послеуспешного завершения системного вызова execl(), процесс (с тем же идентификатором2757) будет содержать реализацию команды ls, и управление в нем будет передано наточку входа (т.е. запустится функция main()).Итак, семейство системных вызовов exec() производит замену тела вызывающегопроцесса, после чего данный процесс начинает выполнять другую программу, передаваяуправление на точку ее входа.
Возврат к первоначальной программе происходит только вслучае ошибки при обращении к exec(), т.е. если фактической замены тела процесса непроизошло.Заметим, что выполнение «нового» тела происходит в рамках уже существующегопроцесса, т.е. после вызова exec() сохраняется идентификатор процесса, и идентификатор109родительского процесса, таблица дескрипторов файлов (за исключением, быть может,файлов, открытых в специальном режиме), приоритет и большая часть других атрибутовпроцесса. Фактически происходит замена сегмента кода и сегмента данных.
Изменяетсяследующая системная информация, которая должна корректироваться при смене телапроцесса:режимы обработки сигналов: для сигналов, которые перехватывались, послезамены тела процесса будет установлена обработка по умолчанию, т.к. вновой программе могут отсутствовать указанные функции-обработчикисигналовэффективные идентификаторы владельца и группы могут измениться, еслидля новой выполняемой программы установлен s-битперед началом выполнения новой программы могут быть закрытынекоторые файлы, ранее открытые в процессе. Это касается тех файлов, длякоторых при помощи системного вызова fcntl() был установлен флаг closeon-exec. Соответствующие файловые дескрипторы будут помечены каксвободные.При обращении к системным вызовам семейства exec() сохраняются основныеатрибуты текущего процесса (в частности, идентификатор процесса, идентификаторродительского процесса, приоритет и др.), а также сохраняются все открытые в текущемпроцессе файлы (за исключением, быть может, файлов, открытых в специальном режиме).С другой стороны, изменяются режимы обработки сигналов, эффективныеидентификаторы владельца и группы и прочая системная информация, которая должнакорректироваться при смене тела процесса.PID=2757main(){execl(“/bin/ls”,”ls”,”-l”,(char*)0);}PID=2757execl()main(){/* реализацияпрограммы ls */}Рис.
85. Пример использования системного вызова execl().Приведем ряд примеров для иллюстрации применения различных вызововсемейства exec().Пример. Если обращение к системному вызову будет неуспешным, то функцияprintf() отобразит на экране соответствующий текст.#include <unistd.h>int main(int argc, char **argv){.../*тело программы*/...execl(“/bin/ls”, ”ls”, ”-l”, (char*)0);/* или execlp(“ls”, ”ls”, ”-l”, (char*)0); */printf(“это напечатается в случае неудачного обращения кпредыдущей функции, к примеру, если не был найден файл ls \n”);...}110Пример.
Вызов C-компилятора. В данном случае второй параметр — вектор изуказателей на параметры строки, которые будут переданы в вызываемую программу. Каки ранее, первый указатель — имя программы, последний — нулевой указатель. Этивызовы удобны, когда заранее неизвестно число аргументов вызываемой программы.int main(int argc, char **argv){char *pv[]={“cc”, “-o”, “ter”, “ter.c”, (char*)0};.../*тело программы*/...execv (“/bin/cc”, pv);...}Итак, мы рассмотрели по отдельности системные вызовы fork() и exec().Чрезвычайно полезным является использование fork() совместно с системным вызовомexec(). Как отмечалось выше, системный вызов exec() используется для запускаисполняемого файла в рамках существующего процесса.
Ниже (Рис. 86) приведена общаясхема использования связки fork() — exec(). В данном случае родительский процесс (PID= 2757) порождает своего потомка посредством обращения к системному вызову fork(),после чего в отцовском процессе управление переходит на else-блок. В то же время всыновнем процессе (PID = 2760) управление передается на первую инструкцию thenблока, где происходит обращение к системному вызову execl(). После чего тело сыновнегопроцесса меняется на тело команды ls.Рис. 86.
Пример использования схемы fork-exec.Рассмотрим еще один пример. Программа порождает три процесса, каждый изкоторых запускает программу echo посредством системного вызова exec(). Данныйпример демонстрирует важность проверки успешного завершения системного вызоваexec(), в противном случае возможно исполнение нескольких копий исходной программы.В нашем случае, если все вызовы exec() проработают неуспешно, то копий программ111будет восемь. Если все вызовы exec() будут успешными, то после последнего вызова fork()будет существовать четыре копии процесса. В каком порядке они пойдут на выполнениепредсказать трудно.int main(int argc, char **argv){if(fork() == 0){execl(“/bin/echo”,”echo”,”это”,”сообщение один”,NULL);printf(“ошибка”);}if(fork() == 0){execl(“/bin/echo”,”echo”,”это”,”сообщение два”,NULL);printf(“ошибка”);}if(fork() == 0){execl(“/bin/echo”,”echo”,”это”,”сообщение три”,NULL);printf(“ошибка”);}printf(“процесс-предок закончился”);}Результат работы может быть следующим:процесс-предок закончилсяэто сообщение триэто сообщение одинэто сообщение дваТеперь рассмотрим системные вызовы, которые сопутствуют базовым системнымвызовам управления процессами в ОС Unix.
Прежде всего, речь пойдет о завершениипроцесса. Вообще говоря, процесс может завершиться по одной из двух причин. Перваяпричина связана с возникновением в процессе сигнала. Сигнал можно считатьпрограммным аналогом прерывания, и речь о сигналах пойдет ниже при обсуждениивопросов взаимодействия процессов.
Сигнал может быть связан с тем, что в процессепроизошло деление на ноль, или сигнал может прийти от другого процесса с указаниемнезамедлительного завершения. Вторая причина связана с обращением к системномувызову завершения процесса. При этом обращение может быть явным, когда в телепрограммы встречается обращение к системному вызову _exit(), или неявным, еслипроисходит выполнение оператора return языка C внутри функции main() или выход назакрывающую скобку функции main(). В последнем случае компилятор заменит действиеоператора return обращением к системному вызову _exit().#include <unistd.h>void _exit(int status);Системный вызов _exit() никогда не завершается неудачно, поэтому для него непредусмотрено возвращаемого значения.
С помощью единственного параметра statusпроцесс может передать породившему его процессу информацию о статусе своегозавершения - т.н. программный код завершения процесса. Принято, хотя и не является112обязательным правилом, что возврат нулевого значения сигнализирует об успешномзавершении процесса; ненулевые значения трактуются как ошибочное завершение (вчастности, процесс может возвращать некий код ошибки).Рассмотрим, что происходит с процессом и в системе при обращении к системномувызову _exit(). Очевидно, что сиюминутно процесс не может завершиться, поэтомупроцесс переходит в переходное состояние — т.н. состояние зомби.