先说结论: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 原样保存或传给下一次系统调用。
这意味着保存记录至少应包含以下三类信息:
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 的挂载记录;找到相同挂载点后,打开一个位于该挂载中的对象即可得到参照文件描述符。
# 先观察挂载记录,确认目标目录属于哪个挂载。
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;
}
调用前要重点检查四件事:
- 进程是否具备
CAP_DAC_READ_SEARCH;没有该能力时,系统调用会失败。 mount_fd是否来自与保存记录匹配的挂载,而不是同路径下后来替换的另一个挂载。- 目标文件系统是否支持这种文件句柄;
AT_HANDLE_FID只承诺用于标识对象,后续打开可能失败。 - 保存的是符号链接本身还是链接目标。默认不跟随最终符号链接;如果句柄指向符号链接,重新打开时通常需要
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