Теория вычислительных процессов и структур
Алексеев Игорь Геннадиевич, Бранцевич Петр Юльянович
Учебно-методическое пособие для студентов специальности «Программное обеспечение информационных технологий» дневной формы обучения
Белорусский государственный университет информатики и радиоэлектроники
Минск 2004
В пособии рассмотрены основные команды операционной системы UNIX, предназначенные для работы с файлами и каталогами, а также для создания процессов и организации взаимодействия между ними. Даны структуры лабораторных работ по курсу «ТВПиС»
1. Основные команды ос unix
Вход в систему и выход
В ответ на приглашение системы ввести Logon вводим: sxtxx, например s5t03, где 5 – номер Вашей группы, а 03 – Ваш порядковый номер в группе. Затем после входа в систему устанавливаем с помощью команды passwd свой пароль длиной не менее 6 символов. Не забывайте свой логин и пароль! Пароль нельзя восстановить!
Пароль в зашифрованном виде находится в каталоге ./etс в файле shadow и для его сброса необходимо удалить набор символов после имени пользователя между двоеточиями. Например, пользователь stud1, запись в файле shadow:
stud1:gdwiefu@#@#$%66reHHrrnCvcn:12060:………
после удаления пароля запись должна быть следующая:
stud1::12060:………
Выход из системы можно осуществить по команде exit
Рабочие каталоги и файлы
Ваш рабочий каталог: /home/sxtxx, где x и xx – номер группы и порядковый номер студента в группе.
Включаемые файлы типа: stdio.h, stdlib.h и т.п. находятся в каталоге: /usr/include/
Работа с каталогами и файлами
Для вывода содержимого текущего каталога можно использовать команду: dir или ls, для изменения текущего каталога – команду: cd.
Для вывода полного имени текущего каталога можно использовать команду: pwd, для создания или удаления каталога – команды: mkdir и rmdir.
Для вывода на терминал содержимого файла можно использовать команду: cat имя_файла, например: cat prog.txt.
Для вызова файл-менеджера типа Norton`а набираем: mc (вызов оболочки файл-менеджера Midnight Commander) и далее работаем с его меню.
Для вызова текстового редактора набираем: joe или joe имя_создаваемого_или_редактируемого_файла. В самом редакторе практически все команды начинаются с последовательности ctrl-k, и нажатия нужного символа. Например, ctrl-k h выведет справку по основным командам редактора, а ctrl-k x завершит работу редактора с сохранением редактируемого файла.
Работа с программами и процессами
Запуск программы на выполнение:
./имя_программы например: ./prog1.exe
Для компиляции программ на С/С++ вызываем компилятор:
cc имя_входного_файла – о имя_выходного_файла или:
gcc имя_входного_файла – о имя_выходного_файла ,
где имя_входного_файла обязательно должно быть с расширением *.с или *.cpp, а имя_выходного_файла может быть любым (желательно совпадать с именем входного файла, кроме расширения).
Например: cc myprog1.c –o myprog1
или
gcc myprog1.c –o myprog1
Для вывода списка запущенных процессов можно использовать команду:
ps, например: ps –x выведет список всех запущенных процессов.
Для снятия задачи (процесса) можно использовать команду: kill pid_процесса, предварительно узнав его pid командой ps.
В каталоге ./proc находятся сведения обо всех запущенных процессах в системе, их состоянии, распределении памяти и т.д.
Типовой вид каталога:
./proc/1081/……..,
./proc/1085/………, где 1081 и 1082 соответственно pid запущенных процессов в системе.
Справку по командам системы или по языку С можно получить по команде:
man имя_команды, например: man ls
2. Лабораторные работы
Лабораторная работа №1
Работа с файлами и каталогами ОС UNIX
Цель работы – изучить основные команды ОС UNIX для работы с файлами и каталогами.
Теоретическая часть
Для выполнения операций записи и чтения данных в существующем файле его следует открыть при помощи системного вызова open. Ниже приведено описание этого вызова:
# include
# include
# include
int open (const char *pathname, int flags, (mode_t mode));
Первый аргумент, pathname, является указателем на строку маршрутного имени открываемого файла. Значение pathname может быть абсолютным путём, например: /usr / keith / junk. Данный путь задаёт положение файла по отношению к корневому каталогу. Аргумент pathname может также быть относительным путём, задающим маршрут от текущего каталога к файлу, например: keith / junk или просто junk. В последнем случае программа откроет файл junk в текущем каталоге. В общем случае, если один из аргументов системного вызова или библиотечной процедуры – имя файла, то в качестве него можно задать любое допустимое маршрутное имя файла UNIX.
Второй аргумент системного вызова open - flags - имеет целочисленный тип и определяет метод доступа. Параметр flags принимает одно из значений, заданных постоянными в заголовочном файле
O_RDONLY – открыть файл только для чтения,
O_WRONLY – открыть файл только для записи,
O_RDWR – открыть файл для чтения и записи.
В случае успешного завершения вызова open и открытия файла возвращаемое вызовом open значение будет содержать неотрицательное целое число – дескриптор файла. В случае ошибки вызов open возвращает вместо дескриптора файла значение –1. Это может произойти, например, если файл не существует.
Третий параметр mode, является необязательным, он используется только вместе с флагом O_CREAT.
Следующий фрагмент программы открывает файл junk для чтения и записи и проверяет, не возникает ли при этом ошибка. Этот последний момент особенно важен: имеет смысл устанавливать проверку ошибок во все программы, которые используют системные вызовы, поскольку каким бы простым ни было приложение, иногда может произойти сбой. В приведенном ниже примере используются библиотечные процедуры printf для вывода сообщения и exit – для завершения процесса:
# include
# include
char workfile=”junk”; / Задать имя рабочего файла */
main()
{
int filedes;
/* Открыть файл, используя постоянную O_RDWR из
/* Файл открывается для чтения / записи */
if ((filedes=open(workfile, O_RDWR)) = = -1)
{
printf (“Невозможно открыть %sn”, workfile);
exit (1); /* Выход по ошибке */
}
/* Остальная программа */
exit (0); /* Нормальный выход */
}
Вызов open может использоваться для создания файла, например:
filedes = open (“/tmp/newfile”, O_WRONLY | O_CREAT, 0644);
Здесь объединены флаги O_CREAT и O_WRONLY, задающие создание файла /tmp/newfile при помощи вызова open. Если /tmp/newfile не существует, то будет создан файл нулевой длины с таким именем и открыт только для записи.
Параметр mode содержит число, определяющее права доступа к файлу, указывающие, кто из пользователей системы может осуществлять чтение, запись или выполнение файла. Пользователь, создавший файл, может выполнять чтение из файла и запись в него. Остальные пользователи будут иметь доступ только для чтения файла.
Следующая программа создаёт файл newfile в текущем каталоге:
# include
# include
#define PERMS 0644 /* Права доступа при открытии с O_CREAT */
char *filename=”newfile”;
main()
{
int filedes;
if ((filedes=open (filename, O_RDWR | O_CREAT, PERMS)) = = -1)
{
printf (“Невозможно открыть %sn”, filename);
exit (1); /* Выход по ошибке */
}
/* Остальная программа */
exit (0);
}
Другой способ создания файла заключается в использовании системного вызова creat. Так же, как и вызов open, он возвращает либо ненулевой дескриптор файла, либо –1 в случае ошибки. Если файл успешно создан, то возвращаемое значение является дескриптором этого файла, открытого для записи. Вызов creat осуществляется так:
# include
# include
# include
int creat (const char *pathname, mode_t mode);
Первый параметр pathname указывает на маршрутное имя файла UNIX, определяющее имя создаваемого файла и путь к нему. Так же, как и в случае вызова open, параметр mode задаёт права доступа. При этом, если файл существует, то второй параметр также игнорируется. Тем не менее, в отличие от вызова open, в результате вызова creat файл всегда будет усечён до нулевой длины. Пример использования вызова creat:
filedes = creat (“/tmp/newfile”, 0644);
что эквивалентно вызову:
filedes = open (“/tmp/newfile”, O_WRONLY | O_CREAT | O_TRUNC, 0644);
Следует отметить, что вызов creat всегда открывает файл только для записи. Например, программа не может создать файл при помощи creat, записать в него данные, затем вернуться назад и попытаться прочитать данные из файла, если предварительно не закроет его и не откроет снова при помощи вызова open.
Библиотечная процедура fopen является эквивалентом вызова open:
#include
FILE *fopen (const char *filename, const char *type);
Процедура fopen открывает файл, заданный параметром filename, и связывает с ним структуру FILE. В случае успешного завершения процедура fopen возвращает указатель на структуру FILE, идентифицирующую открытый файл; объект FILE * также часто называют открытым потоком ввода / вывода (эта структура FILE является элементом внутренней таблицы). В случае неудачи процедура fopen возвращает нулевой указатель NULL. При этом, так же, как и для open, переменная errno будет содержать код ошибки, указывающий на её причину.
Второй параметр процедуры fopen указывает на строку, определяющую режим доступа. Она может принимать следующие основные значения:
r - открыть файл filename только для чтения (если файл не существует, то процедура fopen вернёт нулевой указатель NULL);
w - создать файл filename и открыть его только для записи (если файл не существует, то он будет усечён до нулевой длины);
а - открыть файл filename только для записи, все данные будут добавляться в конец файла (если файл не существует, он создаётся).
Следующий пример программы показывает использование процедуры fopen. При этом, если файл indata существует, то он открывается для чтения, а файл outdata создаётся (или усекается до нулевой длины, если он существует). Процедура fatal предназначена для вывода сообщения об ошибке. Она просто передаёт свой аргумент процедуре perror, а затем вызывается exit для завершения работы программы:
#include
char *inname = “indata”;
char *outname = “outdata”;
main()
{
FILE *inf, *outf;
if ((inf = fopen (inname, “r”)) = = NULL)
fatal (“Невозможно открыть входной файл”);
if ((outf = fopen (outname, “w”)) = = NULL)
fatal (“Невозможно открыть выходной файл”);
/* Выполняются какие-либо действия */
exit (0);
}
Основные процедуры для ввода строк называются gets и fgets:
# include
char *gets (char *buf);
char *fgets (char *buf, int nsize, FILE *inf);
Процедура gets считывает последовательность символов из потока стандартного ввода (stdin), помещая все символы в буфер, на который указывает аргумент buf. Символы считываются до тех пор, пока не встретится символ перевода строки или конца файла. Символ перевода строки newline отбрасывается, и вместо него в буфер помещается нулевой символ, образуя завершённую строку. В случае возникновения ошибки или при достижении конца файла возвращается значение NULL.
Процедура fgets является обобщённой версией процедуры gets. Она считывает из потока inf в буфер buf до тех пор, пока не будет считано nsize-1 символов или не встретится раньше символ перевода строки newline, или не будет достигнут конец файла. В процедуре fgets символы перевода строки newline не отбрасываются, а помещаются в конец буфера (это позволяет вызывающей функции определить, в результате чего произошёл возврат из процедуры fgets). Как и процедура gets, процедура fgets возвращает указатель на буфер buf в случае успеха и NULL – в противном случае.
Следующая процедура yesno использует процедуру fgets для получения положительного или отрицательного ответа от пользователя, она также вызывает макрос isspace для пропуска пробельных символов в строке ответа:
# include
# include
#define YES 1
#define NO 0
#define ANSWSZ 80
static char *pdefault = “Наберите ‘y’ (YES), или ‘n’ (NO)”;
static char *error = “Неопределённый ответ”;
int yesno (char *prompt)
{
char buf (ANSWSZ), *p_use, *p;
/* Выводит приглашение, если оно не равно NULL
Иначе использует приглашение по умолчанию pdefault */
p_use = (prompt != NULL) ? prompt : pdefault;
/* Бесконечный цикл до получения правильного ответа */
for (;;)
{
/* Выводит приглашение */
printf (“%s >”, p_use );
if (fgets (buf, ANSWSZ, stdin) = = NULL)
return EOF;
/* Удаляет пробельные символы */
for (p = buf; isspace (*p); p++)
switch (*p)
{
case ‘Y’:
case ‘y’:
return (YES);
case ‘N’:
case ‘n’:
return (NO);
default:
printf (“n%sn”, error);
}
}
}
Обратными процедурами для gets и fgets будут соответственно процедуры puts и fputs:
# include
int puts (const char *string);
int fputs (const char *string, FILE *outf);
Процедура puts записывает все символы (кроме завершающего нулевого символа) из строки string на стандартный вывод (stdout). Процедура fputs записывает строку string в поток outf. Для обеспечения совместимости со старыми версиями системы процедура puts добавляет в конце символ перевода строки, процедура же fputs не делает этого. Обе функции возвращают в случае ошибки значение EOF.
Для осуществления форматированного вывода используются процедуры printf и fprintf:
# include
int printf (const char *fmt, arg1, arg2 … argn);
int fprintf (FILE *outf, const char *fmt, arg1, arg2 … argn);
Каждая из этих процедур получает строку формата вывода fmt и переменное число аргументов произвольного типа, используемых для формирования выходной строки вывода. В выходную строку выводится информация из параметров arg1 … argn согласно формату, заданному аргументом fmt . В случае процедуры printf эта строка затем копируется в stdout. Процедура fprintf направляет выходную строку в файл outf.
Для каждого из аргументов arg1 … argn должна быть задана своя спецификация формата, которая указывает тип соответствующего аргумента и способ его преобразования в выходную последовательность символов ASCII.
Рассмотрим пример, демонстрирующий использование формата процедуры printf в двух простых случаях:
int iarg = 34;
…
printf (“Hello, world!n”);
printf (“Значение переменной iarg равно %dn”, iarg);
Результат:
Hello, world!
Значение переменной iarg равно 34
Возможные типы спецификаций (кодов) формата:
Целочисленные форматы:
%d - общеупотребительный код формата для значений типа int. Если значение является отрицательным, то будет автоматически добавлен знак минуса;
%u - тип unsigned int, выводится в десятичной форме;
%o - тип unsigned int, выводится как восьмеричное число без знака;
%x - тип unsigned int, выводится как шестнадцатеричное число без знака;
%ld - тип long со знаком, выводится в десятичной форме.
Можно также использовать спецификации %lo, %lu, %x.
Форматы вещественных чисел:
%f - тип float или double, выводится в стандартной десятичной форме;
%е - тип float или double, выводится в экспоненциальной форме (для обозначения экспоненты будет использоваться символ е);
%g - объединение спецификаций %e и %f - аргумент имеет тип float или double в зависимости от величины числа, оно будет выводиться либо в обычном формате, либо в формате экспоненциальной записи.
Форматирование строк и символов:
%c - тип char, выводится без изменений, даже если является «непечатаемым» символом (численное значение символа можно вывести, используя код формата для целых чисел, это может понадобиться при невозможности отображения символа на терминале);
%s - соответствующий аргумент считается строкой ( указателем на массив символов). Содержимое строки передаётся дословно в выходной поток, строка должна заканчиваться нулевым символом.
Спецификации формата могут также включать информацию о минимальной ширине поля, в котором выводится аргумент, и точности. В случае целочисленного аргумента под точностью понимается максимальное число выводимых цифр. Если аргумент имеет тип float или double, то точность задаёт число цифр после десятичной точки. Для строчного аргумента этот параметр определяет число символов, которые будут взяты из строки. Например, могут использоваться такие записи: %10.5d; %.5f; %10s; %-30s.
Функция fprintf может использоваться для вывода диагностических ошибок:
#include
#include
int notfound (const char *progname, const char *filename)
{ fprintf (stderr, “%s: файл %s не найденn”,progname, filename);
exit (1); }
Для опроса состояния структуры FILE существует ряд простых функций. Одна из них - функция feof:
#include
int feof (FILE *stream);
Функция feof является предикатом, возвращающим ненулевое значение, если для потока stream достигнут конец файла. Возврат нулевого значения просто означает, что этого ещё не произошло.
Функция main:
int main( int argc , char *argv( ) (, char *envp( ) ) );
Данное объявление позволяет удобно передавать аргументы командной строки и переменные окружения. Определение аргументов:
argc - количество аргументов, которые содержатся в argv() (всегда больше либо равен 1);
argv - в массиве строки представляют собой параметры из командной строки, введенные пользователем программы. По соглашению, argv (0) – это команда, которой была запущена программа, argv(1) – первый параметр из командной строки и так далее до argv (argc) – элемент, всегда равный NULL;
envp - массив envp общее расширение, существующее во многих UNIX® системах. Это массив строк, которые представляют собой переменные окружения. Массив заканчивается значением NULL.
Следующий пример показывает, как использовать argc, argv и envp в функции main:
#include
#include
void main( int argc, char * argv (), char *envp() )
{
int iNumberLines = 0; /* По умолчанию нет аргументов */
if( argc == 2 && strcmp(argv(1), "/n" ) == 0 )
iNumberLines = 1;
/* Проходим список строк пока не NULL */
for( int i = 0; envp(i) != NULL; ++i )
{
if( iNumberLines )
cout << i << ": " << envp(i) << "n";
}
}
Для работы с каталогами существуют системные вызовы:
int mkdir (const char *pathname, mode_t mode) – создание нового каталога,
int rmdir(const char *pathname) – удаление каталога.
Первый параметр – имя создаваемого каталога, второй – права доступа:
retval=mkdir(“/home/s1/t12/alex”,0777);
retval=rmdir(“/home/s1/t12/alex”);
Заметим, что вызов rmdir(“/home/s1/t12/alex”) будет успешен, только если удаляемый каталог пуст, т.е. содержит записи “точка” ( . ) и “двойная точка” (..).
Для открытия или закрытия каталогов существуют вызовы:
#include
DIR *opendir (const char *dirname);
int closedir( DIR *dirptr);
Пример вызова:
if ((d= opendir (“/home/s1”))==NULL) /* ошибка открытия */ exit(1);
Передаваемый вызову opendir параметр является именем открываемого каталога. При успешном открытии каталога dirname вызов opendir возвращает указатель на переменную типа DIR. Определение типа DIR, представляющего дескриптор открытого каталога, находится в заголовочном файле “dirent.h”.
В частности, поле name структуры DIR содержит запись имени файла, содержащегося в каталоге:
DIR *d;
ff=d->name ;
printf(“%sn”, ff);
Указатель позиции ввода/вывода после открытия каталога устанавливается на первую запись каталога. При неуспешном открытии функция возвращает значение NULL. После завершения работы с каталогом необходимо его закрыть вызовом closedir.
Для чтения записей каталога существует вызов:
struct dirent *readdir(DIR *dirptr);
Пример вызова:
DIR *dp;
struct dirent *d;
d=readdir(dp);
При первом вызове функции readdir в структуру dirent будет считана первая запись каталога. После прочтения всего каталога в результате последующих вызовов readdir будет возвращено значение NULL.
Для возврата указателя в начало каталога на первую запись существует вызов:
void rewindir(DIR *dirptr);
Чтобы получить имя текущего рабочего каталога существует функция:
char *getcwd(char *name, size_t size);
В переменную name при успешном вызове будут помещено имя текущего рабочего каталога:
char name1(255);
if (getcwd(name1, 255)==NULL) perror(“ошибка вызова”)
else printf(“текущий каталог=%s”,name1);
Вызов:
int chdir(const char *path);
изменяет текущий рабочий каталог на каталог path.
Системные вызовы stat и fstat позволяют процессу определить значения свойств в существующем файле:
#include
#include
int stat (const char *pathname, struct stat *buf);
int fstat (int filedes, struct stat *buf);
Системный вызов stat имеет два аргумента: pathname – полное имя файла, buf – указатель на структуру stat, которая после успешного вызова будет содержать связанную с файлом информацию.
Системный вызов fstat функционально идентичен системному вызову stat. Отличие состоит в интерфейсе: вместо полного имени файла вызов fstat ожидает дескриптор файла, поэтому он может использоваться только для открытых файлов.
Определение структуры stat находится в системном заголовочном файле
st_dev – описывает логическое устройство, на котором находится файл,
st_ino – задает номер индексного дескриптора,
st_mode – задает режим доступа к файлу,
st_nlink – определяет число ссылок, указывающих на файл,
st_uid, st_gid - соответственно идентификаторы пользователя и группы файла,
st_size – текущий логический размер файла в байтах,
st_atime – время последнего чтения из файла,
st_mtime – время последней модификации,
st_ctime – время последнего изменения информации, возвращаемой в структуре stat,
st_blksize – размер блока ввода/вывода,
st_blocks – число физических блоков, занимаемых файлом.
Для изменения прав доступа к файлу используется вызов:
int chmod(const char *pathname, mode_t mode);
Пример:
if(chmod(“myfile.c”, 0604)==-1) perror(“ошибка вызова chmodn”);
где 0604 – новые права доступа к файлу.
Порядок выполнения работы
1. Изучить теоретическую часть лабораторной работы.
2. Написать программу ввода символов с клавиатуры и записи их в файл (в качестве аргумента при запуске программы вводится имя файла). Для чтения или записи файла использовать функции посимвольного ввода-вывода getc(),putc() или им подобные. Предусмотреть выход после ввода определённого символа (например: ctrl-F). После запуска и отработки программы просмотреть файл. Предусмотреть контроль ошибок открытия/закрытия/чтения файла.
3. Написать программу просмотра текстового файла и вывода его содержимого на экран (в качестве аргумента при запуске программы передаётся имя файла, второй аргумент (N) устанавливает вывод по группам строк (по N –строк) или сплошным текстом (N=0)). Для чтения или записи файла использовать функции посимвольного ввода-вывода getc(),putc() или им подобные. Предусмотреть контроль ошибок открытия/закрытия/чтения/записи файла.
4. Написать программу копирования одного файла в другой. В качестве параметров при вызове программы передаются имена первого и второго файлов. Для чтения или записи файла использовать функции посимвольного ввода-вывода getc(),putc() или им подобные. Предусмотреть копирование прав доступа к файлу и контроль ошибок открытия/закрытия/чтения/записи файла.
5. Написать программу вывода на экран содержимого текущего каталога.
Вывести с использованием данной программы содержимое корневого каталога. Предусмотреть контроль ошибок открытия/закрытия/чтения каталога.
6. Написать программу подсчёта числа отображаемых символов в строках текстового файла и формирование из полученных значений другого текстового файла, в котором будут расположены строки, каждая из которых представляет собой символьное изображение числа символов в данной строке из первого файла. Для чтения или записи файла использовать функции посимвольного ввода-вывода getc(),putc() или им подобные. Имена файлов передаются в программу в качестве аргументов. Пример вывода программы для текстового файла:
QWER
REEEt
WEEEEEEERSIIIONN
Файл, полученный в результате работы программы:
1. 4
2. 5
3. 16
итого: 3 строки 25 символов
7. Написать программу поиска заданного пользователем файла в текущем каталоге. Предусмотреть контроль ошибок.
8. Написать программу сравнения двух заданных пользователем файлов по их содержимому. Предусмотреть контроль ошибок.
9. Написать программу сравнения двух заданных пользователем каталогов.
Предусмотреть контроль ошибок.
Лабораторная работа №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), но выполняет ту же самую программу, что и процесс А, т. е. те же три строки исходного кода, приведённые на рисунке. В соответствии с введенной выше терминологией процесс А является родительским процессом, а процесс В – дочерним. Две стрелки с надписью РС в этой части рисунка
Рис. 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 потомка %dn, pid”);
else
printf (“Ошибка вызова fork, потомок не созданn”);
}
Оператор if, следующий за вызовом fork, имеет три ветви. Первая определяет дочерний процесс, соответствующий нулевому значению переменной pid. Вторая задаёт действия для родительского процесса, соответствуя положительному значению переменной pid. Третья ветвь неявно соответствует отрицательному (а на самом деле равно –1) значению переменной pid, которое возвращается, если вызову fork не удаётся создать дочерний процесс. Это может означать, что вызывающий процесс попытался нарушить ограничения (например – число процессов одновременно выполняющихся и запущенных одним пользователем). В обоих случаях переменная errno содержит код ошибки EAGAIN. Обратите также внимание на то, что поскольку оба процесса, созданных программой, будут выполняться одновременно без синхронизации, то нет гарантии, что вывод родительского и дочернего процессов не будет смешиваться.
Для смены исполняемой программы можно использовать функции семейства exec. Основное отличие между разными функциями в семействе состоит в способе передачи параметров. Как видно из рис. 2.2, все эти функции выполняют один системный вызов execve.
Рис. 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 (“Запуск программы lsn”);
execl (“/bin/ls”, “ls”, “-l”, (char*)0);
/* Если execl возвращает значение, то вызов был неудачным*/
perror(“Вызов execl не смог запустить программу ls”);
exit(1);
}
Работа этой программы показана на рис. 2.3.
Рис. 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. Так как процесс А ожидает завершения процесса В, то положение его программного счетчика РС не изменилось.
A |
Порядок выполнения работы
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
#include
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:%ldn”, string, pos);
}
Результаты, полученные после выполнения данной программы:
До вызова fork : 10
Дочерний процесс до чтения : 10
Дочерний процесс после чтения : 20
Родительский процесс после ожидания : 20
Дочерний процесс до чтения : 10
Системный вызов exit уже известен, но теперь следует дать его правильное описание. Этот вызов используется для завершения процесса, хотя это также происходит, когда управление доходит до конца тела функции main или до оператора return в функции main. Описание exit:
#include
void exit ( int status);
Единственный целочисленный аргумент вызова exit называется статусом завершения (exit status) процесса, младшие 8 бит которого доступны родительскому процессу при условии, если он выполнил системный вызов wait. При этом возвращаемое вызовом exit значение обычно исп