Системное программное обеспечение - Учебное пособие (Терехин А.Н.)

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() вызывается в цикле три раза – первые два ожидают завершения процессов-потомков, последний вызов вернет неуспех, ибо ждать более некого.