API файловой системы

Файлы существуют в нескольких качествах:

  • объекты файловой системы (ФС)
  • источники кода для программ
  • источники данных для процессов

Файлы в файловой системе

В файловой системе UNIX хранятся различные объекты: файлы, каталоги, символические ссылки, файлы устройств, FIFO, сокеты. Объекты в ФС адресуются именами и характеризуются правами доступа. Один объект может иметь несколько имен. Права доступа (R)ead, (W)rite, e(X)ecute определены по отдельности для трех категорий пользователей (U)ser, (G)roup, (O)ther. Дополнительно к правам доступа есть флаг смены владельца на время выполнения файла, смены группы на время выполнения файла и признак «липкости», что бы он не означал. Права доступа и флаги для разных типов объектов интерпретируются немного по-разному.

Файлы можно создавать (одновременно давая имя), добавлять новые имена, удалять старые имена, а также менять права доступа к файлу (влияет на сам файл, вне зависимости от того к какому из его имен применялась операция). Администратор может еще и поменять владельца файла.

Файл оставшийся без имени и не используемый для вода/вывода или в качестве источника данных программы – уничтожается. Вызовы ядра Unix, связанные с файловой системой можно условно поделить на несколько групп:

  • работа с содержимым файла или каталога
  • работа с именами объектов
  • работа с символическими ссылками
  • работа с FIFO и файлами устройств
  • работа с метаданными

Метаданные:

int chown(const char *path, uid_t owner, gid_t group); //смена владельца и группы
int chmod(const char *path, mode_t mode); //смена прав доступа
int stat(const char *file_name, struct stat *buf); //получение всех атрибутов файла

Имена в каталогах:

int mkdir(const char *pathname, mode_t mode); //создание каталога
int rmdir(const char *pathname); //удаление каталога

int link(const char *oldpath, const char *newpath); //создание нового имени
int unlink(const char *pathname); //удаление старого имени
int rename(const char *oldpath, const char *newpath); //атомарная операция удаляющая имя oldpath и создающая newpath

Создание символических ссылок, FIFO и файлов устройств:

int symlink(const char *oldpath, const char *newpath); //создание файла ссылки newpath ссылающегося на oldpath
int mknod(const char *path, mode_t mode, dev_t dev); //создание FIFO или файла устройства (определяется mode)

Открытые файлы = файловые дескрипторы

В качестве источника данных файлы представлены в программе в виде файловых дескрипторов – целых чисел являющимися индексами в таблице открытых файлов. За файловыми дескрипторами могут скрываться файлы, FIFO и файлы устройств в ФС, неименованые каналы - pipe или сокеты. Для операций чтения и записи все эти источники данных равноценны.

По соглашениям UNIX вновь запущенная программа может рассчитывать на три открытых файла с индексами 0,1,2, соответствующие stdin, stdout, stderr. Ответственность за это возлагается на программу вызвавшую exec().

Работа с метаданными файла может происходить и по имени и по файловому дескриптору.

int fchmod(int fd, mode_t mode);
int fchown(int fd, uid_t owner, gid_t group);
int fstat(int fd, struct stat *buf);

Некоторые примеры использования API файловой системы

//Перенаправление стандартного файла ошибок
int newfd;
char *fname="file";
if( (newd = creat(fname, S_IRUSR|S_IWUSR) >=0 ) ){
   dup2(newfd,2);
   close(newfd);
}else{
   perror("Cannot open new stderr file:");
   exit(1);
}

// Создание большого "дырявого" файла, который имеет длину 10000000 байт а реально занимает 1 блок на диске
newd = open(fname, O_WRONLY|O_TRUNC);
lseek(fd,10000000,SEEK_SET);
write(fd," ",1);

//узнаем текущую позицию чтения/записи
pos=lseek(newfd,0,SEEK_CUR);

//делаем "невидимым" временный  файл 
int tmpfd;
char tmpname[]="/tmp/qqqXXXXXX"
mktemp(tmpname);
int tmpfs = open(tmpfname, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR)
unlink(tmpfname);
//теперь файл не имеет имени и будет уничтожен по завершению нашей программы
//читать и писать в него можно без проблем
write(tmpfd,tmpname,1);
lseek(tmpfd,0,SEEK_SET);
read(tmpfd,tmpname,1);

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 - при открытии терминала не назначать его в качестве управляющего

Наследование файловых дескрипторов

Создание нового процесса в Unix осуществляется системным вызовом fork() который создаёт точную копию текущего процесса. Копируются все структуры данных в ядре и в пространстве пользователя, а значит и таблица открытых файлов. Таким образом дочерний процесс наследует все открытые файлы родительского процесса.

В таблице виртуальных Inode во время вызова fork() счётчики числа открытий файлов увеличиваются на количество ссылающихся на них файловых дескрипторов нового процесса.

Вызов exec("exefile",...) загружает в память существующего процесса код и данные из файла exefile. Все открытые файлы сохраняют своё состояние, кроме тех, которые помечены флагом "O_CLOEXEC". Помеченные файлы закрываются. Исполняемый файл exefile не занимает файловый дескриптор, но так же считается открытым, т.е. exec() увеличивает счётчик числа открытий в таблице виртуальных Inode.

Благодаря цепочке вызовов fork() - exec() и наследованию открытых файлов shell при запуске внешней программы создаёт новый процесс с помощью fork(), а в нём перед вызовом exec() может переназначить файлы стандартного ввод-вывода, которые будут унаследованы запускаемой программой.

Связь со стандартной библиотекой языка Си

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

Если хочется воспользоваться стандартными библиотечными функциями fprintf, fscanf и т.п. то можно создать структуру FILE на основе существующего дескриптора:

FILE *fp=fdopen(int fd, const char *mode); //mode как в fopen вида “rb” или “w” и т.п.

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

Операция блокировки части файла необходима для организации совместного доступа к файлу из нескольких процессов. Главная задача блокировки - обеспечение атомарности чтения данных, т.е. гарантия того, что при чтении (возможно несколькими вызовами read()) все данные будут принадлежать одной "версии" файла и не будут перезаписаны во время чтения. Соответственно бываю эксклюзивные блокировки со стороны писателя (я пишу в этот файл, другим в это время читать и писать бессмысленно) и разделяемые блокировки со стороны читателей (мы читаем, пожалуйста, не пишите).

В Unix традиционно используются рекомендательные (advisory) блокировки. Такие блокировки требуют от всех процессов, которые хотят получить доступ к файлу, вызова специальных функций для установки и/или проверки блокировок. Вызовы open(), read() и write() про блокировки ничего не знают, и, в результате, процессы, которые явно не вызывают функции работы с блокировками не узнают об их существовании и могут нарушить логику совместной работы с файлом.

Отдельной проблемой является то, что разные способы установки блокировок никак не взаимодействуют друг с другом. Так блокировка установленная вызовом fcntl() не может быть проверена вызовом flock(). Единственным реальным сценарием применения файловых блокировок в Unix является разработка комплекса программ с заранее прописанным алгоритмом синхронизации доступа к файлам через блокировки.

Хорошая статья на английском.

Блокировка через вспомогательный файл

Самые ранние версии Unix не поддерживали блокировок, поэтому в различных программах можно встретить создание вспомогательного файла с расширением .lck или .lock. Перед записью в файл somefile.txt программа в цикле пытается создать файл somefile.txt.lck с флагами O_CREAT|O_EXCL. Если файл уже существует, то вызов open() возвращает ошибку и цикл продолжается. Если файл удалось успешно создать, то цикл завершается и можно открывать на запись основной файл. В служебный файл блокировки часто пишут PID процесса, чтобы было понятно, кто его создал.

int fd=-1;
while(fd==-1){
   fd=open("somefile.lck", O_CREAT|O_EXCL|O_WRONLY,0500);
}

//Пишем в файл блокировки свой PID
char pid[6];
itoa(getpid(), pid, 10);
write(fd,pid,strlen(pid));
//этот файловый дескриптор нам больше не нужен
close(fd);

//основная работа
int mainfd=open("somefile", O_WRONLY,0500);
...
close(mainfd);

//Удаляем файл блокировки
unlink("somefile.lck");

Блокировка flock()

Следующим шагом стало появление в BSD системах вызова flock(), который позволял пометить в ядре файл как заблокированный. Этот вызов не стандартизован POSIX, но поддерживается в Linux и во многих версиях Unix. flock() не поддерживается сетевой файловой системой NFS.

#include <sys/file.h>
int flock(int fd, int operation);

Операции:

  • LOCK_EX - если файл не заблокирован, то установить эксклюзивную блокировку. Иначе приостановить выполнение процесса, пока все остальные блокировки не будут сняты. По определению только один процесс может держать эксклюзивную блокировку файла.
  • LOCK_SH - если файл не заблокирован эксклюзивно, то увеличить счётчик разделяемых блокировок. Иначе приостановить процесс до снятия эксклюзивной блокировки. Разделяемую блокировку на заданный файл может держать более чем один процесс.
  • LOCK_UN - удалить блокировку, удерживаемую данным процессом
  • LOCK_NB - (not block) флаг, применяемый вместе с LOCK_EX и LOCK_SH для проверки возможности блокировки. Если блокировка невозможна, то вместо приостановки процесса flock() возвращает -1, а переменная errno устанавливается в значение EWOULDBLOCK.

Блокировки в POSIX fcntl()

Стандарт POSIX определяет операции с блокировками записей - участков файлов. Блокировки производятся универсальный вызов управления открытыми файлами fcntl() (file control) или через функцию locf(). В Linux locf() это библиотечная обёртка для fcntl().

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, struct flock *lock);

fcntl() - это перегруженный (в смысле C++) вызов ядра с переменным числом параметров. Его поведение определяется значением второго параметра - cmd. За блокировки отвечают команды F_SETLK, F_SETLKW и F_GETLK, которые используются для установки/снятия (SET) и чтения (GET) блокировок файла .

Параметры блокировки задаются/считываются через структуру flock. Блокируемая позиция задаётся параметром, указывающим откуда отсчитывать стартовое смещение, стартовым смещением и размером. Нулевой размер означает блокировку участка файла от стартового смещения и до бесконечности.

struct flock {
    ...
    short l_type;    /* Тип блокировки: F_RDLCK,
                        F_WRLCK, F_UNLCK */
    short l_whence;  /* Как интерпретировать l_start:
                        SEEK_SET, SEEK_CUR, SEEK_END */
    off_t l_start;   /* Начальное смещение для блокировки */
    off_t l_len;     /* Количество байт для блокировки */
    pid_t l_pid;     /* PID процесса блокирующего нашу блокировку
                        (F_GETLK only) */
    ...
};

Тип блокировки:

  • F_RDLCK - разделяемая блокировка. Мы собираемся только читать файл и другие читатели могут к нам присоединиться. В это время никто не должен писать в файл.
  • F_WRLCK - эксклюзивная блокировка. Мы собираемся менять файл и никто не должен его читать или в него писать.
  • F_UNLCK - снятие блокировки

Команды:

  • F_SETLK - установить блокировку (если l_type установлен в значение F_RDLCK или F_WRLCK) или снять блокировку (если l_type установлен в значение F_UNLCK). Если другой процесс заблокировал указанный участок или его часть, то fcntl() вернёт -1 и установит значение errno в EACCES или EAGAIN.
  • F_SETLKW - (setlk + wait) тоже, что и предыдущий случай, но с приостановкой текущего процесса до освобождения внешней блокировки
  • F_GETLK - проверка наличия блокировки. На вход подаётся параметры интересующего участка и тип блокировки, на выходе - l_type==F_UNLCK если конфликтов не обнаружено или параметры блокировки, которая пересекается с интересующей.

Блокировки в POSIX lockf()

Функция lockf() в Linux реализуется через fcntl() и представляет из себя упрощённый интерфейс для работы с блокировками.

#include <unistd.h>
int lockf(int fd, int cmd, off_t len);

lockf() работает с участком файла начиная с текущей позиции чтения/записи и длиною len байт если len > 0; c позиции раньше текущей на len и до текущей если len < 0; с текущей позиции до конца файла если len = 0.

Команды:

  • F_LOCK - Устанавливает исключительную блокировку указанной области файла. Если эта область (или её часть) уже блокирована другим процессом, то вызов приостановит выполнение текущего процесса до тех пор, пока не будет снята предыдущая блокировка. Если эта область перекрывается с ранее заблокированной в этом процессе областью, то они объединяются. Файловые блокировки снимаются сразу после того, как установивший их процесс закрывает файловый дескриптор. Дочерние процессы не наследуют подобные блокировки.
  • F_TLOCK - То же самое, что и F_LOCK, но вызов никогда не блокирует выполнение и возвращает ошибку, если файл уже заблокирован.
  • F_ULOCK - Снимает блокировку с заданной области файла. Может привести к тому, что блокируемая область будет поделена на две заблокированные области.
  • F_TEST - Проверяет наличие блокировки: возвращает 0, если указанная область не заблокирована или заблокирована вызвавшим процессом; возвращает -1, меняет значение errno на EAGAIN (в некоторых системах на EACCES), если блокировка установлена другим процессом.

Блокировки в shell

При программировании в shell можно воспользоваться командой

flock [options] <file|directory> <command> [command args]

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

Обязательные (mandatory) блокировки

В Linux и некоторых Unix возможна установка обязательных блокировок, которые приостановят вызовы read() и write() на заблокированных участках файла (документация по Linux). Для этого файловая система должна быть смонтирована с опцией-o mand, а в правах доступа к файлу должны быть одновременно выставлены флаг запрещения прав на выполнение группой и флаг смены группы процесса при выполнении SGID. В буквенной записи это выглядит так: -rw-r-Sr-- (если бы было право на исполнение группе, то была бы показана маленькая 's') .

  • Если на файл установлена разделяемая блокировка F_RDLCK то блокируются все вызовы, которые изменяют файл (write(), open() с флагом O_TRUNC и т.п.)
  • Если на файл установлена разделяемая блокировка F_WRLCK то блокируются вызовы чтения read() и вызовы, изменяющие содержимое файла (write() и т.д.)
  • Обязательная блокировка несовместима с отображением файла в память.

Файловые дескрипторы и их дублирование

файловый дескриптор ФД (созданный через вызов open или унаследованный от родительского процесса), является целочисленным индексом в таблице ссылок на структуры открытых файлов процесса. Сама структура, связанная с открытым файлом, содержит дальнейшую ссылку на виртуальный Inode файла в памяти ядра, а так же флаги доступа к файлу (чтение, запись), текущую позицию чтения/записи в файле и некоторые дополнительные данные. Информация о блокировках используется совместно несколькими процессами, а потому вынесена в виртуальный Inode.

По соглашениям, принятым в ОС Unix, в момент запуска программы должны быть открыты ФД 0, 1 и 2, которые интерпретируются как STDIN, STDOUT и STDERR соответственно. Другие ФД так же могут быть открыты, но это никак не регламентируется. При выделении нового ФД при вызове open, pipe, dup и т.п., выбирается наименьший свободный ФД.

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

Для создания ссылок используются вызовы dup и dup2. dup возвращает первый свободный номер ФД, а dup2 позволяет явно указать номер нового ФД. Если желаемый номер ФД окажется занятым, то он сначала будет закрыт.

int newfd=dup(oldfd)
int newfd=dup2(oldfd,1)

Самое частое использование dup2 это перенаправление стандартного ввода-вывода. Например, shell, выполняя команду:

grep str infile > outfile

может выполнить следующую последовательность действий

Вариант 1:

if (!fork() ){
  // Дочерний процесс для запуска grep

  // Освобождаем ФД==1 (STDOUT)
  close(1);

  // open должен вернуть наименьший свободный ФД т.е. 1
  open("outfile", O_CREAT|OTRUNC|O_WRONLY, 0544);

  // Запускаем программу grep, наследующую STDOUT -> "outfile"
  execlp("grep", "str", "infile",NULL);
}

Вариант 1 в многопоточной среде может привести к гонкам за захват ФД==1, поэтому рекомендуется Варинт2:

if (!fork() ){
  // Дочерний процесс для запуска grep

  // open возвращает какой-то ФД 
  int fd= open("outfile", O_CREAT|OTRUNC|O_WRONLY, 0544);

  // Связываем 1 (STDOUT) с "outfile" через fd
  dup2(fd,1);

  // Освобождаем fd
  close(fd);

  // Запускаем программу grep, наследующую STDOUT -> "outfile"
  execlp("grep", "str", "infile",NULL);
}

Перенаправление через pipe выполняется аналогично.

Доступ к каталогам

Создание каталога

Для создания каталога в вызов mkdir() передаётся путь к создаваемому каталогу. Вышележащий каталог должен существовать (??) и пользователь должен иметь право на запись в него.

#include <sys/stat.h>
int mkdir(const char *path, mode_t mode);

Создание имени файла

При создании файла вызовом open() с флагом O_CREAT в указанном в пути к файлу каталоге создаётся запись с именем файла. Новое имя для файла можно создать вызовом link().

#include <unistd.h>
 int link("oldpath", "newpath");

Поскольку жёсткие ссылки возможны только в рамках одной ФС, то и имена "oldpath" и "newpath" должны находиться внутри одной ФС.

Удаление имени файла

В Unix нет операции удаление файла. Есть лишь операция удаление из каталога жёсткой ссылки (имени) на объект. Каждый раз после удаления имени уменьшается счётчик имён в Inode файла. Когда счётчик имён становится равным нулю, файл становится недоступным по имени. Однако, если в этот момент файл был открыт одним или несколькими процессами, то он не удаляется из ФС. Только тогда, когда у файла ноль имён и он не открыт ни в одном процессе, его Inode и его блоки данных помечаются как свободные, т.е. происходит уничтожение файла.

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

Вызов unlink() неприменим к каталогам. unlink() символической ссылки удаляет имя символической ссылки, никак не влияя на объект, на который указывает символическая ссылка.

#include <unistd.h>
 int unlink("file");

Переименование файла

Вызов rename("old", "new") эквивалентен паре вызовов link("old", "new"); unlink("old");. В отличие от этой пары rename() можно применять к каталогам, а так же он не удалит объект при переименовании его в самого себя - rename("x", "x").

Чтение каталога

Каталог в Linux можно открыть как файл с помощью open с флагом O_DIRECTORY:

struct old_linux_dirent  {
     long d_ino;                 /* inode number */
     off_t d_off;                /* offset to this dirent */
     unsigned short d_reclen;    /* length of this d_name */
     char d_name [NAME_MAX+1];   /* file name (null-terminated) */
} olddirp[SIZE];

// в Linux каталог можно открыть с помощью open
int fd=open("dirname", O_DIRECTORY); 
// и прочитать его внутреннюю структуру
retval=readdir(fd, olddirp, SIZE);

но так делать не надо.

Для работы с каталогами надо использовать библиотечные функции opendir(3), readdir(3) и т.д.

#include <dirent.h>
struct dirent {
       ino_t          d_ino;       /* номер inode */
       off_t          d_off;         /* заглушка */
       unsigned short d_reclen;    /* заглушка */
       unsigned char  d_type;      /* тип файла; поле не стандартизовано */
       char           d_name[256]; /* имя файла */
};

// открыть каталог
DIR *dirp;
dirp=opendir("dirname");
// или
dirp=fopendir(fd);

// прочитать запись за записью
struct dirent *rec;
do{
    rec=readdir(dirp);
}while(rec)

// начать сначала
rewinddir(dirp);

// закрыть каталог
closedir(dirp);

Удаление каталога

Удалять можно только пустые каталоги.

#include <unistd.h>
int rmdir("pathname");

lseek

С каждым открытым файлом в UNIX связано понятие позиции чтения-записи. Это понятие не применимо к сокетам и каналам, но обычном файле у нас есть внутренняя нумерация байт, начинающаяся с нуля, и некая условная головка чтения-записи. Позиция головки одна для чтения и записи. При открытии файла на чтение или на запись головка выставляется на начало файла. При каждой операции чтения-записи головка устанавливается на позиции за последним считанным-записанным байтом. Особый случай - это открытие файла на дозапись с флагом O_APPEND. В этом случае каждая операция записи предварительно перемещает головку в конец файла. Конец файла - это позиция равная числу байт в файле. Если мы начнем запись в конец файла, то будем писать после последнего существующего байта. Если начнем читать, то прочитаем 0 байт, что является признаком конца файла.

Иногда появляется желание переместиться по файлу вперед или назад и начать читать или писать с некой определенной позиции. Для того чтобы управлять положением головки используется вызов lseek, который изменяет положение головки чтения-записи. lseek получает файловый дескриптор и два значения: целочисленное значение смещения и макрос whence, который описывает, откуда это смещение отсчитывается. В man странице написано, что использование слова whence нарушает правила английского языка, но сохраняется по историческим причинам.

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

Параметр whence может принимать три значения: + SEEK_CUR - смещение относительно текущей позиции. Если смещение положительное, то головка смещается к концу файла, если отрицательное - то ближе к началу файла. + SEEK_END - смещение вычисляется относительно текущего размера файла. + SEEK_SET - смещение от начала файла. Отрицательное смещение не имеет смысла.

В качестве результата lseek возвращает текущую позицию относительно начала файла.

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

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

У вызова lseek есть проблема, связанная с разрядностью чисел.

В 32 разрядных системах максимальное значение целочисленного смещения четыре гигабайта, в то время как все современные файловые системы поддерживают файлы большего размера. Проблема в том, что мы просто не можем записать значение смещение больше 4ГБ. Тем не менее выполняя последовательные вызовы lseek мы можем перемещаться неограниченно далеко.

В таком случае lseek возвращает -1 и выставляет переменную errno в значение EOVERFLOW.

Чтобы работать со сверхбольшими файлами в 32 разрядной системе компилятор gcc и библиотека gnulibc предоставляют специальную функцию lseek64, у которой в качестве параметра offset передается структура, состоящая из двух целочисленных значений. Для ее использования необходимо открывать файл с флагом O_LARGEFILE и указывать при компиляции особые макросы -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS.