3.5 механизм замены тела процесса.Семейство системных вызовов exec()[6] производит замену тела вызывающего процесса, после чего данный процесс начинает выполнять другую программу, передавая управление на точку ее входа. Возврат к первоначальной программе происходит только в случае ошибки при обращении к exec(), т.е. если фактической замены тела процесса не произошло. Заметим, что выполнение “нового” тела происходит в рамках уже существующего процесса, т.е. после вызова exec() сохраняется идентификатор процесса, и идентификатор родительского процесса, таблица дескрипторов файлов, приоритет, и большая часть других атрибутов процесса. Фактически происходит замена сегмента кода и сегмента данных. Изменяются следующие атрибуты процесса: диспозиция сигналов: для сигналов, которые перехватывались, после замены тела процесса будет установлена обработка по умолчанию, так как в новой программе могут отсутствовать указанные функции-обработчики сигналов; эффективные идентификаторы владельца и группы могут измениться, если для новой выполняемой программы установлен s-бит перед началом выполнения новой программы могут быть закрыты некоторые файлы, ранее открытые в процессе. Это касается тех файлов, для которых при помощи системного вызова fcntl() был установлен флаг close-on-exec. Соответствующие файловые дескрипторы будут помечены как свободные.
Рис. 10 Выполнение системного вызова exec() Ниже представлены прототипы функций семейства 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); Первый параметр во всех вызовах задает имя (краткое или полное путевое) файла программы, подлежащей исполнению. Этот файл должен быть исполняемым файлом (в UNIX-системах это может быть также командный файл (сценарий) интерпретатора shell, но стандарт POSIX этого не допускает), и пользователь-владелец процесса должен иметь право на исполнение данного файла. Для функций с суффиксом «p» в названии имя файла может быть кратким, при этом при поиске нужного файла будет использоваться переменная окружения PATH. Далее передаются аргументы командной строки для вновь запускаемой программы, которые отобразятся в ее массив argv – в виде списка аргументов переменной длины для функций с суффиксом «l» либо в виде вектора строк для функций с суффиксом «v». В любом случае, в списке аргументов должно присутствовать как минимум 2 аргумента: имя программы, которое отобразится в элемент argv[0], и значение NULL, завершающее список. В функциях с суффиксом «e» имеется также дополнительный аргумент, описывающий переменные окружения для вновь запускаемой программы – это массив строк вида name=value, завершенный значением NULL. Запуск на выполнение команды ls. #include <unistd.h> #include <stdio.h> int main(int argc, char **argv) { … /*тело программы*/ … execl(“/bin/ls”,”ls”,”-l”,(char*)0); /* или execlp(“ls”,”ls”, ”-l”,(char*)0);*/ printf(“это напечатается в случае неудачного обращения к предыдущей функции, к примеру, если не был найден файл ls ”); … } В данном случае второй параметр – вектор из указателей на параметры строки, которые будут переданы в вызываемую программу. Как и ранее первый указатель – имя программы, последний – нулевой указатель. Эти вызовы удобны, когда заранее неизвестно число аргументов вызываемой программы. Вызов программы компиляции. #include <unistd.h> int main(int argc, char **argv) { char *pv[]={ “cc”, “-o”, “ter”, “ter.c”, (char*)0 }; … /*тело программы*/ … execv (“/bin/cc”,pv); … } Наиболее интересным является использование fork() совместно с системным вызовом exec(). Как отмечалось выше системный вызов exec() используется для запуска исполняемого файла в рамках существующего процесса. Ниже приведена общая схема использования связки fork() - exec(). Рис. 11 Использование схемы fork()-exec() Схема использования fork-exec #include <sys/types.h> #include <unistd.h> int main(int argc, char **argv) { int pid; if ((pid=fork())!=0){ if(pid>0) { /* процесс-предок */ } else { /* ошибка */ }
} else { /* процесс-потомок */ } } Использование схемы fork-exec Программа порождает три процесса, каждый из которых запускает программу echo посредством системного вызова exec(). Данный пример демонстрирует важность проверки успешного завершения системного вызова exec(), в противном случае возможно исполнение нескольких копий исходной программы. В нашем случае если все вызовы exec() проработают неуспешно, то копий программ будет восемь. Если все вызовы exec() будут успешными, то после последнего вызова fork() будет существовать четыре копии процесса. В любом случае, порядок, в котором они будут выполняться, не определен. #include <sys/types.h> #include <unistd.h> #include <stdio.h> 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(“процесс-предок закончился ”); return 0; } Результат работы может быть следующим. процесс-предок закончился это сообщение три это сообщение два это сообщение один Завершение процесса. Для завершения выполнения процесса предназначен системный вызов _exit() #include <unistd.h> void _exit(int exitcode); Этот вызов никогда не завершается неудачно, поэтому для него не предусмотрено возвращающего значения. С помощью параметра exit_code процесс может передать породившему его процессу информацию о статусе своего завершения. Принято, хотя и не является обязательным правилом, чтобы процесс возвращал нулевое значение при нормальном завершении, и ненулевое – в случае какой-либо ошибки или нештатной ситуации. В стандартной библиотеке Си имеется сервисная функция exit(), описанная в заголовочном файле stdlib.h, которая, помимо обращения к системному вызову _exit(), осуществляет ряд дополнительных действий, таких как, например, очистка стандартных буферов ввода-вывода. Кроме обращения к вызову _exit(), другими причинами завершения процесса могут быть: выполнение оператора return, входящего в состав функции main() получение некоторых сигналов (об этом речь пойдет чуть ниже) В любом из этих случаев происходит следующее: освобождаются сегмент кода и сегмент данных процесса закрываются все открытые дескрипторы файлов если у процесса имеются потомки, их предком назначается процесс с идентификатором 1 освобождается большая часть контекста процесса, однако сохраняется запись в таблице процессов и та часть контекста, в которой хранится статус завершения процесса и статистика его выполнения процессу-предку завершаемого процесса посылается сигнал SIGCHLD Состояние, в которое при этом переходит завершаемый процесс, в литературе часто называют состоянием “зомби”. Процесс-предок имеет возможность получить информацию о завершении своего потомка. Для этого служит системный вызов wait(): #include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status); При обращении к этому вызову выполнение родительского процесса приостанавливается до тех пор, пока один из его потомков не завершится либо не будет остановлен. Если у процесса имеется несколько потомков, процесс будет ожидать завершения любого из них (т.е., если процесс хочет получить информацию о завершении каждого из своих потомков, он должен несколько раз обратиться к вызову wait()). Возвращаемым значением wait() будет идентификатор завершенного процесса, а через параметр status будет возвращена информация о причине завершения процесса (завершен путем вызова _exit(), либо прерван сигналом) и коде возврата. Если процесс не интересуется это информацией, он может передать в качестве аргумента вызову wait() NULL-указатель. Конкретный формат данных, записываемых в параметр status, может различаться в разных реализациях ОС. Во всех современных версиях UNIX определены специальные макросы для извлечения этой информации, например: макрос WIFEXITED(*status) возвращает ненулевое значение, если процесс был завершен путем вызова _exit(), при этом макрос WEXITSTATUS(*status) возвращает статус завершения, переданный через _exit(); макрос WIFSIGNALED(*status) возвращает ненулевое значение, если процесс был прерван сигналом, при этом макрос WTERMSIG(*status) возвращает номер этого сигнала; макрос WIFSTOPPED(*status) возвращает ненулевое значение, если процесс был приостановлен системой управления заданиями, при этом макрос WSTOPSIG(*status) возвращает номер сигнала, c помощью которого он был приостановлен. Если к моменту вызова wait() один из потомков данного процесса уже завершился, перейдя в состояние зомби, то выполнение родительского процесса не блокируется, и wait() сразу же возвращает информацию об этом завершенном процессе. Если же к моменту вызова wait() у процесса нет потомков, системный вызов сразу же вернет –1. Также возможен аналогичный возврат из этого вызова, если его выполнение будет прервано поступившим сигналом. После того, как информация о статусе завершения процесса-зомби будет доставлена его предку посредством вызова wait(), все оставшиеся структуры, связанные с данным процессом-зомби, освобождаются, и запись о нем удаляется из таблицы процессов. Таким образом, переход в состояние зомби необходим именно для того, чтобы процесс-предок мог получить информацию о судьбе своего завершившегося потомка, независимо от того, вызвал он wait() до или после фактического его завершения. Что происходит с процессом-потомком, если его предок вообще не обращался к wait() и/или завершился раньше потомка? Как уже говорилось, при завершении процесса отцом для всех его потомков становится процесс с идентификатором 1. Он и осуществляет системный вызов wait(), тем самым освобождая все структуры, связанные с потомками-зомби. Часто используется сочетание функций fork()-wait(), если процесс-сын предназначен для выполнения некоторой программы, вызываемой посредством функции exec(). Фактически этим предоставляется процессу-родителю возможность контролировать окончание выполнения процессов-потомков. Использование системного вызова wait() Пример программы, последовательно запускающей программы, имена которых указаны при вызове. #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> #include <stdio.h> int main(int argc, char **argv) { int i; for (i=1; i<argc; i++) { int status; if(fork()>0) { /*процесс-предок ожидает сообщения от процесса-потомка о завершении */ wait(&status); printf(“process-father ”); continue; } execlp(argv[i], argv[i], 0); return -1; /*попадем сюда при неуспехе exec()*/ } return 0; } Пусть существуют три исполняемых файла print1, print2, print3, каждый из которых только печатает текст first, second, third соответственно, а код вышеприведенного примера находится в исполняемом файле с именем file. Тогда результатом работы команды file print1 print2 print3 будет first process-father second process-father third process-father Использование системного вызова wait() В данном примере процесс-предок порождает два процесса, каждый из которых запускает команду echo. Далее процесс-предок ждет завершения своих потомков, после чего продолжает выполнение. #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> #include <stdio.h> int main(int argc, char **argv) { if ((fork()) == 0) /*первый процесс-потомок*/ { execl(“/bin/echo”, ”echo”, ”this is”, ”string 1”, 0); return -1; } if ((fork()) == 0) /*второй процесс-потомок*/ { execl(“/bin/echo”, ”echo”, ”this is”, ”string 2”, 0); return -1; } /*процесс-предок*/ printf(“process-father is waiting for children ”); while(wait(NULL) != -1); printf(“all children terminated ”); return 0; } В данном случае wait() вызывается в цикле три раза – первые два ожидают завершения процессов-потомков, последний вызов вернет неуспех, ибо ждать более некого. |
| Оглавление| |