登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  linux

Linux name_to_handle_at 怎样保存可复用的文件句柄

来源:17golang原创

时间:2026-11-15 00:40:07 340浏览 收藏

先说结论:name_to_handle_at() 保存的不是路径,也不是可以自行拆解的 inode 字符串,而是一段由文件系统定义的 opaque file handle。要让它以后还有机会被 open_by_handle_at() 使用,至少要把 handle_type、有效的 f_handle 字节、对应的 mount_id 一起保存,并在重新打开时找到同一挂载的 mount_fd。此外,调用 open_by_handle_at() 需要 CAP_DAC_READ_SEARCH 能力。

这组系统调用适合做文件对象标识、文件系统事件关联或路径变化后的重新定位。它不适合被当作“永久路径替代品”:文件系统是否支持句柄、挂载是否仍是同一个对象、对象是否已经删除,都会影响后续打开。

name_to_handle_at 返回的到底是什么

Linux VFS 会把路径查找、目录项和 inode 等对象连接起来;系统调用返回的 file_handle 是文件系统提供的对象标识。调用方只应使用 handle_bytes 管理缓冲区大小,使用 handle_type 和 f_handle 原样保存或传给下一次系统调用。

pathname 经过 name_to_handle_at 变成 file_handle 和 mount_id 的静态生命周期关系图
图1:name_to_handle_at 句柄生命周期说明图,展示路径、opaque handle 与挂载身份的关系,不是运行截图。

这意味着保存记录至少应包含以下三类信息:

  • handle_type:后续解释这段句柄所需的类型。
  • f_handle 的有效字节:长度由成功返回后的 handle_bytes 决定。
  • mount_id:句柄所属挂载的身份,重新打开时用它定位参照挂载。

先用 handle_bytes 获取正确空间

struct file_handle 尾部的 f_handle 是变长区域,不能只按结构体本身的大小分配。稳妥流程是先把 handle_bytes 置零调用一次,让内核通过 EOVERFLOW 告知需要的字节数,然后按这个长度重新分配。

#define _GNU_SOURCE
#include 
#include 
#include 
#include 
#include 
#include 

static struct file_handle *capture_handle(const char *path, int *mount_id_out) {
    unsigned int bytes = 0;
    int mount_id = -1;

    // 第一次调用只探测所需的 f_handle 空间,不读取未分配的尾部。
    struct file_handle probe = { .handle_bytes = 0 };
    if (name_to_handle_at(AT_FDCWD, path, &probe, &mount_id, 0) != -1 ||
        errno != EOVERFLOW || probe.handle_bytes == 0) {
        // EOVERFLOW 且长度没有增加,可能代表文件系统不给这个对象提供句柄。
        return NULL;
    }
    bytes = probe.handle_bytes;

    // 尾部追加内核要求的字节数,并把输入长度写入 handle_bytes。
    struct file_handle *handle = calloc(1, sizeof(*handle) + bytes);
    if (handle == NULL) return NULL;
    handle->handle_bytes = bytes;

    // 第二次调用填入 handle_type、f_handle 和 mount_id。
    if (name_to_handle_at(AT_FDCWD, path, handle, &mount_id, 0) == -1) {
        free(handle); // 失败时释放包含尾部缓冲区的一整块内存。
        return NULL;
    }
    *mount_id_out = mount_id;
    return handle; // 调用者使用完 handle_type/f_handle 后负责 free。
}

这里不能把所有 EOVERFLOW 都理解成“只要再分配就能成功”。官方手册特别区分了两种情况:如果 handle_bytes 被增加,通常表示需要更大的缓冲区;如果错误返回时长度没有增加,可能是该对象没有可用的文件句柄,自动挂载点也可能落在这个边界。

保存时不要改写 opaque 数据

拿到句柄后,可以把它编码成自有记录格式,但不要根据当前文件系统猜测 f_handle 的字段含义,也不要把它截断成固定长度。一个实际的记录可以是:

struct saved_file_handle {
    int mount_id;
    int handle_type;
    unsigned int handle_bytes;
    unsigned char *handle_data;
};

static void release_saved_handle(struct saved_file_handle *saved) {
    // handle_data 与记录本身分开分配,释放时先清理句柄字节。
    free(saved->handle_data);
    saved->handle_data = NULL;
    saved->handle_bytes = 0;
}

static int copy_handle(const struct file_handle *src, int mount_id,
                       struct saved_file_handle *dst) {
    // 只复制实际返回的字节,避免把未使用容量当成句柄内容。
    dst->handle_data = malloc(src->handle_bytes);
    if (dst->handle_data == NULL) return -1;
    memcpy(dst->handle_data, src->f_handle, src->handle_bytes);
    dst->mount_id = mount_id;
    dst->handle_type = src->handle_type;
    dst->handle_bytes = src->handle_bytes;
    return 0;
}

如果记录需要跨进程或跨重启保存,建议同时记录生成时的文件系统、挂载信息和业务用途,但这些是诊断元数据,不是替代 mount_id 与句柄本身的必要字段。句柄数据仍应被视为只可由内核解释的字节串。

重新打开时为什么必须配对 mount_id

open_by_handle_at() 不只接收 file handle,还要接收一个 mount_fd,因为同一段句柄必须在正确的挂载上下文中解释。name_to_handle_at() 返回的 mount_id 对应 /proc/self/mountinfo 的挂载记录;找到相同挂载点后,打开一个位于该挂载中的对象即可得到参照文件描述符。

mount_id 经过 mountinfo 定位 mount_fd 并交给 open_by_handle_at 的静态依赖关系图
图2:mount_id 与 mount_fd 配对关系图,展示重新打开的静态条件和权限边界,不是执行结果。
# 先观察挂载记录,确认目标目录属于哪个挂载。
awk '$1 == TARGET_MOUNT_ID { print }' TARGET_MOUNT_ID=1234 /proc/self/mountinfo

# mount_fd 必须指向同一挂载中的对象;不要只凭路径字符串猜测。
mount_fd=$(open_same_mount_object_somehow)

# open_by_handle_at 还需要 CAP_DAC_READ_SEARCH,失败时先检查能力与挂载是否匹配。
./open-with-handle "$mount_fd"

上面的 shell 片段只表达定位关系,open_same_mount_object_somehow 不是系统自带命令。真正的 C 程序应解析 /proc/self/mountinfo,对目标挂载点执行 open() 或 openat(),再把得到的描述符作为 mount_fd 传入。

open_by_handle_at 的权限和符号链接边界

重新打开的核心调用可以概括为:

int reopen_saved(int mount_fd, const struct saved_file_handle *saved) {
    size_t total = sizeof(struct file_handle) + saved->handle_bytes;
    struct file_handle *handle = calloc(1, total);
    if (handle == NULL) return -1;

    // 恢复内核需要的类型和字节内容,不恢复路径字符串。
    handle->handle_bytes = saved->handle_bytes;
    handle->handle_type = saved->handle_type;
    memcpy(handle->f_handle, saved->handle_data, saved->handle_bytes);

    // flags 使用 open(2) 的语义;符号链接句柄通常需要 O_PATH。
    int fd = open_by_handle_at(mount_fd, handle, O_RDONLY | O_CLOEXEC);
    int saved_errno = errno;
    free(handle); // 系统调用完成后,临时恢复结构体即可释放。
    errno = saved_errno;
    return fd;
}

调用前要重点检查四件事:

  1. 进程是否具备 CAP_DAC_READ_SEARCH;没有该能力时,系统调用会失败。
  2. mount_fd 是否来自与保存记录匹配的挂载,而不是同路径下后来替换的另一个挂载。
  3. 目标文件系统是否支持这种文件句柄;AT_HANDLE_FID 只承诺用于标识对象,后续打开可能失败。
  4. 保存的是符号链接本身还是链接目标。默认不跟随最终符号链接;如果句柄指向符号链接,重新打开时通常需要 O_PATH。

什么时候不要把句柄当成永久引用

文件句柄适合减少对路径名的依赖,但它不是跨环境通用的序列化主键。下面这些情况都应该让业务回退到重新查找、重新生成记录或报告“对象不可恢复”:

  • 文件已经删除,或者文件系统不再提供同一对象。
  • 挂载被卸载、重新挂载,mount_id 与当前 mount 不再匹配。
  • 对象位于不支持句柄查找的文件系统或特殊自动挂载边界。
  • 保存时使用了 AT_HANDLE_FID,但恢复流程却把它当成一定可打开的句柄。
  • 程序没有需要的权限,或把不可信来源的句柄数据直接交给高权限进程。

小结:一条可落地的保存规则

阶段必须保留常见误区
采集handle_bytes、handle_type、f_handle、mount_id只保存路径或只保存固定长度字节
定位挂载与 mount_id 对应的 mount_fd把普通工作目录 fd 当成同一挂载凭据
重新打开匹配的 mount_fd、原始句柄、正确 flags忽略 CAP_DAC_READ_SEARCH 或符号链接语义
失败处理区分 EOVERFLOW、权限、挂载和对象生命周期失败后盲目扩大缓冲区或无限重试

一句话记忆:先用 handle_bytes 把句柄完整取出,再把 handle_type + f_handle + mount_id 当作一个整体保存;恢复时用同一挂载的 mount_fd 交给 open_by_handle_at(),并把权限和文件系统支持情况当作前置条件。

相关问题

name_to_handle_at() 为什么第一次调用会返回 EOVERFLOW?

当调用者把 handle_bytes 设为零用于探测空间时,内核会用 EOVERFLOW 返回所需大小。若错误返回时长度没有增加,则还要考虑该对象没有可用句柄或处于自动挂载边界。

mount_id 可以只保存成路径吗?

不建议。路径只是帮助你重新定位挂载的线索,真正的配对依据是返回的挂载身份和当前 /proc/self/mountinfo。路径对应的挂载可能被替换。

所有 file handle 都能被 open_by_handle_at() 打开吗?

不能。文件系统支持、句柄获取时的 flags、对象生命周期、挂载匹配和进程能力都会影响结果;尤其是用于对象标识的 AT_HANDLE_FID 不保证之后还能打开。

参考文档:https://man7.org/linux/man-pages/man2/name_to_handle_at.2.html、https://man7.org/linux/man-pages/man2/open_by_handle_at.2.html、https://docs.kernel.org/filesystems/vfs.html

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>