Перейти к содержанию

Файловые дескрипторы

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

Файловый дескриптор (file descriptor, FD) — это неотрицательное целое число, которое служит идентификатором потока ввода-вывода в рамках процесса. Дескриптор может быть связан с обычным файлом, каталогом, сокетом, каналом (pipe) или любым другим объектом, поддерживающим интерфейс ввода-вывода.

При старте каждый процесс получает три стандартных дескриптора:

FD Имя Назначение
0 stdin Стандартный ввод
1 stdout Стандартный вывод
2 stderr Стандартный вывод ошибок

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

Таблица файловых дескрипторов и открытые файловые описания

Ядро поддерживает два уровня абстракции:

  1. Таблица дескрипторов процесса — массив в PCB (process control block), индексируемый номером FD. Каждая запись указывает на открытое файловое описание и хранит флаги дескриптора (в частности FD_CLOEXEC).

  2. Таблица открытых файловых описаний (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:

#include <fcntl.h>

int open(const char *pathname, int flags, mode_t mode);

Параметры:

  • 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:

#include <unistd.h>

int close(int fd);

close освобождает дескриптор и уменьшает счётчик ссылок на открытое файловое описание (open file description). Когда счётчик достигает нуля, ядро может освободить связанные ресурсы. Возвращает 0 при успехе или -1 при ошибке.

Позиционирование в файле: lseek

Каждое открытое файловое описание имеет текущую позицию (file offset). Перемещать её позволяет lseek:

#include <unistd.h>

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

Параметр 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 файловой системы.

ls -lh sparse.bin   # логический размер
du -h sparse.bin    # реально занятое место на диске

Дублирование дескрипторов

Дескриптор можно продублировать системным вызовом dup2, создав новый дескриптор, ссылающийся на то же открытое файловое описание. Этот механизм используется при перенаправлении ввода-вывода — подробнее в статье Перенаправление ввода/вывода.

Просмотр дескрипторов процесса

Все открытые дескрипторы текущего процесса можно посмотреть через /proc/self/fd — подробнее в статье Открытые файлы и процессы.

Связанные темы

Источники

  • man 2 open — описание системного вызова open и всех флагов
  • man 2 close — освобождение файлового дескриптора
  • man 2 lseek — позиционирование в файле
  • man 2 read — чтение из дескриптора
  • man 2 write — запись в дескриптор
  • man 7 path_resolution — как ядро разбирает пути к файлам