当前位置:首页 > 文章列表 > 文章 > linux > Linux name_to_handle_at 怎样保存可复用的文件句柄

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

来源:17golang原创 2026-11-15 00:40:07 0浏览 收藏

先说结论: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删除
systemd.timer OnBootSec 与 OnUnitActiveSec 的周期触发配置方法systemd.timer OnBootSec 与 OnUnitActiveSec 的周期触发配置方法
上一篇
systemd.timer OnBootSec 与 OnUnitActiveSec 的周期触发配置方法
journalctl -b 怎样筛选本次启动日志
下一篇
journalctl -b 怎样筛选本次启动日志
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    1029次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    1030次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1173次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1134次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    974次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码