Файловые дескрипторы¶
Что такое файловый дескриптор¶
Файловый дескриптор (file descriptor, FD) — это неотрицательное целое число, которое служит идентификатором потока ввода-вывода в рамках процесса. Дескриптор может быть связан с обычным файлом, каталогом, сокетом, каналом (pipe) или любым другим объектом, поддерживающим интерфейс ввода-вывода.
При старте каждый процесс получает три стандартных дескриптора:
| FD | Имя | Назначение |
|---|---|---|
| 0 | stdin | Стандартный ввод |
| 1 | stdout | Стандартный вывод |
| 2 | stderr | Стандартный вывод ошибок |
Когда процесс вызывает open, ядро создаёт новую запись в таблице открытых файлов процесса и возвращает наименьший
свободный номер дескриптора.
Таблица файловых дескрипторов и открытые файловые описания¶
Ядро поддерживает два уровня абстракции:
-
Таблица дескрипторов процесса — массив в PCB (process control block), индексируемый номером FD. Каждая запись указывает на открытое файловое описание и хранит флаги дескриптора (в частности
FD_CLOEXEC). -
Таблица открытых файловых описаний (open file descriptions) — глобальная структура ядра. Каждая запись хранит: текущую позицию (
offset), флаги статуса (O_RDONLY,O_APPENDи т.д.) и указатель на inode. Несколько дескрипторов могут ссылаться на одно и то же описание — это происходит приdup,dup2и при наследовании дескрипторов послеfork.
Два дескриптора, ссылающиеся на одно описание, разделяют позицию в файле. Два дескриптора, открытых через open на один
и тот же файл — независимые позиции.
Процесс A open file descriptions inodes (на диске)
┌──────────────────┐
│ fd[] (per-process│
│ descriptor table│
├────┬─────────────┤ ┌─────────────────────────┐
│ 0 │ flags │ │ offset: 0 │
│ │ (FD_CLOEXEC)│──────▶ │ flags: O_RDONLY │──────▶ ┌───────────┐
├────┼─────────────┤ │ refcnt: 1 │ │ inode 42 │
│ 1 │ flags │ └─────────────────────────┘ │ /etc/hosts│
│ │ │──────▶ ┌─────────────────────────┐ └───────────┘
├────┼─────────────┤ │ offset: 512 │ ▲
│ 2 │ flags │ │ flags: O_WRONLY|O_APPEND│──────────────┘
│ │ │──────▶ │ refcnt: 1 │ (два open() на
├────┼─────────────┤ └─────────────────────────┘ один файл —
│ 3 │ flags │──────┐ разные описания,
│ │ │ │ ┌─────────────────────────┐ общий inode)
├────┼─────────────┤ │ │ offset: 0 │
│ 4 │ flags │──────┘ │ flags: O_RDWR │──────▶ ┌──────────┐
│ │ │ │ refcnt: 2 │ │ inode 77 │
└────┴─────────────┘ └─────────────────────────┘ │ data.bin │
▲ └──────────┘
dup(3) → fd 4 тоже указывает
на это же описание;
offset и flags — общие
Сценарий dup: dup(3) (или dup2(3, 4)) создаёт fd 4, указывающий на то же open file description, что и fd 3.
Счётчик ссылок (refcnt) растёт, оба FD разделяют offset и флаги статуса.
Сценарий двойного open: два последовательных вызова open("data.bin", ...) создают два независимых open file
description — у каждого собственный offset, — но оба указывают на один и тот же inode на диске.
Системные вызовы open и close¶
Для открытия файлов используется системный вызов open:
Параметры:
pathname— путь к файлу;flags— набор флагов, объединённых через|:O_RDONLY,O_WRONLY,O_RDWR— режим: только чтение / только запись / чтение–запись;O_CREAT— создать файл, если его нет (требует параметраmode);O_TRUNC— обрезать существующий файл до нулевой длины;O_APPEND— все записи идут только в конец файла;O_NONBLOCK— неблокирующий режим;O_CLOEXEC— закрыть дескриптор автоматически приexec.
mode— права доступа (например0644); применяется только при создании файла черезO_CREAT.
open возвращает новый файловый дескриптор или -1 при ошибке, устанавливая errno.
Для закрытия дескриптора используется close:
close освобождает дескриптор и уменьшает счётчик ссылок на открытое файловое описание (open file description). Когда
счётчик достигает нуля, ядро может освободить связанные ресурсы. Возвращает 0 при успехе или -1 при ошибке.
Позиционирование в файле: lseek¶
Каждое открытое файловое описание имеет текущую позицию (file offset). Перемещать её позволяет lseek:
Параметр whence задаёт точку отсчёта:
whence |
Описание |
|---|---|
SEEK_SET |
От начала файла |
SEEK_CUR |
От текущей позиции |
SEEK_END |
От конца файла |
lseek возвращает новую позицию в байтах от начала файла, либо -1 при ошибке.
Чтение и запись¶
Для чтения и записи используются системные вызовы read и write:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
Оба возвращают количество фактически прочитанных/записанных байт. Это число может быть меньше запрошенного (partial
read/write), поэтому в надёжном коде write оборачивают в цикл.
Реализация cp через системные вызовы¶
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv) {
if (argc != 3) {
fprintf(stderr, "Usage: %s src dst\n", argv[0]);
return 1;
}
int in = open(argv[1], O_RDONLY);
if (in == -1) {
perror("open src");
return 1;
}
int out = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (out == -1) {
perror("open dst");
close(in);
return 1;
}
char buf[4096];
while (1) {
ssize_t n = read(in, buf, sizeof(buf));
if (n == 0) // EOF
break;
if (n == -1) {
perror("read");
return 1;
}
ssize_t written = 0;
while (written < n) {
ssize_t m = write(out, buf + written, n - written);
if (m == -1) {
perror("write");
return 1;
}
written += m;
}
}
close(in);
close(out);
return 0;
}
Внутренний цикл по write необходим, потому что один вызов может записать меньше байт, чем запрошено, — особенно при
работе с сокетами или каналами.
Разреженные файлы¶
Если переместить позицию за конец файла через lseek и затем что-нибудь записать, файл станет разреженным (sparse
file):
int fd = open("sparse.bin", O_WRONLY | O_CREAT | O_TRUNC, 0644);
lseek(fd, 1000000, SEEK_SET);
write(fd, "X", 1);
close(fd);
После этого ls -lh покажет логический размер около 1 МБ, а du -h — только реально занятое место (несколько
килобайт). Промежуток (hole) существует как отсутствие блоков в структурах inode файловой системы.
Дублирование дескрипторов¶
Дескриптор можно продублировать системным вызовом dup2, создав новый дескриптор, ссылающийся на то же открытое
файловое описание. Этот механизм используется при перенаправлении ввода-вывода — подробнее в
статье Перенаправление ввода/вывода.
Просмотр дескрипторов процесса¶
Все открытые дескрипторы текущего процесса можно посмотреть через /proc/self/fd — подробнее в
статье Открытые файлы и процессы.
Связанные темы¶
- Перенаправление ввода/вывода —
dup,dup2и shell-перенаправления - Операции с директориями —
stat,fstatдля получения метаданных по дескриптору - Открытые файлы и процессы —
/proc/<pid>/fd,lsof - Основы файловых систем — inode и структуры файловой системы
Источники¶
man 2 open— описание системного вызоваopenи всех флаговman 2 close— освобождение файлового дескриптораman 2 lseek— позиционирование в файлеman 2 read— чтение из дескриптораman 2 write— запись в дескрипторman 7 path_resolution— как ядро разбирает пути к файлам