Определение структуры stat находится в системном заголовочном файле <sys/stat.h> и включает следующие элементы:
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(“ошибка вызова chmod\n”);
где 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 <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 потомка %d\n, 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 (“Запуск программы ls\n”);
execl (“/bin/ls”, “ls”, “-l”, (char*)0);
/* Если execl возвращает значение, то вызов был неудачным*/
perror(“Вызов execl не смог запустить программу ls”);
exit(1);
}
Работа этой программы показана на рис. 2.3.
Рис. 2.3. Вызов exec
Другие формы вызова exec упрощают задание списков параметров запуска загружаемой программы. Вызов execv принимает два аргумента: первый является строкой, которая содержит полное имя и путь к запускаемой программе. Второй аргумент является массивом строк. Первый элемент этого массива указывает на имя запускаемой программы (исключая префикс пути). Оставшиеся элементы указывают на все остальные аргументы программы. Следующий пример использует вызов execv для запуска той же программы ls, что и в предыдущем примере: