文章目录
- [1. 简介](#1. 简介)
- [2. 相关系统调用介绍](#2. 相关系统调用介绍)
-
- [2.1 clone](#2.1 clone)
- [2.2 unshare](#2.2 unshare)
- [2.3 setns](#2.3 setns)
- [2.4 mount](#2.4 mount)
- [2.5 umount2](#2.5 umount2)
- [2.6 OJ 系统 sand_box 中 mount namespace 构建全流程](#2.6 OJ 系统 sand_box 中 mount namespace 构建全流程)
- [3. 总结](#3. 总结)
1. 简介
Linux 中,namespace 是有关资源的一个重要概念,有点类似 cpp 中的 namespace , 本质就是划定一个区域,区域内的事物仅可见该区域,区域外的资源均不可见。
Linux 中,进程处在特定namespace 中,仅可见该 namespace 中的相关资源,namespace 外均不可见,因此实现了对进程的特定隔离。
Linux 中一共有 8 种 namespace, 较为常见的是mount, pid, network, user, ipc等 。
关于namespace ,有三个核心系统调用clone, unshare, setns, 当然,考虑到存在 mount namespace, 因此讲解 mount 和 umount2 也是必要的。
2. 相关系统调用介绍
2.1 clone
int clone(int (*fn)(void *), void *stack, int flags, void* arg)
clone 本质是 fork 的超集,fork 本质就是再调用 clone 。同时,pthread_create 线程创建,本质也是调用 clone 。
关于上述参数,逐个介绍:
fn: 子进程的入口函数。相较于 fork, 在父子进程中,各返回 1 次,而 clone 直接指定子进程执行函数,clone本身仅在 father process 中返回。- stack : 子进程的栈。在 clone 中,需要手动分配
clone的栈空间。特别注意的是,如果是创建子进程,那么 COW , 写时拷贝,会自动分配新的栈空间;但如果创建线程,必须手动分配。一般传入栈顶指针,因为大多数架构栈向下增长。 - flags : 关键参数。主要讲两类 flags。一类是
namespace相关 flags:- CLONE_NEWPID: 创建新的 PID NAMESPACE, 子进程进入该新的 PID NAMESPACE 中。此时,child process 自身成为 PID 1 .
- CLONE_NEWNS: 新的 mount namespace . 子进程拿到父进程挂载树的一份拷贝,之后独立操作。这是在进程隔离中,隔离目录树,即 dentry 结构的重要参数。
- CLONE_NEWNET: 新 network namespace 。 子进程看到一个干净的 network, 只有 loopback, 没有外部网络接口,实现网络隔离。
- CLONE_NEWUSER: 新的 user namespace . 这个特殊,因为是唯一一个非特权用户能够创建的 namespace 。 创建后,子进程在新的 user namespace 中,可拥有全部能力。
第二类是资源共享相关 flags, 主要用于区别创建进程和线程上: - CLONE_VM: share 地址空间,否则 COW。前者 thread, 后者进程。
- CLONE_FILES: 共享文件描述表,否则则拷贝一份。
- CLONE_FS: 共享 fs_struct .
- CLONE_SIGNALED: 信号处理表共享
- CLONE_THEAD: 放入同一个线程组,即创建线程。
2.2 unshare
int unshare(int flags);
unshare 是用于操作当前进程,即让当前进程脱离指定的 namespace, 进入新创建的 namespace 。
相较于 clone, 二者均会创建新的namespace ,但是 unshare 不创建新进程。
关于PID NAMESPACE, unshare 中,这个具备延迟语义,因为已存在进程,无法切换 PID namespace。因此,如果使用该 flags:那么相应子进程会进入新的 PID NAMESPACE 。
flags 中相关参数,和 clone 中的 namespace 参数完全相同。
2.3 setns
int setns(int fd, int nstype);
简而言之,让当前进程加入一个已存在的 namespace 。
setns 不创建新 namespace, 而是加入到其它进程已有的 namespace 中。
参数解析:
- fd: 文件描述符,**来自 /proc/pid/ns/ ** 下的
namespace文件,比如/proc/pid/ns/pid,/proc/pid/ns/net等。这些文件与普通文件不同,open后得到的 fd 是内核 namespace 对象的引用。 - nstype: 指定期望的 namespace 类型,本质做安全校验 。传 0 为不校验,一般建议显示传,防止误入错误的
namespace中。
实际上,就是将当前进程放入 fd 所对应的 namespace 中。
与 unshare 相同,对于 PID NAMESAPCE 同样具备延迟语义,得到的子进程才能进入目标 pid namespace 。
2.4 mount
关于 mount, 本质非 namespace 核心系统调用,主要是进入新的 mount namespace 后,需要频繁使用 mount 操作进行挂载。
int mount(const char *source, const char *target, const char *filesystemtype, unsigned long mountflags, const void *data);
本质:在当前 mount namespace 的挂载树上,将一个文件系统,或者源目录路径(可以是块设备) 挂到指定目录节点上,本质就是 映射。
下面逐一介绍参数:
- source: 挂载源头。一般是 块设备 ,tmpfs, 即虚拟内存文件系统 ,或者更常见的 bind mount, 即源目录路径。
- target: 将 source 挂载到的 路径。必须是已存在的目录或文件。
- filesystemtype: 当挂载 source 是
tmpfs, proc, ext4时,与相应的 source 对应,bind amount时,即映射具体文件路劲时,传 NULL 即可。 - mountflags: 控制 mount 具体行为。
- MS_BIND: bind amout 。把一个目录映射到另一个位置,并非新文件系统,而是两个路径指向同一个 dentry。
- MS_REC: 递归操作,配合 MS_BIND 或 MS_PRIVATE 使用,对子挂载点生效,通常用于目录,文件无必要。
- MS_RDONLY:只读挂载。
- MS_NOSUID: 忽略 SUID/SGID 位,防止提权。
- MS_PRIVATE: 设置挂载传播类型为 private, 父子之间 mount namespace 挂载事件完全隔离。
- data: 挂载文件系统时参数,bind amount 传 NULL 即可。例如,"size=16m,nr_inodes=32,mode=0755" 限制该路径下,写入文件总大小,创建文件总数目,以及创建权限等。
2.5 umount2
int umount2(const char *target, int flags)
mount 用于将 source 挂载到 target, umount2 用于解除 target 上的挂载。
参数解析:
- target: 相应解除挂载的目录。
- flags: 限制挂载接触行为,通常两种:MNT_DETACH 和 MNT_FORCE , 前者是惰性卸载,后者为强制卸载 ,二者都将挂载点从
target移除,但是 MNT_DETACH 内核资源等到合适再释放,一般使用这个,若强行 MNT_FORCE, 可能会失败。
2.6 OJ 系统 sand_box 中 mount namespace 构建全流程
下面过程,均在 child process 中完成。
第一步:设置传播类型为 private
mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL)
第二步:bind mount 相关目录
第三步:挂载相关文件系统等,如 tmpfs 或 proc
mount("tmpfs", target, "tmpfs", MS_NOSUID | MS_NODEV, "size=16m");
mount("proc", target, "proc", MS_NOSUID | MS_NODEV | MS_NOEXEC, NULL);
第四步:pivot_root ------ 切换根目录,最关键的一步,上述所有挂载处理完后,再切换根目录
mount(sandbox_root, sandbox_root, NULL, MS_BIND | MS_REC, NULL); // new_root 必须是一个挂载点
mkdir(put_old,0700); // 在 new_root 下创建一个 put_old 目录,用来临时存放旧根
syscall(SYS_pivot_root, sandbox_root, put_old);
chdir("/"); // 切换 cwd, 此时根目录解析起点已经改变
umount2("/,old_root", MNT_DETACH); // 卸除旧根目录挂载
rmdir("/.old_root"); // 删除无用目录
上述便是 OJ sandbox 中,完整 mount namespace 构建流程。上述构建后,child process 便处在一个 隔离的 mount namespace 中,无法看到宿主机上未经过bind mount 的其余文件。
3. 总结
简而言之,namespace 三个核心 syscall : clone, unshare, setns 。 clone 创建新进程+新NS,unshare 仅创建新 NS,setns 什么都不新建。clone 对 pid namespace 无延迟,unshare 和 setns 对 pid namespace 存在延迟。