Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

ОСиС_2008

.pdf
Скачиваний:
96
Добавлен:
29.05.2015
Размер:
2.65 Mб
Скачать

9. Лабораторный курс

325

#include

<sys/types.h>

 

#include

<unistd.h>

 

off_t lseek(int fil, off_t offset, int whence);

При успешном завершении вызов lseek возвращает новое значение указателя файла. В случае ошибки: –1. Первый параметр (fil) — номер файла, открытого ранее. Второй параметр (offset) задает требуемое смещение относительно той позиции указателя файла, которая задается третьим параметром (whence), который можно задать в виде одной из следующих трех констант, определенных в файле <unistd.h>:

SEEK_SET — смещение прибавляется к началу файла; SEEK_CUR — смещение прибавляется к текущему значению

указателя;

SEEK_END — смещение прибавляется к концу файла. Несмотря на то что тип смещения и тип значения указателя

файла один и тот же — off_t (он определен в файле <sys/types.h>), область допустимых значений для каждого из них различна. В отличие от указателя файла, принимающего лищь неотрицательные целые значения, смещение может быть и отрицательным.

Вообще говоря, вызов lseek выдаст признак ошибки для существующего файла только в том случае, если мы попытаемся установить указатель файла на позицию, предшествующую началу файла. И наоборот, операция установки указателя на позицию, расположенную далее конца файла, приведет к появлению в файле «дыры», заполненной нулями.

Пример использования вызова lseek:

off_t newpos;

. . . .

newpos = lseek(fil, (off_t)-16, SEEK_END);

В данном примере новое значение указателя файла будет на 16 меньше, чем значение, соответствующее концу файла. Обратите внимание, что для числа (–16), задающего смещение, используется явное задание типа (off_t ).

Интересно отметить, что вызов lseek можно использовать для определения длины файла. Например, пусть переменная filsize содержит длину файла, тогда

off_t filsize; int fil;

. . . .

filsize = lseek(fil, (off_t) 0, SEEK_END);

326

Одиноков В.В., Коцубинский В.П.

Функции стандартной библиотеки. Установку указателя файла выполняет стандартная функция fseek. Ее описание:

#include <stdio.h>

int fseek(FILE *stream, long offset, int whence);

Функция возвращает ненулевое значение только в случае ошибки. Первый параметр (stream) идентифицирует файл. Назначение двух других параметров аналогично вызову lseek.

С о з д а й т е с помощью текстового редактора небольшой файл, а затем с помощью своей программы образуйте в этом файле «дыру», наличие которой проверьте с помощью текстового редактора.

9.5.6. Чтение из файла

После того как указатель файла установлен в требуемое место файла, можно выполнить чтение из этого файла требуемого числа байтов.

Системные вызовы. Чтение из файла выполняет системный вызов read. Его описание:

#include <unistd.h>

ssize_t read(int fil, void *buffer, size_t n);

Данный вызов выполняет чтение из файла, номер которого задается первым параметром (fil), такого числа байтов, которое задается третьим параметром (n). Считанные байты помещаются в буфер, указатель на который задается вторым параметром (buffer). Буфер представляет собой массив, элементы которого имеют произвольный тип (void). (Чаще всего буфер оформляется как массив символьного типа.)

Если выполнение read завершилось успешно, то он возвращает число байтов, считанных из файла. Это число равно n или меньше его. Второе выполняется тогда, когда мы выполняем чтение из конечной части файла, содержащей меньше символов, чем запрашивается. Если после этого опять выполнить read, то он возвратит число 0, означающее, что достигнут конец файла. В случае ошибки read возвращает (–1).

Одним из результатов выполнения read является перемещение указателя файла на первый, не считанный байт файла или на его конец.

Пример

9. Лабораторный курс

327

Следующая программа выполняет подсчет байтов в файле, имя которого передается из командной строки:

#include

<stdlib.h>

/* Для вызова exit */

#include

<fcntl.h>

 

#include

<unistd.h>

 

#define BUFSIZE 512

 

main(int argc, char *argv[ ])

{

int fil;

char buffer[BUFSIZE]; ssize_t nread;

long total = 0; if (argc !=2)

{

printf(“Need 1 argument for open\n”);

exit(1);

/* Выход по ошибке */

}

 

/* Открыть файл для чтения */

 

if ((fil = open(argv[1], O_RDONLY)) = = -1)

{

printf(“Cannot open file %s\n”, argv[1]);

exit(1);

/* Выход по ошибке */

}

 

/* Повторять до конца файла */

while((nread = read(fil, buffer, BUFSIZE)) >0)

total=total+nread;

 

printf(“total = %ld\n”, total);

 

exit(0);

 

}

 

Поясним, почему длина буфера взята равной 512 байтов. Дело в том, что каждый системный вызов read инициирует считывание с диска блока байтов. Величина этого блока есть число 512, умноженное на степень двойки, и зависит от системы. Если считывать данные с диска небольшими порциями (по несколько байтов), то никаких изменений в результатах работы программы не будет. Единственное — резко возрастет время выполнения программы, так как, во-первых, чтение каждой порции байтов требует считывания в лучшем случае из дискового КЭШа, а в худшем — с диска целого блока. А во-вторых, большое количество системных вызовов приводит к такому же числу переключений ЦП из режима

328

Одиноков В.В., Коцубинский В.П.

«задача» в режим «ядро» и обратно, что требует заметных затрат времени ЦП.

Функции стандартной библиотеки. Применение для чте-

ния из файла стандартных библиотечных функций позволяет не заботиться об эффективности информационного обмена, так как, во-первых, эти функции занимаются буферизацией обмена

сфайлом. Поэтому запрос из программы очередной порции байтов не приводит неизбежно к считыванию блока файла. Если эта порция находится в том блоке, который был считан последним

сдиска, то требуемые байты стандартная функция находит в своем буфере, откуда она и копирует их в буфер программы. Во-вторых, функции стандартной библиотеки выполняются в режиме «задача» и поэтому их вызов не приводит к переключению процессора.

Примером стандартной функции чтения файла является функция чтения символа getc. Ее описание:

#include <stdio.h>

int getc(FILE *istream);

Данная функция возвращает или код символа или число (–1), которое означает «конец файла» (EOF). Единственный параметр функции (istream) представляет собой указатель на структуру FILE. Этот параметр должен быть получен в результате открытия файла

впрограмме (до вызова функции getc).

Вы п о л н и т е приведенную выше программу подсчета символов.

9.5.7. Запись в файл

После того как указатель файла установлен в требуемое место файла, можно выполнить запись в файл требуемого числа байтов. Если указатель файла был установлен на конец файла, то запись будет выполняться на свободное место за счет увеличения длины файла. Иначе — запись выполняется «поверх» прежнего содержимого файла.

Системные вызовы. Запись в файл выполняет системный вызов write. Его описание:

#include <unistd.h>

ssize_t write(int fil, void *buffer, size_t n);

Данный вызов выполняет запись в файл, номер которого задается первым параметром (fil), такого числа байтов, которое задается третьим параметром (n). Запись в файл выполняется из буфера,

9. Лабораторный курс

329

указатель на который задается вторым параметром (buffer). Буфер представляет собой массив, элементы которого имеют произвольный тип (void).

Если выполнение write завершилось успешно, то он возвращает число байтов, записанных в файл. Почти всегда это число равно n. В случае ошибки write возвращает (–1).

Одним из результатов выполнения write является перемещение указателя файла на первый байт после записанного участка файла.

В качестве примера приведем фрагмент программы, выполняющей запись содержимого буфера outbuf, имеющего длину OBSIZE, в файл с именем abc:

fil = open(“abc”, O_RDWR | O_APPEND); write(fil, outbuf, OBSIZE);

Функции стандартной библиотеки. Примером стандартной функции записи в файл является функция записи символа putc. Ее описание:

#include <stdio.h>

int putc(int c, FILE *ostream);

При успешном завершении данная функция возвращает код символа (совпадает с параметром c), а в случае ошибки — число (–1). Параметр ostream аналогичен параметру istream для функции getc.

С о з д а й т е программу, выполняющую добавление символов в конец текстового файла.

9.5.8. Закрытие и уничтожение файла

После того как программа завершила работу с файлом, этот файл желательно закрыть, а если файл в будущем не понадобится, то его желательно уничтожить. Каждая из этих операций (особенно уничтожение) освобождает ресурсы, использовавшиеся для работы с файлом. При завершении процесса все файлы, открытые им, закрываются автоматически.

Системные вызовы. Закрытие файла выполняет системный вызов close. Его описание:

#include <unistd.h> int close(int fil);

Вслучае успешного завершения вызов close возвращает 0.

Вслучае ошибки (–1). Единственный параметр вызова — номер закрываемого файла.

330

Одиноков В.В., Коцубинский В.П.

Для уничтожения файла может быть использован вызов unlink. Его описание:

#include <unistd.h>

int unlink(const char *pathname);

В случае успешного завершения вызов возвращает 0, иначе (–1). Единственный параметр (pathname) есть указатель на имя файла.

Функции стандартной библиотеки. Закрытие файла выпол-

няет стандартная функция fclose. Ее описание:

#include <stdio.h>

int fclose(FILE *stream);

Функция возвращает значение 0 при успешном завершении. В случае ошибки (–1).

Что касается уничтожения файла, то для этого вполне достаточно системного вызова unlink — и соответствующая стандартная функция не существует.

9.5.9. Задание

Требуется разработать одну из следующих программ (по указанию преподавателя):

1)копирование файла (имя старого и нового файлов передается в командной строке) при использовании системных вызовов;

2)копирование файла (имена старого и нового файлов передаются в командной строке) при использовании стандартных функций;

3)вывод на экран содержимого текстового файла, имя которого задается в командной строке, при использовании системных вызовов;

4)вывод на экран содержимого текстового файла, имя которого задается в командной строке, при использовании стандартных функций;

5)ввод с клавиатуры содержимого текстового файла, имя которого задается в командной строке, при использовании системных вызовов;

6)ввод с клавиатуры содержимого текстового файла, имя которого задается в командной строке, при использовании стандартных функций.

9. Лабораторный курс

331

9.6. Лабораторная работа № 6. Системные вызовы для управления процессами

Целью выполнения настоящей лабораторной работы является получение навыков использования в программах на языке СИ системных вызовов UNIX, предназначенных для управления процессами fork, exec, wait, а также работа с переменными окружения.

9.6.1. Подготовка к выполнению работы

В начале выполнения данной работы рекомендуется ознакомиться со следующими вопросами из теоретической части пособия:

1)состояния процесса (подразд. 4.1);

2)создание процесса (подразд. 4.2);

3)переменные окружения (п. 1.5.4).

9.6.2. Создание процесса

Любой процесс может порождать дочерние процессы, используя системный вызов fork. Его описание:

#include <sys/types.h> #include <unistd.h> pid_t fork(void);

В случае успешного завершения вызова fork создается новый процесс, который является почти полной копией своего процессаотца. При этом он имеет такую же программу, переменные окружения, а также структуры данных ядра, обслуживающие процесс. Единственное различие между процессами в том, что новый процесс имеет другой pid. (Вспомним, что pid — номер процесса, уникальный для всей системы.) Таким образом, результатом выполнения fork является параллельное (одновременное) существование в данный момент двух процессов, выполняющих одну и ту же программу, причем в одной и той же ее точке. Эта точка — команда, которая следует в программе за командой вызова подпрограммы fork.

Дальнейшее выполнение одной и той же программы процес- сом-отцом и дочерним процессом различно по той причине, что различается код возврата подпрограммы fork в эти два процесса.

332

Одиноков В.В., Коцубинский В.П.

В процесс-отец возвращается код возврата, который совпадает с pid нового процесса, а код возврата в новый процесс равен 0. Поэтому после оператора вызова подпрограммы fork программа должна содержать или оператор if, или оператор switch, выполняющий ветвление программы в зависимости от кода возврата подпрограммы fork.

Пример

Следующая программа при выполнении порождает дочерний процесс. Дальнейшее выполнение обоих процессов различается тем сообщением, которое процесс выводит на экран:

#include <unistd.h> main()

{

pid_t pid; switch(pid = fork()) { case –1:

perror(“Error”);

exit(1);

break; case 0:

/* Выполнение в дочернем процессе */ printf(“Потомок\n”);

exit(0);

break;

default:

/* Выполнение в родительском процессе */ printf(“Родитель\n”);

exit(0);

}

}

Библиотечная процедура perror выполняет вывод той символьной строки, которая задана в качестве ее единственного параметра, в файл стандартного вывода ошибки. После этого она выводит символ «:» и диагностическое сообщение, уточняющее тип произошедшей ошибки. (Для получения диагностического сообщения процедура perror использует переменную errno из окружения процесса, которая содержит код последней ошибки, возникшей в результате системного вызова.)

В ы п о л н и т е приведенную выше программу.

9. Лабораторный курс

333

9.6.3. Ожидание завершения потомка

В приведенном выше примере создание дочернего процесса приводит к параллельному существованию двух процессов, никак не связанных далее между собой. Простейшим средством синхронизации процессов является системный вызов wait. Его описание:

#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status);

Вызов wait временно приостанавливает выполнение процессаотца до тех пор, пока не завершится любой дочерний процесс. (Это завершение является следствием системного вызова exit.) После этого wait возвращает в процесс-отец pid завершившегося процесса. Единственный параметр (status) представляет собой указатель на целое число. Если этот указатель равен NULL, то данный параметр далее не используется. Иначе в результате завершения wait переменная status указывает на код завершения дочернего процесса, переданный при помощи вызова exit.

Пример

Переделаем приведенную в п. 9.6.2 программу так, чтобы про- цесс-отец ожидал завершения дочернего процесса. Заключительный фрагмент программы:

. . . . . .

default:

/* Выполнение в родительском процессе */ wait((int *) 0);

printf(“Завершение потомка\n”); exit(0);

}

}

В ы п о л н и т е новый вариант программы с использованием вызова wait.

Допустим, что процесс породил не один, а несколько дочерних процессов. Так как вызов wait сообщает о завершении лишь одного потомка, то для ожидания завершения всех потомков необходимо в программе, выполняемой процессом-отцом, организовать соответствующий цикл. Если процесс-отец должен ожидать завершения не всех, а лишь конкретного потомка, то следует использовать вместо wait системный вызов waitpid. Его описание:

#include <sys/types.h>

334

Одиноков В.В., Коцубинский В.П.

#include

<sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);

Первый параметр (pid) есть номер дочернего процесса, завершение которого ожидается. Третий параметр (options) может принимать постоянные значения, определенные в файле <sys/wait.h>. Наиболее интересная константа — WNOHANG. Задание ее в качестве третьего параметра позволяет в цикле вызывать waitpid без блокирования процесса-отца. При этом если требуемый дочерний процесс еще не завершился, waitpid возвращает значение 0.

9.6.4. Загрузка новой программы

Любая программа может заменить себя в адресном пространстве процесса на другую программу, используя одну из шести функций семейства exec. Каждая из этих функций преобразуется транслятором в один и тот же системный вызов, различаясь между собой лишь деталями предоставляемого программного интерфейса. Важно подчеркнуть, что любой такой вызов приводит не к появлению нового процесса, а лишь к смене содержимого памяти того процесса, который содержит вызов exec.

В дальнейшем изложении будем использовать функцию execl. Ее описание:

#include <unistd.h>

int execl(const char *path, const char *arg0, …, const char *argn, (char *) 0);

Все параметры вызова execl являются указателями строк. Первый из них (path) задает имя-путь того файла, который содержит запускаемую программу или скрипт. Второй параметр (arg0) задает простое имя загружаемой программы. Этот параметр и оставшееся переменное число параметров (от arg1 до argn) доступны в вызываемой программе аналогично параметрам командной строки при запуске программы из shell. (На самом деле shell сам использует вызовы fork и exec для запуска программ.) Так как список параметров имеет произвольную длину, он должен заканчиваться нулевым указателем для обозначения конца списка.

Пример

Следующая программа заменяет сама себя на известную программу cat (полное имя файла /bin/cat). Допустим, что cat должна