4.5. Произвольный доступ: перемещения внутри файла

We use cookies. Read the Privacy and Cookie Policy

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 */

10

11 struct person {

12  char name[10]; /* имя */

13  char id[10]; /* идентификатор */

14  off_t pos; /* положение в файле для демонстрации */

15 } people[] = {

16  { "arnold", "123456789", 0 },

17  { "miriam", "987654321", 10240 },

18  { "joe", "192837465", 81920 },

19 };

20

21 int

22 main(int argc, char **argv)

23 {

24  int fd;

25  int i, j;

26

27  if (argc < 2) {

28   fprintf(stderr, "usage: %s file ", argv[0]);

29   return 1;

30  }

31

32  fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0666);

33  if (fd < 0) {

34   fprintf(stderr, "%s: %s: cannot open for read/write: %s ",

35    argv[0], argv[1], strerror(errno));

36   return 1;

37  }

38

39  j = sizeof(people) / sizeof(people[0]); /* число элементов */

Строки 27–30 гарантируют, что программа была вызвана правильно. Строки 32–37 открывают именованный файл и проверяют успешность открытия.

Вычисление числа элементов j массива в строке 39 использует отличный переносимый трюк число элементов является размером всего массива, поделенного на размер первого элемента. Красота этого способа в том, что он всегда верен: неважно, сколько элементов вы добавляете в массив или удаляете из него, компилятор это выяснит. Он не требует также завершающей сигнальной метки; т.е. элемента, в котором все поля содержат нули, NULL или т.п.

Работа осуществляется в цикле (строки 41–55), который отыскивает смещение байтов, приведенное в каждой структуре (строка 42), а затем записывает всю структуру (строка 49):

41  for (i = 0; i < j; i++) {

42   if (lseek(fd, people[i].pos, SEEK_SET) < 0) {

43    fprintf(stderr, "%s: %s: seek error: %s ",

44     argv[0], argv[1], strerror(errno));

45    (void)close(fd);

46    return 1;

47   }

48

49   if (write(fd, &people[i], sizeof(people[i])) != sizeof(people[i])) {

50    fprintf(stderr, "%s: %s: write error: %s ",

51     argv[0], argv[1], strerror(errno));

52    (void)close(fd);

53    return 1;

54   }

55  }

56

57  /* здесь все нормально */

58  (void)close(fd);

59  return 0;

60 }

Вот результаты запуска программы:

$ ch04-holes peoplelist /* Запустить программу */

$ ls -ls peoplelist /* Показать использованные размеры и блоки */

16 -rw-r--r-- 1 arnold devel 81944 Mar 23 17:43 peoplelist

$ echo 81944 / 4096 | bc -l /* Показать блоки, если нет дыр */

20.00585937500000000000

Случайно мы знаем, что каждый дисковый блок файла использует 4096 байтов. (Откуда мы это знаем, обсуждается в разделе 5 4.2 «Получение информации о файле». Пока примите это как данное.) Финальная команда bc указывает, что файлу размером 81944 байтов нужен 21 дисковый блок. Однако, опция -s команды ls, которая сообщает нам, сколько блоков использует файл на самом деле, показывает, что файл использует лишь 16 блоков![48] Отсутствующие блоки в файле являются дырами. Это показано на рис. 4.2.

Рис. 4.2. Дыры в файле

ЗАМЕЧАНИЕ. ch04-holes.c не осуществляет непосредственный двоичный ввод/вывод. Это хорошо демонстрирует красоту ввода/вывода с произвольным доступом: вы можете рассматривать дисковый файл, как если бы он был очень большим массивом двоичных структур данных.

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