Н.В. Вдовикина, А.В. Казунин, И.В. Машечкин, А.Н. Терехин - Системное программное обеспечение - взаимодействие процессов (2002), страница 6
Описание файла
Документ из архива "Н.В. Вдовикина, А.В. Казунин, И.В. Машечкин, А.Н. Терехин - Системное программное обеспечение - взаимодействие процессов (2002)", который расположен в категории "". Всё это находится в предмете "операционные системы" из 3 семестр, которые можно найти в файловом архиве МГУ им. Ломоносова. Не смотря на прямую связь этого архива с МГУ им. Ломоносова, его также можно найти и в других разделах. .
Онлайн просмотр документа "Н.В. Вдовикина, А.В. Казунин, И.В. Машечкин, А.Н. Терехин - Системное программное обеспечение - взаимодействие процессов (2002)"
Текст 6 страницы из документа "Н.В. Вдовикина, А.В. Казунин, И.В. Машечкин, А.Н. Терехин - Системное программное обеспечение - взаимодействие процессов (2002)"
- файлы, открытые в процессе-отце, за исключением тех, которым было запрещено передаваться процессам-потомкам с помощью задания специального параметра при открытии. (Речь идет о том, что в системе при открытии файла с файлом ассоциируется некоторый атрибут, который определяет правила передачи этого открытого файла сыновним процессам. По умолчанию открытые в «отце» файлы можно передавать «потомкам», но можно изменить значение этого параметра и блокировать передачу открытых в процессе-отце файлов.);
- способы обработки сигналов;
- разрешение переустановки эффективного идентификатора пользователя;
- разделяемые ресурсы процесса-отца;
- текущий рабочий каталог и домашний каталоги
- и т.д.
Не наследуются порожденным процессом следующие атрибуты родительского процесса:
-
идентификатор процесса (PID)
-
идентификатор родительского процесса (PPID)
-
сигналы, ждущие доставки в родительский процесс
-
время посылки предупреждающего сигнала, установленное системным вызовом alarm() (в порожденном процессе оно сбрасывается в нуль)
-
блокировки файлов, установленные родительским процессом
По завершении системного вызова fork() каждый из процессов – родительский и порожденный – получив управление, продолжат выполнение с одной и той же инструкции одной и той же программы, а именно с той точки, где происходит возврат из системного вызова fork(). Вызов fork() в случае удачного завершения возвращает сыновнему процессу значение 0, а родительскому PID порожденного процесса. Это принципиально важно для различения сыновнего и родительского процессов, так как сегменты кода у них идентичны. Таким образом, у программиста имеется возможность разделить путь выполнения инструкций в этих процессах.
В случае неудачного завершения, т.е. если сыновний процесс не был порожден, системный вызов fork() возвращает –1, код ошибки устанавливается в переменной errno.
Рис. 9 Выполнение системного вызова fork()
-
Порождение сыновнего процесса. Идентификаторы процессов.
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv)
{
printf("PID=%d; PPID=%d \n",getpid(), getppid());
/*печать PID текущего процесса и PID процесса-предка */
fork();
/*создание нового процесса, с этого момента два процесса функционируют параллельно и независимо*/
printf("PID=%d; PPID=%d \n",getpid(), getppid());
/*оба процесса печатают PID текущего процесса и PID процесса-предка*/
return 0;
}
В этом примере оба процесса узнают свой собственный идентификатор процесса с помощью вызова getpid(), а идентификатор родительского процесса – с помощью вызова getppid().
Следует отметить два момента, связанных с функционированием двух процессов. Во-первых, нельзя определенно сказать, в каком порядке будет происходить печать с момента появления двух процессов – это будет определяться планировщиком процессов. Во-вторых, ответ на вопрос - какой идентификатор родительского процесса распечатает вновь созданный процесс, если процесс-предок завершит свою работу раньше, будет приведен несколько ниже.
-
Порождение сыновнего процесса. Одновременное выполнение.
Программа создает два процесса – процесс-предок распечатывает заглавные буквы, а процесс-потомок строчные.
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv)
{
char ch, first, last;
int pid;
if((pid=fork())>0)
{
/*процесс-предок*/
first =’A’;
last =’Z’;
}
else
{
/*процесс-потомок*/
first =’a’;
last =’z’;
}
for (ch = first; ch <= last; ch++)
{
write(1,&ch,1);
}
return 0;
}
Оба процесса распечатывают буквы одним и тем же оператором for. Оба процесса имеют возможность получить управление, таким образом любой из них может начать исполнение первым.
4.4Механизм замены тела процесса.
Семейство системных вызовов 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 \n”);
…
}
В данном случае второй параметр – вектор из указателей на параметры строки, которые будут переданы в вызываемую программу. Как и ранее первый указатель – имя программы, последний – нулевой указатель. Эти вызовы удобны, когда заранее неизвестно число аргументов вызываемой программы.
-
Вызов программы компиляции.
#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(“ошибка\n”);
}
if(fork()==0)
{
execl(“/bin/echo”, ”echo”, ”это”, ”сообщение два”, NULL);
printf(“ошибка\n”);
}
if(fork()==0)
{
execl(“/bin/echo”, ”echo”, ”это”, ”сообщение три”, NULL);
printf(“ошибка\n ”);
}
printf(“процесс-предок закончился\n”);
return 0;
}
Результат работы может быть следующим.
процесс-предок закончился
это сообщение три
это сообщение два
это сообщение один
4.5Завершение процесса.
Для завершения выполнения процесса предназначен системный вызов _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) возвращает номер этого сигнала;