11.2.4. Чтение, запись и перемещение

11.2.4. Чтение, запись и перемещение

Хотя есть несколько способов читать и писать файлы, мы обсудим здесь только простейшие из них[42]. Чтение и запись почти идентичны, поэтому рассмотрим их одновременно.

#include <unistd.h>

size_t read(int fd, void * buf, size_t length);

size_t read(int fd, const void * buf, size_t length);

Обе функции принимают файловый дескриптор fd, указатель на буфер buf и длину буфера length, read() читает из файлового дескриптора и помещает полученные данные в буфер; write() пишет length байт из буфера в файл. Обе функции возвращают количество переданных байт, или -1 в случае ошибки (это означает, что ничего не было прочитано или записано).

Теперь, когда мы описали эти системные вызовы, рассмотрим простой пример, создающий файл hw в текущем каталоге и записывающий в него строку "Добро пожаловать!".

 1: /* hwwrite.с */

 2:

 3: #include <errno.h>

 4: #include <fcntl.h>

 5: #include <stdio.h>

 6: #include <stdlib.h>

 7: #include <unistd.h>

 8:

 9: int main(void) {

10:  int fd;

11:

12:  /* открыть файл, создавая его, если он не существовал, и удаляя

13:     его содержимое в противном случае */

14:  if ((fd = open("hw", O_TRUNC | O_CREAT | O_WRONLY, 0644)) < 0) {

15:   perror("open");

16:   exit(1);

17:  }

18:

19:  /* магическое число 18 - это кол-во символов, которые

20:     будут записаны */

21:  if (write(fd, "Добро пожаловать! ", 18) != 18) {

22:   perror("write");

23:   exit(1);

24:  }

25:

26:  close(fd);

27:

28:  return 0;

29: }

Ниже показано, что получится, если запустить hwwrite.

$ cat hw

cat: hw: No such file or directory

cat: hw: Файл или каталог не существует

$ ./hwwrite

$ cat hw

Добро пожаловать!

$

Для изменения этой программы, чтобы она читала файл, нужно просто изменить open(), как показано ниже, и заменить write() статической строки на read() в буфер.

open("hw", O_RDONLY);

Файлы Unix можно разделить на две категории: просматриваемые (seekable) и непросматриваемые (nonseekable)[43]. Непросматриваемые файлы представляют собой каналы, работающие в режиме "первый вошел — первый вышел" (FIFO), которые не поддерживают произвольное чтение или запись, их данные не могут быть перечитаны или перезаписаны. Просматриваемые файлы позволяют читать и писать в произвольное место файла. Каналы и сокеты являются не просматриваемыми файлами; блоковые устройства и обычные файлы являются просматриваемыми.

Поскольку FIFO — это непросматриваемые файлы, то, очевидно, что read() читает их с начала, a write() пишет в конец. С другой стороны, просматриваемые файлы не имеют очевидной точки для этих операций. Вместо этого здесь вводится понятие "текущего" положения, которое перемещается вперед после операции. Когда просматриваемый файл изначально открывается, текущее положение устанавливается в его начало, со смещением 0. Если затем из него читается 10 байт, то текущее положение перемещается в точку со смещением 10 от начала, а запись 5 байт переписывает данные, начиная с одиннадцатого байта в файле (то есть, со смещения 10, где расположена текущая позиция после чтения). После такой записи текущая позиция находится в позиции, смещенной на 15 относительно начала файла — сразу после перезаписанных данных.

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

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

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

#include <unistd.h>

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

Текущая позиция для файла fd перемещается на offset байт относительно whence, где whence принимает одно из следующих значений:

SEEK_SET[45] Начало файла.

SEEK_CUR Текущая позиция в файле.

SEEK_END Конец файла.

Для SEEK_CUR и SEEK_END значение offset может быть отрицательным. В этом случае текущая позиция перемещается в сторону начала файла (от whence), а не в сторону конца. Например, приведенный ниже код перемещает текущую позицию на 5 байт назад от конца файла.

lseek(fd, -5, SEEK_END);

Системный вызов lseek() возвращает новую текущую позицию в файле относительно его начала, либо -1 в случае ошибки. То есть lseek(fd, 0, SEEK_END) — это просто способ определения размера файла, но следует убедиться, что вы сбросили текущую позицию, прежде чем читать из fd.

Хотя текущая позиция не разделяется другими процессами, которые одновременно имеют доступ к файлу[46], это не значит, что множество процессов могут безопасно осуществлять совместную запись в файл. Пусть имеется следующая последовательность.

Процесс A                Процесс B

lseek(fd, 0, SEEK_END);

                        lseek(fd, 0, SEEK_END);

                        write (fd, buf, 10);

write(fd, buf, 5);

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

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

Части файлов, которые "создаются" подобным образом, называют "дырками" (holes). Чтение из такой "дырки" в файле возвращает буфер, полный двоичных нулей, а запись в них может завершиться ошибкой по причине отсутствия свободного пространства на диске. Все это значит, что вызов lseek() не должен применяться для резервирования дискового пространства для позднейшего использования, поскольку такое пространство может быть и не выделено. Если ваше приложение нуждается в выделении некоторого дискового пространства для последующего использования, вы должны применять write(). Файлы с "дырками" часто используют для хранения редко расположенных в них данных, таких как файлы, представляющие хеш-таблицы.

Для простой демонстрации "дырок" в файлах, основанной на командной оболочке, рассмотрим следующий пример (/dev/zero — это символьное устройство, которое возвращает столько двоичных нулей, сколько процесс пытается прочитать из него).

$ dd if=/dev/zero of=foo bs=1k count=10

10+0 records in

10+0 records out

$ ls -l foo

-rw-rw-r-- 1 ewt ewt 10240 Feb 6 21:50 foo

$ du foo

10 foo

$ dd if=/dev/zero of=bar bs=1k count=1 seek=9

1+0 records in

1+0 records out

$ ls -l bar

-rw-rw-r-- 1 ewt ewt 10240 Feb 6 21:50 foo

$ du bar

1 bar

$

Хотя оба файла — и foo, и bar — имеют длину в 10 Кбайт, файл bar занимает только 1 Кбайт дискового пространства, потому что остальные 9 Кбайт были пропущены seek(), когда файл был создан или записан.