Функции execlp и execvp почти эквивалентны функциям execl и execv. Основное отличие – первый аргумент есть просто имя программы, а не полный путь к ней.
Системные вызовы fork и exec, объединенные вместе, представляют мощный инструмент для программиста. Благодаря ветвлению при использовании вызова exec во вновь созданном дочернем процессе программа может выполнять другую программу в дочернем процессе, не стирая себя из памяти. Следующий пример показывает, как это можно сделать:
/* работы до завершения дочернего процесса. */
Совместное использование fork и exec изображено на рис. 2.4.
Рисунок разбит на три части: До вызова fork, После вызова fork и После вызова exec. В начальном состоянии, До вызова fork, существует единственный процесс А и программный счетчик РС направлен на оператор fork, показывая, что это следующий оператор, который должен быть выполнен.
После вызова fork существует два процесса – А и В. Родительский процесс А выполняет системный вызов wait, что приведет к приостановке выполнения процесса А до тех пор, пока процесс В не завершится. В это время процесс В использует вызов execl для запуска на выполнение команды ls. Что происходит дальше, показано в части После вызова exec на рис. 2.4. Процесс В изменился и теперь выполняет программу ls. Программный счетчик процесса В установлен на первый оператор команды ls. Так как процесс А ожидает завершения процесса В, то положение его программного счетчика РС не изменилось.
Рис. 2.4. Совместное использование вызовов fork и exec Порядок выполнения работы
1. Изучить теоретическую часть лабораторной работы.
2. Вывести на экран содержимое среды окружения. Провести попытку изменить в среде окружения PATH, вводя дополнительный путь. Проверить факт изменения пути, предпринимая вызов exec.
3. В основной программе с помощью системного вызова fork создать процессы – отец и сын. Процесс-отец выполняет операцию формирования файла из символов N aaa bbb (где N – номер выводимой строки) и выводит формируемые строки в левой половине экрана в виде:
N pid aaa bbb, (где pid – pid отца)
а процесс-сын читает строки из файла и выводит их в правой части экрана, но со своим pid. Имя файла задаётся в качестве параметра. Отследить очерёдность работы процесса-отца и процесса-сына.
4. Разработать программу по условию п.3, но процесс-сын осуществляет, используя вызов exec(), перезагрузку новой программы, которая осуществляет те же функции, что и в п.3 (читает строки из файла и выводит их в правой части экрана). В перезагружаемую программу необходимо передать имя файла для работы.
5. Разработать программу «интерпретатор команд», которая воспринимает команды, вводимые с клавиатуры, и осуществляет их корректное выполнение. Предусмотреть контроль ошибок.
Лабораторная работа №3
Взаимодействие процессов
Цель работы – создание и изучение взаимодействия процессов, созданных при помощи вызова fork.
Взаимодействие процессов
Теоретическая часть
Созданный при помощи вызова fork дочерний процесс является почти точной копией родительского. Все переменные в дочернем процессе будут иметь те же самые значения, что и в родительском (единственным исключением является значение, возвращаемое самим вызовом fork). Так как данные в дочернем процессе являются копией данных в родительском процессе и занимают другое абсолютное положение в памяти, важно знать, что последующие изменения в одном процессе не будут затрагивать переменные в другом.
Аналогично все файлы, открытые в родительском процессе, также будут открытыми и в потомке, при этом дочерний процесс будет иметь свою копию связанных с каждым файлом дескрипторов. Тем не менее файлы, открытые до вызова fork, остаются тесно связанными в родительском и дочернем процессах. Это обусловлено тем, что указатель чтения-записи для каждого из таких файлов используется совместно родительским и дочерним процессами благодаря тому, что он поддерживается системой и существует не только в самом процессе. Следовательно, если дочерний процесс изменяет положение указателя в файле, то в родительском процессе он также окажется в новом положении. Это поведение демонстрирует следующая программа, в которой использованы процедура fatal, описанная в предыдущей лабораторной работе, а также новая процедура printpos. Дополнительно введено допущение, что существует файл с именем data длиной не меньше 20 символов:
#include <unistd.h>
#include <fcntl.h>
main()
{
int fd;
pid_t pid; /*Идентификатор процесса*/
char buf [10]; /*Буфер данных для файла*/
if (( fd = open ( “data”, O_RDONLY)) == -1)
fatal (“Ошибка вызова open”);
read (fd, buf, 10); /* Переместить вперед указатель файла */
printpos (“До вызова fork”, fd);
/* Создать два процесса */
switch (pid = fork ()) {
case -1: /* Ошибка */
fatal (“Ошибка вызова fork ”);
break;
case 0: /* Потомок */
printpos (“Дочерний процесс до чтения”, fd);
read (fd, buf, 10);
printpos (“Дочерний процесс после чтения”, fd);
break;
default: /* Родитель */
wait ( (int *) 0);
printpos (“Родительский процесс после ожидания”, fd);
}
}
Процедура printpos может быть реализована следующим образом:
int printpos ( const char *string, int filedes)
{
off_t pos;
if ((pos = lseek (filedes, 0, SEEK_CUR)) == -1)
fatal (“Ошибка вызова lseek”);
printf (“%s:%ld\n”, string, pos);
}
Результаты, полученные после выполнения данной программы:
До вызова fork : 10
Дочерний процесс до чтения : 10
Дочерний процесс после чтения : 20
Родительский процесс после ожидания : 20
Дочерний процесс до чтения : 10
Системный вызов exit уже известен, но теперь следует дать его правильное описание. Этот вызов используется для завершения процесса, хотя это также происходит, когда управление доходит до конца тела функции main или до оператора return в функции main. Описание exit:
#include <stdlib.h>
void exit ( int status);
Единственный целочисленный аргумент вызова exit называется статусом завершения (exit status) процесса, младшие 8 бит которого доступны родительскому процессу при условии, если он выполнил системный вызов wait. При этом возвращаемое вызовом exit значение обычно используется для определения успешного или неудачного завершения выполнявшейся процессом задачи. По принятому соглашению нулевое возвращаемое значение соответствует нормальному завершению, а ненулевое значение говорит о том, что что-то случилось.
Кроме завершения вызывающего его процесса вызов exit имеет еще несколько последствий: наиболее важным из них является закрытие всех открытых дескрипторов файлов.
Процедура atexit регистрирует функцию, на которую указывает ссылка func, которая будет вызываться без параметров. Каждая из заданных в процедуре atexit функций будет вызываться при выходе в порядке, обратном порядку их расположения. Описание atexit:
#include <stdlib.h>
int atexit (void (*func) (void));
Вызов wait временно приостанавливает выполнение процесса, в то время как дочерний процесс продолжает выполняться. После завершения дочернего процесса выполнение родительского процесса продолжится. Если запущено более одного дочернего процесса, то возврат из вызова wait произойдет после выхода из любого из потомков. Описание wait:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait (int *status);
Вызов wait часто осуществляется родительским процессом после вызова fork. Сочетание вызовов fork и wait наиболее полезно, если дочерний процесс предназначен для выполнения совершенно другой программы при помощи вызова exec.
Возвращаемое значение wait обычно является идентификатором дочернего процесса, который завершил свою работу. Если вызов wait возвращает значение (pid_t) -1, это может означать, что дочерние процессы не существуют, и в этом случае переменная errno будет содержать код ошибки ECHILD. Возможность определить завершение каждого из дочерних процессов по отдельности означает, что родительский процесс может выполнять цикл, ожидая завершения каждого из потомков, а после того, как все они завершатся, продолжать свою работу.
Вызов wait принимает один аргумент, status – указатель на целое число. Если указатель равен NULL, то аргумент просто игнорируется. Если же вызову wait передается допустимый указатель, то после возврата из вызова wait переменная status будет содержать полезную информацию о статусе завершения процесса. Обычно эта информация будет представлять собой код завершения дочернего процесса, переданный при помощи вызова exit.
Следующая программа status показывает, как может быть использован вызов wait:
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
main()
{
pid_t pid;
int status, exit_status;
if ((pid = fork())<0)
fatal (“Ошибка вызова fork”);
if ( pid == 0) /* Потомок */
{
/* Вызвать библиотечную процедуру sleep*/
/* для временного прекращения работы на 4 секунды*/
sleep (4);
exit(5); /* Выход с ненулевым значением*/
}
/* Если мы оказались здесь, то это родительский процесс,*/
/* поэтому ожидать завершения дочернего процесса*/
if (( pid = wait (&status)) == -1)
{
perror (“Ошибка вызова wait”);
exit (2);
}
/* Проверка статуса завершения дочернего процесса*/
if (WIFEXITED (status))