嵌套挂载命名空间中readlink(2)结果异常:fork的作用是什么?
问题描述
当在同一个进程中通过两次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

