open, read, write, close

Открытие файла

Работа с содержимым файла происходит через целочисленный файловый дескриптор, который представляет из себя номер строки в таблице ссылок на открытые файлы процесса.

При открытии файла в вызове ядра open() проверяются соответствие флагов и прав доступа к файлу.

//почти псевдокод
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int fd;
int flags;
mode_t mode;
// открытие (+создание) файла
fd=open("pathname", flags, mode);
//или
fd=creat("pathname",  mode);
// при открытии существующего файла можно опустить параметр mode
fd=open("pathname", flags);

mode - права доступа к файлу, назначаемые в момент его создания. Чтобы нельзя было случайно создать файл со слишком свободным доступом, при создании файла производится побитовое умножение mode на битовую маску umask (mode & ~umask). mode и _umask__ удобно задавать в восьмеричном виде считая, что классические права доступа rwx соответствуют одной восьмеричной цифре. Например, права доступа rwxr-xr-- запишутся в восьмеричном виде как 0754. Типичная маска выглядит так ---w--w- или 022в восьмеричной записи. Такая маска отбирает права на запись у группы и остальных.

Системный вызов umask(mask) устанавливает новую маску и возвращает старую.

#include <sys/types.h>
#include <sys/stat.h>

mode_t old_mask=umask(new_mask);

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

S_IRWXU 00700 User Read, Write,eXecute; S_IRUSR 00400 User Read; S_IWUSR 00200 User Write и т.д. S_IXUSR, S_IRWXG, S_IRGRP, S_IWGRP, S_IXGRP, S_IRWXO, S_IROTH, S_IWOTH, S_IXOTH.

flags - флаги уточняющие режим открытия файла. Флаги делятся на несколько групп:

  • режим доступа: O_RDONLY, O_WRONLY, O_RDWR, O_WRONLY+O_APPEND(чтение,запись, чтение+запись, запись всегда в конец файла);
  • режим создания - O_CREAT, O_CREAT+O_EXCL, O_TRUNC (создавать файл, создавать только если не существует, обрезать существующий до нулевой длины);
  • прочие O_NOFOLLOW, O_CLOEXEC, O_NOCTTY (не открывать символические ссылки, закрывать при вызове exec, не рассматривать открытый файл как управляющий терминал CTTY=Control TeleTYpe).

При ошибке открытия файла возвращается -1 и в переменную errno заносится код ошибки. Возможные значения ошибки (не все):

  • EACCES - нет прав на сам файл или на поиск в каталоге в котором он находится;
  • ENOENT - файл не существует и не указан флаг O_CREAT;
  • EEXIST - файл существует и указаны флаги O_CREAT+O_EXCL;
  • EFAULT - плохой указатель на имя файла (например NULL);
  • EISDIR - попытка открыть каталог;
  • ELOOP - символические ссылки создали кольцо в структуре каталогов.

Чтение/запись файла

#include <unistd.h>
// чтение/запись определенного числа байт
int fd
char buf[SIZE];
size_t count=SIZE;
ssize_t res;
res=read  (fd, buf, count);
res=write(fd, buf, count);

Чтение и запись возвращают количество прочитанных/записанных байтов или -1. -1 не всегда означает ошибку.

Возможные варианты ответа при записи:

  • число от 0 до count - число реально записанных байтов;
  • -1 - ошибка. Если errno при ошибке выставлено в EAGAIN, EWOULDBLOCK или EINTR , то операцию можно повторить см. ниже.

Возможные варианты ответа при чтении:

  • число от 1 до count - число реально записанных байтов;
  • 0 - признак конца файла
  • -1 - ошибка. Если errno при ошибке выставлено в EAGAIN, EWOULDBLOCK или EINTR , то операцию можно повторить см. ниже.

Ошибки чтения/записи:

  • EAGAIN или EWOULDBLOCK (только для сокетов) - не удалось провести неблокирующее чтение/запись для файла (сокета), открытого в режиме O_NONBLOCK;
  • EINTR - операция чтения/записи была прервана доставкой сигнала до того, как удалось прочитать/записать хотя бы один байт;
  • EBADF - плохой дескриптор файла (файл закрыт);
  • EINVAL - неверный режим доступа к файлу (чтение вместо записи или наоборот);
  • EFAULT - неверный указатель на буфер (например NULL).

Чтение/запись сложных объектов за один системный вызов

Чтение/запись из/в фрагментированной памяти

struct iovec {
     void  *iov_base;    /* Starting address */
     size_t iov_len;     /* Number of bytes to transfer */
} iov[SIZE];

res=readv (fd, iov, SIZE);
res=writev(fd, iov, SIZE);

Чтение/запись в определенную позицию. offset - смещение в байтах относительно начала файла.

res=pread (fd, buf, count, offset);
res=pwrite(fd, buf, count, offset);
res=preadv (fd, iov, int iovcnt, offset);
res=pwritev(fd, iov, int iovcnt, offset);

Закрытие файла

// закрытие файла
int retval=close(fd);

Ошибки при закрытии файла встречаются редко, но встречаются.

  • EBADF - попытка закрыть файловый дескриптор, не связанный с открытым файлом;
  • EINTR - операция прервана доставкой сигнала. Может встретиться на медленных (например сетевых) ФС;
  • EIO - ошибка нижележащей системы ввода/вывода. Например обрыв соединения с файловым сервером.

Установка смещения в файле

Для установки позиции/чтения записи в файле используются два параметра offset - смещение в байтах и whence - место от которого отсчитывается смещение. Возможные значения whence:

  • SEEK_SET - от начала файла;
  • SEEK_CUR - от текущей позиции;
  • SEEK_END - от конца файла.

Пример:

#include <sys/types.h>
#include <unistd.h>

// установка позиции чтения/записи
off_t offset=100;
int whence=SEEK_END;
off_t pos=lseek(fd, offset, whence);

Возвращается установленная позиция или -1 в случае ошибки.

Сочетание offset=0 и whence=SEEK_CUR позволяет узнать текущую позицию чтения/записи.

С помощью lseek возможно перемещение указателя записи за конец файла. Многие ФС в такой ситуации не выделяют блоки хранения под пропущенные байты и создают "дырявые" файлы, занимающие на диске пространство меньше своей длины.

int fd=open("/tmp/sparse-file", O_WRONLY|O_CREAT|O_TRUNC, 0700);
off_t pos=lseek(fd, 1000000000, SEEK_SET);
int res=write(fd,"c",1);

В данном примере создаётся файл длиной примерно 1 ГБ, занимающий на диске один блок данных (например 512 Б).

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

Манипулирование файловыми дескрипторами

Возможно создание ссылки на файловый дескриптор.

#include <unistd.h>
int fd1=dup(oldfd);
int fd2=dup2(oldfd, newfd);

dup() - выбирает в таблице открытых файлов первую свободную строку и записывает ссылку на oldfd в неё, dup2() - закрывает файл, связанный с дескриптором newfd (если он был открыт) и записывает ссылку oldfd в newfd. В случае успеха возвращается файловый дескриптор, в случае ошибки -1.

В связи с тем, что в таблицу открытых файлов вписывается именно ссылка, у файловых дескрипторов oldfd и newfd всегда будет одна и та же позиция головки чтения/записи.

Типичное применение dup2() - это подмена стандартных дескрипторов 0,1,2 (stdin,stdout,stderr). oldfd в этом случае закрывается после создания ссылки. dup2() предпочтительнее чем dup(), т.к. выполняется атомарно, что может быть важно в многопоточной среде.

int newfd=open("file",O_RDONLY);
dup2(newfd,0);
close(newfd);

вариант с dup()

int newfd=open("file",O_RDONLY);
close(0);
dup(newfd);
close(newfd);

Флаги вызова open

Флаги, влияющие на создание файла

Флаги файл существует файл не существует
Без флагов Нет ошибки ENOENT
O_CREAT Нет ошибки Нет ошибки
O_CREAT+O_EXCL EEXIST Нет ошибки

Флаги режима доступа

  • O_RDONLY - чтение +O_WRONLY - запись +O_RDWR - чтение и запись +O_WRONLY+O_APPEND - запись всегда в конец файла

Флаги, влияющие на позицию записи

  • O_TRUNC - обнулить размер файла и писать с начала
  • O_APPEND - всегда записывать в хвост файла

Флаги оптимизации доступа

  • O_SYNC - блокировка операции записи до завершения записи на диск
  • O_NONBLOCK - открыть файл в неблокирующем режиме (используется с FIFO)
  • O_NOATIME - не обновлять время последнего доступа

Специфические флаги

  • O_NOCTTY - при открытии терминала не назначать его в качестве управляющего