OPEN_BY_HANDLE_AT(2) Руководство программиста Linux OPEN_BY_HANDLE_AT(2)

ИМЯ

name_to_handle_at, open_by_handle_at - получение описателя для пути и открытие файла через описатель

ОБЗОР

#define _GNU_SOURCE         /* Смотрите feature_test_macros(7) */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int name_to_handle_at(int dirfd, const char *pathname,
                      struct file_handle *handle,
                      int *mount_id, int flags);
int open_by_handle_at(int mount_fd, struct file_handle *handle,
                      int flags);

ОПИСАНИЕ

Функциональное назначение openat(2) было разделено на две части и добавлено в системные вызовы name_to_handle_at() и open_by_handle_at(): name_to_handle_at() возвращает описатель с произвольной формой(opaque), который соответствует указанному файлу; open_by_handle_at() открывает файл по описателю, который был возвращён предыдущим вызовом name_to_handle_at() и возвращает дескриптор открытого файла.

Системный вызов name_to_handle_at() возвращает файловый описатель и идентификатор монтирования для файла, указанного в аргументах dirfd и pathname. Файловый описатель возвращается через аргумент handle, который является указателем на следующую структуру:

struct file_handle {

    unsigned int  handle_bytes;   /* размер f_handle [in, out] */

    int           handle_type;    /* тип описателя [out] */

    unsigned char f_handle[0];    /* идентификатор файла (размер

                                     задаёт вызывающий) [out] */
};

Вызывающий должен выделить память достаточного размера под структуру описателя, возвращаемого в f_handle. Перед вызовом поле handle_bytes должно содержать размер выделенной памяти для f_handle (константа MAX_HANDLE_SZ, определённая в <fcntl.h>, равна максимально ожидаемому размеру описателя файла. Это не гарантированное верхнее ограничение, так как файловые системы в будущем могут потребовать больше места). При успешном выполнении поле handle_bytes обновляется и содержит количество байт действительно занятых под f_handle.

Вызывающий может определить требуемый размер структуры file_handle указав при вызове значение handle->handle_bytes равное нулю; в этому случае вызов завершается с ошибкой EOVERFLOW, а в поле handle->handle_bytes записывается требуемый размер; затем вызывающий может использовать эту информацию для выделения памяти под структуру правильного размера (смотрите ПРИМЕР ниже). Здесь нужно учесть, что EOVERFLOW может указывать на то, что файловый описатель недоступен для этого заданного имени в файловой системе, которая, обычно, поддерживает поиск файловых описателей. На данный случай указывает то, что ошибка EOVERFLOW возвращается без увеличившегося значения handle_bytes.

Все остальные поля, кроме handle_bytes структуры file_handle, вызывающий должен считать неизвестными: поля handle_type и f_handle требуются только в последующих вызовах open_by_handle_at().

Аргумент flags представляет собой битовую маску (OR) из комбинации нуля или более флагов AT_EMPTY_PATH и AT_SYMLINK_FOLLOW, описанных ниже.

Аргументы pathname и dirfd вместе задают файл, для которого будет получен описатель. Есть четыре различных варианта:

  • Если значение pathname — непустая строка, содержащая абсолютный путь, то описатель возвращается для файла, на который указывает путь. В этом случае dirfd игнорируется.
  • Если значение pathname — непустая строка, содержащая относительный путь и dirfd равно специальному значению AT_FDCWD, то pathname рассматривается относительно текущего рабочего каталога вызывающего и описатель возвращается для файла, на который он указывает.
  • Если значение pathname — непустая строка, содержащая относительный путь и dirfd равно файловому дескриптору, указывающему на каталог, то pathname рассматривается относительно каталога, на который указывает dirfd, и описатель возвращается для файла, на который он указывает (смотрите в openat(2) объяснение полезности «файловых дескрипторов каталогов»).
  • Если значение pathname — пустая строка и значение flags равно AT_EMPTY_PATH, то dirfd может быть открытым файловым дескриптором, указывающим на файл любого типа, или AT_FDCWD, означающим текущий рабочий каталог, и описатель возвращается для файла, на который он указывает.

В аргументе mount_id возвращается идентификатор точки монтирования в файловой системе, соответствующий pathname. Это значение соответствует первому полю одной из записей в /proc/self/mountinfo. Открытие пути из пятого поля этой записи возвращает файловый дескриптор этой точки монтирования; этот файловый дескриптор можно использовать в последующем вызове open_by_handle_at(). Аргумент mount_id возвращается при успешном выполнении, а также при ошибке EOVERFLOW().

По умолчанию, name_to_handle_at() не разыменовывает pathname, если это символическая ссылка, и поэтому возвращается описатель самой ссылки. Если в flags указан AT_SYMLINK_FOLLOW, то pathname разыменовывается, если это символическая ссылка (то есть вызов возвращает описатель файла, на который указывает ссылка).

Системный вызов name_to_handle_at() не вызывает монтирования, если конечная часть пути является автоматической точкой монтирования. Если файловая система поддерживает файловые описатели и автоматические точки монтирования, то вызов name_to_handle_at() для автоматической точки монтирования завершится ошибкой EOVERFLOW без увеличения значения handle_bytes. Это может происходить с NFS начиная с версии Linux 4.13, когда задействованный каталог находится в отдельной файловой системе на сервере. В этом случае автомонтирование можно получить добавлением «/» в конец пути.

Системный вызов open_by_handle_at() открывает файл, на который указывает handle, файловый описатель, полученный от предшествующего вызова name_to_handle_at().

Аргумент mount_fd — это файловый дескриптор любого объекта (файла, каталога и т. д.) в смонтированной файловой системе, в которой должен находиться handle. Может быть равен специальному значению AT_FDCWD, которое обозначает текущий рабочий каталог вызывающего.

Значение аргумента flags как у open(2). Если handle указывает на символическую ссылку, то вызывающий должен указать флаг O_PATH, и символическая ссылка не разыменовывается; флаг O_NOFOLLOW игнорируется.

Для вызова open_by_handle_at() вызывающий должен иметь мандат CAP_DAC_READ_SEARCH.

ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ

При успешном выполнении name_to_handle_at() возвращает 0, а open_by_handle_at() возвращает неотрицательный файловый дескриптор.

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

ОШИБКИ

Вызовы name_to_handle_at() и open_by_handle_at() могут завершиться с теми же ошибками что и openat(2). Также могут возникать следующие ошибки:

Вызов name_to_handle_at() может завершиться со следующими ошибками:

Значение pathname, mount_id или handle указывает за пределы доступного адресного пространства.
Значение flags содержит некорректно установленный бит.
Значение handle->handle_bytes больше MAX_HANDLE_SZ.
Значение pathname равно пустой строке, но в flags не указано значение AT_EMPTY_PATH.
Файловый дескриптор, указанный в dirfd, не ссылается на каталог и это не тот случай, когда flags содержит AT_EMPTY_PATH и pathname равно пустой строке.
Файловая система не поддерживает преобразование пути в файловый описатель.
Значение handle->handle_bytes, переданное в вызов, слишком мало. При этой ошибке handle->handle_bytes присваивается требуемый размер для описателя.

Вызов open_by_handle_at() может завершиться со следующими ошибками:

Значение mount_fd не является открытым файловым дескриптором.
Значение handle указывает за пределы доступного адресного пространства.
Значение handle->handle_bytes больше MAX_HANDLE_SZ или равно нулю.
Значение handle указывает на символическую ссылку, но в flags не указан O_PATH.
Вызывающий не имеет мандата CAP_DAC_READ_SEARCH.
Значение handle некорректно. Эта ошибка возникает, например, из-за удаления файла.

ВЕРСИИ

Данные системные вызовы впервые появились в Linux 2.6.39. Поддержка в glibc появилась в версии 2.14.

СООТВЕТСТВИЕ СТАНДАРТАМ

Данные системные вызовы являются нестандартными расширениями Linux.

В FreeBSD есть относительно похожая пара системных вызовов getfh() и openfh().

ЗАМЕЧАНИЯ

Файловый описатель может быть сгенерирован с помощью name_to_handle_at() в одном процессе и использован в вызовах open_by_handle_at() в другом.

Некоторые файловые системы не поддерживают трансляцию путей в файловые описатели, например: /proc, /sys и различные сетевые файловые системы.

Файловый описатель может стать некорректным («просроченным»), если файл удалён, или по другим причинам, относящимся к файловой системе. Для некорректных описателей open_by_handle_at() возвращает ошибку ESTALE.

Данные системные вызовы предназначены для использования в файловых серверах пространства пользователя. Например, сервер пользовательского пространства NFS может генерировать файловый описатель и передавать его клиенту NFS. Позднее, когда клиент захочет открыть файл, он может передать описатель обратно серверу. Такого рода возможность позволяет файловому серверу пространства пользователя работать без формирования состояния (stateless fashion) для файлов, которые они обслуживают.

Если pathname указывает на символическую ссылку и в flags отсутствует AT_SYMLINK_FOLLOW, то name_to_handle_at() возвращает описатель ссылки (а не файла, на который она ссылается). Процесс, получивший описатель, может позднее выполнить операции над символической ссылкой, преобразовав описатель в файловый дескриптор, используя open_by_handle_at() с флагом O_PATH, и затем передав файловый дескриптор через аргумент dirfd в системные вызовы readlinkat(2) и fchownat(2).

Получение постоянного идентификатора файловой системы

Идентификаторы монтирования в /proc/self/mountinfo могут быть использованы повторно после размонтирования и монтирования файловой системы. Поэтому, идентификатор монтирования, возвращаемый name_to_handle_at() (в *mount_id), не должен считаться постоянным идентификатором соответствующей файловой системы. Однако, приложение может использовать информацию в записи mountinfo, которая соответствует идентификатору монтирования, для получения постоянного идентификатора.

Например, можно использовать имя устройства в пятом поле записи mountinfo для поиска соответствующего устройству UUID через символические ссылки в /dev/disks/by-uuid (более удобный способ получения UUID — использовать библиотеку libblkid(3)). Этот процесс может быть и обратным — используя UUID найти имя устройства, и затем получить соответствующую точку монтирования, чтобы создать аргумент mount_fd, используемый для open_by_handle_at().

ПРИМЕР

Две представленные далее программы демонстрируют использование name_to_handle_at() и open_by_handle_at(). Первая программа (t_name_to_handle_at.c) использует name_to_handle_at() для получения файлового описателя и идентификатора монтирования для файла, указанного в аргументе командной строки; описатель и идентификатор монтирования записываются в стандартный вывод.

Вторая программа (t_open_by_handle_at.c) читает идентификатор монтирования и файловый описатель из стандартного ввода. Затем программа, используя описатель, применяет open_by_handle_at() для открытия файла. Если указан необязательный параметр командной строки, то аргумент mount_fd для open_by_handle_at() создаётся из открытия каталога, указанного в аргументе. В противном случае mount_fd заполняется результатом сканированием /proc/self/mountinfo в целях найти запись, чей идентификатор монтирования совпадает с идентификатором монтирования из стандартного ввода, и открывается каталог монтирования, указанный в этой записи (эти программы не учитывают, что идентификатор монтирования не постоянен).

Следующий сеанс работы в оболочке показывает использование этих программ:

$ echo 'Can you please think about it?' > cecilia.txt
$ ./t_name_to_handle_at cecilia.txt > fh
$ ./t_open_by_handle_at < fh
open_by_handle_at: Operation not permitted
$ sudo ./t_open_by_handle_at < fh      # требуется CAP_SYS_ADMIN
Read 31 bytes
$ rm cecilia.txt

Теперь мы удаляем и (быстро) пересоздаём файл с тем же содержимым и (если повезёт) с той же инодой. Не смотря на это, open_by_handle_at() распознаёт, что первоначальный файл, на который указывал файловый описатель, больше не существует.

$ stat --printf="%i\n" cecilia.txt     # вывести номер иноды
4072121
$ rm cecilia.txt
$ echo 'Can you please think about it?' > cecilia.txt
$ stat --printf="%i\n" cecilia.txt     # проверить номер иноды
4072121
$ sudo ./t_open_by_handle_at < fh
open_by_handle_at: Stale NFS file handle

Исходный код программы: t_name_to_handle_at.c

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \

                        } while (0)
int
main(int argc, char *argv[])
{

    struct file_handle *fhp;

    int mount_id, fhsize, flags, dirfd, j;

    char *pathname;

    if (argc != 2) {

        fprintf(stderr, "Использование: %s путь\n", argv[0]);

        exit(EXIT_FAILURE);

    }

    pathname = argv[1];

    /* выделяем место под структуру file_handle */

    fhsize = sizeof(*fhp);

    fhp = malloc(fhsize);

    if (fhp == NULL)

        errExit("malloc");

    /* выполняем первоначальный вызов name_to_handle_at() для

       определения требуемого размера файлового описателя */

    dirfd = AT_FDCWD;           /* для вызовов name_to_handle_at() */

    flags = 0;                  /* для вызовов name_to_handle_at() */

    fhp->handle_bytes = 0;

    if (name_to_handle_at(dirfd, pathname, fhp,

                &mount_id, flags) != -1 || errno != EOVERFLOW) {

        fprintf(stderr, "Unexpected result from name_to_handle_at()\n");

        exit(EXIT_FAILURE);

    }

    /* перераспределяем структуру file_handle с правильным размером */

    fhsize = sizeof(struct file_handle) + fhp->handle_bytes;

    fhp = realloc(fhp, fhsize);         /* копируем fhp->handle_bytes */

    if (fhp == NULL)

        errExit("realloc");

    /* получаем файловый описатель из пути, который

       указан в командной строке */

    if (name_to_handle_at(dirfd, pathname, fhp, &mount_id, flags) == -1)

        errExit("name_to_handle_at");

    /* пишем идентификатор монтирования, размер файлового описателя

       и файловый описатель в stdout

       для повторного использования в t_open_by_handle_at.c */

    printf("%d\n", mount_id);

    printf("%d %d   ", fhp->handle_bytes, fhp->handle_type);

    for (j = 0; j < fhp->handle_bytes; j++)

        printf(" %02x", fhp->f_handle[j]);

    printf("\n");

    exit(EXIT_SUCCESS);
}

Исходный код программы: t_open_by_handle_at.c

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \

                        } while (0)
/* сканируем /proc/self/mountinfo в поиске строки, чей идентификатор

   монтирования совпадает с 'mount_id' (простейший способ,

   это установить и использовать библиотеку

   «libmount», предоставляемую проектом «util-linux»).

   Открываем соответствующий путь монтирования и возвращаем

   полученный файловый дескриптор. */
static int
open_mount_path_by_id(int mount_id)
{

    char *linep;

    size_t lsize;

    char mount_path[PATH_MAX];

    int mi_mount_id, found;

    ssize_t nread;

    FILE *fp;

    fp = fopen("/proc/self/mountinfo", "r");

    if (fp == NULL)

        errExit("fopen");

    found = 0;

    linep = NULL;

    while (!found) {

        nread = getline(&linep, &lsize, fp);

        if (nread == -1)

            break;

        nread = sscanf(linep, "%d %*d %*s %*s %s",

                       &mi_mount_id, mount_path);

        if (nread != 2) {

            fprintf(stderr, "Bad sscanf()\n");

            exit(EXIT_FAILURE);

        }

        if (mi_mount_id == mount_id)

            found = 1;

    }

    free(linep);

    fclose(fp);

    if (!found) {

        fprintf(stderr, "Could not find mount point\n");

        exit(EXIT_FAILURE);

    }

    return open(mount_path, O_RDONLY);
}
int
main(int argc, char *argv[])
{

    struct file_handle *fhp;

    int mount_id, fd, mount_fd, handle_bytes, j;

    ssize_t nread;

    char buf[1000];
#define LINE_SIZE 100

    char line1[LINE_SIZE], line2[LINE_SIZE];

    char *nextp;

    if ((argc > 1 && strcmp(argv[1], "--help") == 0) || argc > 2) {

        fprintf(stderr, "Использование: %s [путь-монт.]\n", argv[0]);

        exit(EXIT_FAILURE);

    }

    /* Стандартный ввод содержит идентификатор монтирования и

       информацию о файловом описателе:

         Строка 1: <идентификатор монтирования>

         Строка 2: <handle_bytes> <handle_type>

                    <байты описателя в шестнад. системе счисления>

    */

    if ((fgets(line1, sizeof(line1), stdin) == NULL) ||

           (fgets(line2, sizeof(line2), stdin) == NULL)) {

        fprintf(stderr, "Missing mount_id / file handle\n");

        exit(EXIT_FAILURE);

    }

    mount_id = atoi(line1);

    handle_bytes = strtoul(line2, &nextp, 0);

    /* получаем handle_bytes, теперь мы можем выделить место

       под структуру file_handle */

    fhp = malloc(sizeof(struct file_handle) + handle_bytes);

    if (fhp == NULL)

        errExit("malloc");

    fhp->handle_bytes = handle_bytes;

    fhp->handle_type = strtoul(nextp, &nextp, 0);

    for (j = 0; j < fhp->handle_bytes; j++)

        fhp->f_handle[j] = strtoul(nextp, &nextp, 16);

    /* получаем файловый дескриптор для точки монтирования, или

       открываем путь, указанный в командной строке, или сканируем

       /proc/self/mounts в поиске монтирования, которое совпадает с

       «mount_id», который мы получили из stdin. */

    if (argc > 1)

        mount_fd = open(argv[1], O_RDONLY);

    else

        mount_fd = open_mount_path_by_id(mount_id);

    if (mount_fd == -1)

        errExit("opening mount fd");

    /* открываем файл, используя описатель и точку монтирования */

    fd = open_by_handle_at(mount_fd, fhp, O_RDONLY);

    if (fd == -1)

        errExit("open_by_handle_at");

    /* пытаемся прочитать несколько байт из файла */

    nread = read(fd, buf, sizeof(buf));

    if (nread == -1)

        errExit("read");

    printf("Read %zd bytes\n", nread);

    exit(EXIT_SUCCESS);
}

СМОТРИТЕ ТАКЖЕ

open(2), libblkid(3), blkid(8), findfs(8), mount(8)

Документация libblkid и libmount в последнем выпуске util-linux https://www.kernel.org/pub/linux/utils/util-linux/

2019-03-06 Linux