You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

嵌套挂载命名空间中readlink(2)结果异常:fork的作用是什么?

嵌套mount命名空间中readlink路径缺失挂载点的问题解析

问题描述

当在同一个进程中通过两次unshare(CLONE_NEWNS)创建嵌套的mount命名空间时,在最内层命名空间调用readlink(2)读取前一层命名空间打开的文件描述符时,返回的路径会丢失挂载点组件(比如/dev/zero变成/zero);但如果在两次unshare之间执行fork,这个问题就不会出现。请问fork操作改变了什么导致这种行为差异?

复现代码

#include <unistd.h>
#include <sched.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <iostream>
#include <string>
#include <vector>

std::string readlink_path(const std::string &path) {
    std::vector<char> buf(4096);
    ssize_t size = readlink(path.c_str(), buf.data(), buf.size());
    return size == -1 ? "" : std::string(buf.data(), static_cast<size_t>(size));
}
std::string readlink_fd(int fd) { return readlink_path("/proc/self/fd/" + std::to_string(fd)); }

int main(int argc, const char *argv[]) {
    int fdFromFirstNS = -1, fdFromInitialNS = -1;
    /* Initial namespace */
    std::cout << readlink_path("/proc/self/ns/mnt") << '\n';
    fdFromInitialNS = open("/dev/zero", O_RDONLY);
    std::cout << "open() = " << fdFromInitialNS << '\n';
    std::cout << "readlink(" << fdFromInitialNS << ") = " << readlink_fd(fdFromInitialNS) << "\n\n";
    
    /* First namespace */
    if (unshare(CLONE_NEWNS) != 0) goto END;
    std::cout << readlink_path("/proc/self/ns/mnt") << '\n';
    fdFromFirstNS = open("/dev/zero", O_RDONLY);
    std::cout << "open() = " << fdFromFirstNS << '\n';
    std::cout << "readlink(" << fdFromInitialNS << ") = " << readlink_fd(fdFromInitialNS) << '\n';
    std::cout << "readlink(" << fdFromFirstNS << ") = " << readlink_fd(fdFromFirstNS) << "\n\n";
    
    /* Fork if the first argument is "fork" */
    if (argc == 2 && std::string("fork") == argv[1]) {
        pid_t child_pid = fork();
        if (child_pid)
        {
            waitpid(child_pid, NULL, 0);
            goto END;
        }
        std::cout << "Forked. " << readlink_path("/proc/self/ns/mnt") << "\n\n";
    }
    
    /* Second namespace */
    if (unshare(CLONE_NEWNS) != 0) goto END;
    std::cout << readlink_path("/proc/self/ns/mnt") << '\n';
    std::cout << "readlink(" << fdFromInitialNS << ") = " << readlink_fd(fdFromInitialNS) << '\n';
    std::cout << "readlink(" << fdFromFirstNS << ") = " << readlink_fd(fdFromFirstNS) << '\n';
END:
    close(fdFromInitialNS);
    close(fdFromFirstNS);
}

运行输出

未执行fork的情况

执行命令:g++ test.cpp && sudo ./a.out

mnt:[4026532253]
open() = 3
readlink(3) = /dev/zero

mnt:[4026532259]
open() = 4
readlink(3) = /dev/zero
readlink(4) = /dev/zero

mnt:[4026532260]
readlink(3) = /dev/zero
readlink(4) = /zero

执行fork的情况

执行命令:g++ test.cpp && sudo ./a.out fork

mnt:[4026532253]
open() = 3
readlink(3) = /dev/zero

mnt:[4026532259]
open() = 4
readlink(3) = /dev/zero
readlink(4) = /dev/zero

Forked. mnt:[4026532259]

mnt:[4026532260]
readlink(3) = /dev/zero
readlink(4) = /dev/zero

原因解析

无fork时的挂载树关联问题

在同一个进程内连续调用unshare(CLONE_NEWNS)时,每次新创建的mount命名空间是直接复制当前进程的挂载树,但原进程与新命名空间共享部分内部挂载状态引用。当你在第一个命名空间打开/dev/zero后,再进入第二个命名空间,这个文件描述符对应的挂载点信息在新命名空间中无法正确映射——因为第二个命名空间的挂载树是从第一个复制而来,但第一个命名空间的/dev挂载点在新命名空间中没有被标记为“可见”的挂载前缀,导致readlink只能解析到挂载点内部的文件路径(即/zero)。

fork带来的挂载树完全隔离

在两次unshare之间执行fork时,子进程会继承父进程的mount命名空间,但子进程会拥有独立的挂载树副本,与父进程的挂载树彻底分离。此时在子进程中调用unshare(CLONE_NEWNS)创建第二个命名空间,新命名空间是基于子进程独立的挂载树复制而来。之前在第一个命名空间打开的文件描述符(fdFromFirstNS),其关联的挂载点信息在子进程的挂载树中完整保留,进入第二个命名空间后,readlink可以通过挂载树的映射关系正确解析出完整路径,不会丢失挂载点组件。

简单总结:

  • 同进程内多次unshare创建的命名空间共享进程内部挂载状态,跨命名空间的fd路径解析会丢失挂载点;
  • fork创建独立进程上下文,每个进程的挂载树独立,后续unshare的新命名空间能正确保留fd对应的挂载点映射。

内容的提问来源于stack exchange,提问作者user24145812

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 05:50:23