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 <sys/types.h>
#include <unistd.h>
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 <unistd.h>
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 <unistd.h>
/* Для семейства вызовов 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 <unistd.h>
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 <unistd.h>
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 <unistd.h>
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 <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:%ldn”, 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))
{
exit_status = WEXITSTATUS (status);
printf (“Статус завершения %d равен %dn”, pid, exit_status);
}
exit (0);
}
Значение, возвращаемое родительскому процессу при помощи вызова exit, записывается в старшие 8 бит целочисленной переменной status. Чтобы оно имело смысл, младшие 8 бит должны быть равны нулю. Макрос WIFEXITED (определенный в файле <sys/wait.h>) проверяет, так ли это на самом деле. Если макрос WIFEXITED возвращает 0, то это означает, что выполнение дочернего процесса было остановлено (или прекращено) другим процессом при помощи межпроцессного взаимодействия, называемого сигналом.
Для ожидания завершения определенного дочернего процесса используется системный вызов waitpid. Его описание:
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid (pid_t pid, int *status, int options);
Первый аргумент pid определяет идентификатор дочернего процесса, завершения которого будет ожидать родительский процесс. Если этот аргумент установлен равным -1, а аргумент options установлен равным 0, то вызов waitpid ведет себя в точности так же, как и вызов wait, поскольку значение -1 соответствует любому дочернему процессу. Если значение pid больше нуля, то родительский процесс будет ждать завершения дочернего процесса с идентификатором процесса, равным pid. Во втором аргументе status будет находиться статус дочернего процесса после возврата из вызова waitpid.
Последний аргумент, options, может принимать константные значения, определенные в файле <sys/wait.h>. Наиболее полезное из них – константа WNOHANG. Задание этого значения позволяет вызывать waitpid в цикле без блокирования процесса, контролируя ситуацию, пока дочерний процесс продолжает выполняться. Если установлен флаг WNOHANG, то вызов waitpid будет возвращать 0 в случае, если дочерний процесс еще не завершился.
Следующий пример демонстрирует работу вызова waitpid:
#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 секунды*/
printf (“Потомок %d пауза …n”, getpid ());
sleep (4);
exit (5);
}
/* Если мы оказались здесь, то это родительский процесс*/
/* Проверить, закончился ли дочерний процесс, и если нет, */
/* то сделать секундную паузу и потом проверить снова*/
while (waitpid (pid, &status, WNOHANG) == 0)
{
printf (“Ожидание продолжается …n”);
sleep (1);
}
/* Проверка статуса завершения дочернего процесса*/
if (WIFEXITED (status))
{
exit_status = WEXITSTATUS (status);
printf (“Статус завершения %d равен %dn”, pid, exit_status);
}
exit (0);
}
При запуске программы получим следующий вывод:
Ожидание продолжается…
Потомок 12857 пауза…
Ожидание продолжается…
Ожидание продолжается…
Ожидание продолжается…
Статус завершения 12857 равен 5
До сих пор предполагалось, что вызовы exit и wait используются правильно и родительский процесс ожидает завершения каждого процесса. Вместе с тем иногда могут возникать две другие ситуации. В момент завершения дочернего процесса родительский процесс не выполняет вызов wait. Завершающийся процесс как бы «теряется» и становится зомби-процессом. Зомби-процесс занимает ячейку в таблице, поддерживаемой ядром для управления процессами, но не использует других ресурсов ядра. В конце концов, он будет освобожден, если его родительский процесс вспомнит о нем и вызовет wait. Тогда родительский процесс сможет прочитать статус завершения процесса и ячейка освободится для повторного использования. Второй случай – родительский процесс завершается, в то время как один или несколько дочерних процессов продолжают выполняться. Родительский процесс завершается нормально, дочерние процессы (включая зомби-процессы) принимаются процессом init (процесс, идентификатор которого pid = 1, становится их новым родителем).
Порядок выполнения работы
1. Изучить теоретическую часть лабораторной работы.
2. Организовать функционирование процессов следующей структуры:
2.1. Отец формирует нумерованные сообщения вида: N pid time (N –текущий номер сообщения, pid – pid процесса, time – время записи в формате мм.сс (минуты.секунды)) и через файл передаёт их сыновьям. Одновременно сообщение отображается на экране дисплея. Сыновья читают данные из общего файла и отображают их на экране в своей зоне вывода в виде: N pid time1 time2 (N – номер сообщения, pid – pid процесса сына, time1 – текущее время, time2 – время, считанное из файла). Все процессы начинают свою работу по записи/чтению файла одновременно.
... Министерство образования Российской Федерации Саратовский государственный технический университет Формульный компилятор методические указания к выполнению лабораторной работы по курсу «Теория вычислительных процессов и структур для студентов специальности ПВС Составил доцент кафедры ПВС Сайкин А.И. ...
... рехэширования с помощью произведения; б) – Блок-схема функции поиска идентификатора; в) – Блок-схема функции добавления идентификатора 2 Проектирование лексического анализатора 2.1 Назначение лексического анализатора Лексический анализатор (или сканер) – это часть-компилятора, которая читает литеры программы на исходном языке и строит из них слова (лексемы) исходного языка. На вход ...
... приведенные ниже: Из последнего выражения видно, что: Иногда в практике целесообразно использовать зависимости: где - естественная составляющая коэффициента - длительности переходных процессов соответственно реальная и нормированная. 3. Метод случайного поиска В задачах модального формирования динамических свойств системы управления в экстремальных условиях на первое ...
... , его стали называть арифметико-логическим. Оно стало основным устройством современных компьютеров. Таким образом, два гения XVII века, установили первые вехи в истории развития цифровой вычислительной техники. Заслуги В. Лейбница, однако, не ограничиваются созданием "арифметического прибора". Начиная со студенческих лет и до конца жизни он занимался исследованием свойств двоичной системы ...
0 комментариев