Код, работающий с struct stat и функцией fstat() (строки 31–36 и 50–56), без сомнения, непрозрачен, поскольку мы еще не рассматривали эти функции и не будем рассматривать до следующей главы (Но обратите внимание на использование fileno() в строке 50 для получения нижележащего дескриптора файла, связанного с переменными FILE*.) Идея в основе этого кода заключается в том, чтобы убедиться, что входной и выходной файлы не совпадают. Это предназначено для предотвращения бесконечного роста файла, в случае подобной команды:
$ cat myfile >> myfile /* Добавить копию myfile к себе? */
И конечно же, проверка работает:
$ echo hi > myfile /* Создать файл */
$ v7cat myfile >> myfile /* Попытка добавить файл к себе */
cat: input myfile is output
Если вы попробуете это с ch04-cat, программа продолжит работу, и myfile будет расти до тех пор, пока вы не прервете ее. GNU версия cat осуществляет эту проверку. Обратите внимание, что что-то вроде этого выходит за рамки контроля cat:
$ v7cat < myfile > myfile
cat: input - is output
$ ls -l myfile
-rw-r--r-- 1 arnold devel 0 Mar 24 14:17 myfile
В данном случае это слишком поздно, поскольку оболочка урезала файл myfile (посредством оператора >) еще до того, как cat получила возможность исследовать файл! В разделе 5.4.4.2 «Возвращаясь к V7 cat» мы объясним код с struct stat.
4.5. Произвольный доступ: перемещения внутри файла
До сих пор мы обсуждали последовательный ввод/вывод, при котором данные читаются или записываются с начала файла и продолжаются до его конца. Часто это все, что требуется программе. Однако, возможно осуществление произвольного ввода/вывода; т.е. читать данные из произвольного положения в файле без необходимости предварительного чтения всего, что находится перед этим местом.
Смещение дескриптора файла является положением внутри открытого файла, начиная с которого будет осуществляться следующая операция чтения или записи. Программа устанавливает смещение с помощью системного вызова lseek():
#include <sys/types.h> /* для off_t; POSIX */
#include <unistd.h> /* объявления lseek() и значений whence */
off_t lseek(int fd, off_t offset, int whence);
Тип off_t (тип смещения) является знаковым целым, представляющим позиции байтов (смещений от начала) внутри файла. На 32-разрядных системах тип представлен обычно как long. Однако, многие современные системы допускают очень большие файлы, в этом случае off_t может быть более необычным типом, таким, как C99 int64_t или какой-нибудь другой расширенный тип. lseek() принимает три следующих аргумента.
int fd
Дескриптор открытого файла.
off_t offset
Позиция, в которую нужно переместиться. Интерпретация этого значения зависит от параметра whence. offset может быть положительным или отрицательным; отрицательные значения перемещают к началу файла; положительные значения перемещают к концу файла.
int whence
Описывает положение в файле, относительно которого отсчитывается offset. См. табл. 4.4.
Таблица 4.4. Значения whence для lseek()
| Именованная константа | Значение | Комментарий |
|---|---|---|
SEEK_SET |
0 | offset абсолютно, т.е. относительно начала файла |
SEEK_CUR |
1 | offset относительно текущей позиции в файле |
SEEK_END |
2 | offset относительно конца файла. |
Большое количество старого кода использует числовые значения, приведенные в табл. 4.4. Однако, любой новый код, который вы пишете, должен использовать символические имена, значение которых более ясно.
Смысл значений и их действие на положение в файле показаны на рис. 4.1. При условии, что файл содержит 3000 байтов и что перед каждым вызовом lseek() текущим является смещение 2000 байтов, новое положение после каждого вызова будет следующим.
Рис. 4.1. Смещения для lseek()
Отрицательные смещения относительно начала файла бессмысленны; они вызывают ошибку «недействительный параметр».
Возвращаемое значение является новым положением в файле. Поэтому, чтобы получить ваше текущее местоположение в файле, используйте
off_t curpos;
...
curpos = lseek(fd, (off_t)0, SEEK_CUR);
Буква l в lseek() означает long. lseek() был введен в V7 Unix, когда размеры файлов были увеличены; в V6 был простой системный вызов seek(). В результате большое количество старой документации (и кода) рассматривает параметр offset как имеющий тип long, и вместо приведения к типу off_t довольно часто можно видеть суффикс L в константных значениях смешений:
curpos = lseek(fd, 0L, SEEK_CUR);
На системах с компилятором стандартного С, где lseek() объявлена с прототипом, такой старый код продолжает работать, поскольку компилятор автоматически преобразует 0L из long в off_t, если это различные типы.
Одной интересной и важной особенностью lseek() является то, что она способна устанавливать смещение за концом файла. Любые данные, которые впоследствии записываются в это место, попадают в файл, но с образованием «интервала» или «дыры» между концом предыдущих данных файла и началом новых данных. Данные в промежутке читаются, как если бы они содержали все нули.
Следующая программа демонстрирует создание дыр. Она записывает три экземпляра struct в начало, середину и дальний конец файла. Выбранные смешения (строки 16–18, третий элемент каждой структуры) произвольны, но достаточно большие для демонстрации особенности:
1 /* ch04-holes.c --- Демонстрация lseek() и дыр в файлах. */
2
3 #include <stdio.h> /* для fprintf(), stderr, BUFSIZ */
4 #include <errno.h> /* объявление errno */
5 #include <fcntl.h> /* для flags для open() */
6 #include <string.h> /* объявление strerror() */
7 #include <unistd.h> /* для ssize_t */
8 #include <sys/types.h> /* для off_t, etc. */
9 #include <sys/stat.h> /* для mode_t */