Лабораторная работа №2
Создание процессов
Цель работы - организация функционирования процессов заданной структуры и исследование их взаимодействия.
Теоретическая часть
Для создания процессов используется системный вызов fork:
#include
#include
pid_t fork (void);
В результате успешного вызова fork ядро создаёт новый процесс, который является почти точной копией вызывающего процесса. Другими словами, новый процесс выполняет копию той же программы, что и создавший его процесс, при этом все его объекты данных имеют те же самые значения, что и в вызывающем процессе.
Созданный процесс называется дочерним процессом, а процесс, осуществляющий вызов fork, называется родительским.
После вызова родительский процесс и его вновь созданный потомок выполняются одновременно, при этом оба процесса продолжают выполнение с оператора, который следует сразу же за вызовом fork.
Идею, заключённую в вызове fork, быть может, достаточно сложно понять тем, кто привык к схеме последовательного программирования. Ниже приведен пример, иллюстрирующий это понятие (рис. 2.1). На рисунке рассматриваются три строки кода, состоящие из вызова printf, за которым следуют вызов fork и ещё один вызов printf. Рисунок разбит на две части: До и После. Часть рисунка До показывает состояние до вызова fork. Существует единственный процесс А (его обозначили буквой А только для удобства, для системы это ничего не значит). Стрелка, обозначенная РС (Program counter – программный счётчик), указывает на выполняемый в настоящий момент оператор. Так как стрелка указывает на первый оператор printf, на стандартный вывод выдаётся тривиальное сообщение One.
Часть рисунка После показывает ситуацию сразу же после вызова fork. Теперь существуют два выполняемых одновременно процесса: А и В. Процесс А – это тот же самый процесс, что и в части рисунка До. Процесс В – это новый процесс, порождённый вызовом fork. Этот процесс является копией процесса А, кроме одного важного исключения – он имеет другое значение идентификатора (процесса pid), но выполняет ту же самую программу, что и процесс А, т. е. те же три строки исходного кода, приведённые на рисунке. В соответствии с введенной выше терминологией процесс А является родительским процессом, а процесс В – дочерним. Две стрелки с надписью РС в этой части рисунка
print (“One\n”);
pid = fork ();
printf (“Two\n”);
A
PC
print (“One\n”);
pid = fork ();
printf (“Two\n”);
A
PC
print (“One\n”);
pid = fork ();
printf (“Two\n”);
B
PC
До
После
fork
Рис. 2.1. Вызов fork
показывают, что следующим оператором, который выполняется родителем и потомком после вызова fork, является вызов printf. Другими словами, оба процесса А и В продолжают выполнение с той же точки кода программы, хотя процесс В и является новым процессом для системы. Поэтому сообщение Two выводится дважды.
Вызов fork не имеет аргументов и возвращает идентификатор процесса pid_t. Родитель и потомок отличаются значением переменной pid: в родительском процессе значение переменной pid будет ненулевым положительным числом, для потомка же оно равно нулю. Так как возвращаемые в родительском и дочернем процессе значения различаются, то программист может задавать различные действия для двух процессов.
Следующая короткая программа более наглядно показывает работу вызова fork и использование процесса:
#include
main ()
{
pid_t pid; /*process-id в родительском процессе */
printf (“Пока всего один процесс\n”);
printf (“Вызов fork … \n”);
pid = fork (); /*Создание нового процесса */
if (pid = = 0)
printf (“Дочерний процесс\n”);
else if (pid > 0)
printf (“Родительский процесс, pid потомка %d\n, pid”);
else
printf (“Ошибка вызова fork, потомок не создан\n”);
}
Оператор if, следующий за вызовом fork, имеет три ветви. Первая определяет дочерний процесс, соответствующий нулевому значению переменной pid. Вторая задаёт действия для родительского процесса, соответствуя положительному значению переменной pid. Третья ветвь неявно соответствует отрицательному (а на самом деле равно –1) значению переменной pid, которое возвращается, если вызову fork не удаётся создать дочерний процесс. Это может означать, что вызывающий процесс попытался нарушить ограничения (например – число процессов одновременно выполняющихся и запущенных одним пользователем). В обоих случаях переменная errno содержит код ошибки EAGAIN. Обратите также внимание на то, что поскольку оба процесса, созданных программой, будут выполняться одновременно без синхронизации, то нет гарантии, что вывод родительского и дочернего процессов не будет смешиваться.
Для смены исполняемой программы можно использовать функции семейства exec. Основное отличие между разными функциями в семействе состоит в способе передачи параметров. Как видно из рис. 2.2, все эти функции выполняют один системный вызов execve.
execl
execle
execve
execvp
execv
execlp
Рис. 2.2. Дерево семейства вызовов exec
Все множество системных вызовов exec выполняет одну и ту же функцию: они преобразуют вызывающий процесс, загружая новую программу в его пространство памяти. Вызов exec не создает новый подпроцесс, который выполняется одновременно с вызывающим, а вместо этого новая программа загружается на место старой, поэтому успешный вызов exec не возвращает значения.
#include
/* Для семейства вызовов execl аргументы должны быть списком, заканчивающимся NULL*/
/* Вызову execl нужно передать полный путь к файлу программы */
int execl (const char *path, const char *arg0,..., const char argn, (char *)0);
/* Вызову execlp нужно только имя файла */
int execlp (const char *file, const char *arg0,..., const char argn, (char *)0);
/* Для семейства вызовов execv нужно передать массив аргументов */
int execv (const char *path, char *const argv[]);
int execvp (const char *file, char *const argv[]);
Следующая программа использует вызов execl для запуска программы вывода содержимого каталога ls:
#include
main()
{
printf (“Запуск программы ls\n”);
execl (“/bin/ls”, “ls”, “-l”, (char*)0);
/* Если execl возвращает значение, то вызов был неудачным*/
perror(“Вызов execl не смог запустить программу ls”);
exit(1);
}
Работа этой программы показана на рис. 2.3.
PC
printf (...)
execl (“bin/ls”,...);
run 1s
До
exec
После
Команда 1s
PC
/*1st line of ls*/
Рис. 2.3. Вызов exec
Другие формы вызова exec упрощают задание списков параметров запуска загружаемой программы. Вызов execv принимает два аргумента: первый является строкой, которая содержит полное имя и путь к запускаемой программе. Второй аргумент является массивом строк. Первый элемент этого массива указывает на имя запускаемой программы (исключая префикс пути). Оставшиеся элементы указывают на все остальные аргументы программы. Следующий пример использует вызов execv для запуска той же программы ls, что и в предыдущем примере:
include
main()
{
char * const av[]={“ls”, “-l”, (char *)0};
execv(“/bin/ls”, av);
/* Если мы оказались здесь, то произошла ошибка*/
perror(“execv failed”);
exit(1);
}
Функции execlp и execvp почти эквивалентны функциям execl и execv. Основное отличие – первый аргумент есть просто имя программы, а не полный путь к ней.
Системные вызовы fork и exec, объединенные вместе, представляют мощный инструмент для программиста. Благодаря ветвлению при использовании вызова exec во вновь созданном дочернем процессе программа может выполнять другую программу в дочернем процессе, не стирая себя из памяти. Следующий пример показывает, как это можно сделать:
include
main()
{
pid_t pid;
switch (pid = fork()) {
case -1:
fatal(“Ошибка вызова fork”);
break;
case 0:
/* Потомок вызывает exec */
execl (“/bin/ls”, “ls”, “-l”, (char *)0);
fatal(“Ошибка вызова exec”);
break;
default:
/* Родительский процесс вызывает wait для приостановки */
/* работы до завершения дочернего процесса. */
wait ( (int *)0);
printf (“ Программа ls завершилась\n”);
exit (0);
}
}
Процедура fatal реализована следующим образом:
int fatal (char s)
{
perror (s);
exit (1);
}
Совместное использование fork и exec изображено на рис. 2.4.
Рисунок разбит на три части: До вызова fork, После вызова fork и После вызова exec. В начальном состоянии, До вызова fork, существует единственный процесс А и программный счетчик РС направлен на оператор fork, показывая, что это следующий оператор, который должен быть выполнен.
После вызова fork существует два процесса – А и В. Родительский процесс А выполняет системный вызов wait, что приведет к приостановке выполнения процесса А до тех пор, пока процесс В не завершится. В это время процесс В использует вызов execl для запуска на выполнение команды ls. Что происходит дальше, показано в части После вызова exec на рис. 2.4. Процесс В изменился и теперь выполняет программу ls. Программный счетчик процесса В установлен на первый оператор команды ls. Так как процесс А ожидает завершения процесса В, то положение его программного счетчика РС не изменилось.
pid = fork();
PC
A
pid = fork();
PC
A
wait((int*)0);
wait((int*)0);
PC
A
PC
exec(“/bin/ls”,...);
PC
B
/*1st line of ls*/
PC
B (Теперь выполняет команду ls)
До вызова fork
После вызова fork
После вызова exec
A
Рис. 2.4. Совместное использование вызовов fork и exec
Достарыңызбен бөлісу: |