1)前言
随着docker的出现, Linux container这种轻量级虚拟化方案越来越在产业里得到大规模的部署和应用. 而Namespace是Linux Container的基础, 了解namespace的实现对了解container和docker有着关键的作用. 本着知其然亦知其所以然的原则, 这个系列的笔记会对namespace的方方面面做一个详尽的分析.
简而言之, namespace就是一种纯软件的隔离方案. Namespace这个词在学习编程的语言的时候学习过, 在程序设计里, 它指变量或者函数的作用范围. 在Kernel里也有类似的作用, 可以把它理解成操作的可见范围, 例如同一个namespace的对象(即进程)可以看到另一个进程的存在和资源(如进程的pid, 进程间通信, 等等).
这一节会介绍namespace在用户空间的接口, 这样可以方便地建立namespace的全局观.
2) namespace的生命周期
namespace的生存周期如下图所示
上图列出了namespace的状态转换以及对应的操作. 据此可知, namespace会经历创建, 加入, 离开以及销毁这几个过程.
2.1) 创建namespace
新的namespace由带有CLONE_NEW*标志的clone() system call所创建. 这些标志包括: CLONE_NEWIPC,CLONE_NEWNET,CLONE_NEWNS,CLONE_NEWPID,CLONE_NEWUTS,CLONE_NEWUSER,这些标志分别表示namespace所隔离的资源:
- CLONE_NEWPID: 创建一个新的PID namespace. 只有在同一个namespace里的进程才能看到相互的 它直接影 响用户空间类似ps命令的行为.
- CLONE_NEWNET: 创建一个新的Network namespace, 将网络协议栈进行隔离,包括网络接口,ipv4/ipv6协议栈,路由,iptable规则, 等等.
- CLONE_NEWNS: 创建一个新的Mount namespace, 它将mount的行为进行隔离,也就是说mount只在同一个mount namespace中可见。BTW,根据它的行为,这里恰当的名称应该是CLONE_NEWMOUNT. 这是历史遗留的原因,因为mount namespace是第一个namespace且当时没有人想到会将这套机制扩展到其它的subsystem, 等它成了API, 想改名字也没有那么容易了。
- CLONE_NEWUTS: 创建一个新的UTS namespace, 同理, 它用来隔离UTS. UTS包括domain name, host name. 直接影响setdomainname(), sethostname()这类接口的行为.
- CLONE_NEWIPC: 创建一个新的IPC namespace, 用来隔离进程的IPC通信, 直接影响ipc shared memory, ipc semaphore等接口的行为.
- CLONE_NEWUSER: 创建一个新的User namespace, 用来隔离用户的uid, gid. 用户在不同的namespace中允许有不同的uid和gid, 例如普通用户可以在子container中拥有root权限。这是一个新的namespace, 在Linux Kernel 3.8中被加入,所以在较老的发行版中,man clone可能看不到这个标志.
2.2) namespace的组织
进程所在的namespace可以在/proc/$PID/ns/中看到. Pid为1是系统的init进程, 它所在的namespace为原始的namespace,如下示:
其下面的文件依次表示每个namespace, 例如user就表示user namespace. 所有文件均为符号链接, 链接指向$namespace:[$namespace-inode-number], 前半部份为namespace的名称,后半部份的数字表示这个namespace的 inode number. 每个namespace的inode number均不同, 因此, 如果多个进程处于同一个namespace. 在该目录下看到的inode number是一样的,否则可以判定为进程在不同的namespace中。
该链接指向的文件比较特殊,它不能直接访问,事实上指向的文件存放在被称为”nsfs”的文件系统中,该文件系统用户不可见。可以用stat()看到指向文件的inode信息:
这个文件在后续分析namespace实现的时候再来详细讲解。
再来看看当前shell的namespace:
可以看到它跟init进程处于同一个namespace里面。
再用unshare来启动一个新的shell
可以看到新的shell已经运行到了完全新的namespace里面,所有的namespace均和父进程不一样了。
2.3) 加入namespace
加入一个已经存在的namespace中以通过setns() 系统调用来完成。它的原型如下
int setns(int fd, int nstype);
第一个参数fd由打开/proc/$PID/ns/下的文件所得到,nstype表示要加入的namespace类型。一般来说,由fd就可以确定namespace的类型了,nstype只是起一个辅助check的作用。如果调用者明确知道fd是由打开相应的namespace文件所得到,可以nstype设为0,来bypass这个check. 相反的,如果fd是由其它组件传递过来的,调用者不知道它是否是open想要的namespace而得到,就可以设置对应nstype来让kernel做check。
util-linux这个包里提供了nsenter的指令, 其提供了一种方式将新创建的进程运行在指定的namespace里面, 它的实现很简单, 就是通过命令行指定要进入的namespace的file, 然后利用setns()指当前的进程放到指定的namespace里面, 再clone()运行指定的执行文件. 我们可以用strace来看看它的运行情况:
# strace nsenter -t 6814 -i -m -n -p -u /bin/bash
execve(“/usr/bin/nsenter”, [“nsenter”, “-t”, “6814”, “-i”, “-m”, “-n”, “-p”, “-u”, “/bin/bash”], []) = 0
brk(0) = 0xb13000
……
open(“/proc/6814/ns/ipc”, O_RDONLY) = 3
open(“/proc/6814/ns/uts”, O_RDONLY) = 4
open(“/proc/6814/ns/net”, O_RDONLY) = 5
open(“/proc/6814/ns/pid”, O_RDONLY) = 6
open(“/proc/6814/ns/mnt”, O_RDONLY) = 7
setns(3, CLONE_NEWIPC) = 0
close(3) = 0
setns(4, CLONE_NEWUTS) = 0
close(4) = 0
setns(5, CLONE_NEWNET) = 0
close(5) = 0
setns(6, CLONE_NEWPID) = 0
close(6) = 0
setns(7, CLONE_NEWNS) = 0
close(7) = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fab236d8a10) = 20034
wait4(20034, [[email protected] /]#
从上可以看到, nsenter先获得target进程(-t参数指定)所在的namespace的文件, 然后再调用setns()将当前所在的进程加入到对应的namespace里面, 最后再clone()运行我们指定的二进制文件.
我们来看一个实际的例子, 先打开一个 terminal:
再打开另一个terminal, 将新的进程加入到第一个terminal创建的namespace
先通过 ps aux | grep /bin/bash找到我们在第一个terminal里运行的程序, 在这里需要注意新的进程并不是unshare对应的进程. 这里我们找到的pid是2342, 通过proc下的ns文件进行确认, 看到这个进程所在的namespace确实是我们在第一个terminal所创建的namespace.
最后通过nsenter将要运行的进程加入到这个namespace里. 在这里我们在nsenter中并没有使用-U (–user)参数将进程加入到新的user namespace里, 这是因为nsenter的一个bug, 在同时指定user namespace和其它的namepsace里, 它会先加user namespace, 造成在操作其它的namespace时权限不够, 如下示:
我们在随后分析namespace实现的时候再来详细分析这个bug.
现在这两个进程都在同样的namespace里面了(除了user namespace外), 我们来看看:
可以看到这两个进程在同一个pid namespace里. 我们同样地可以进行mount, uts等其它namespace的check.
2.4) 离开namespace
unshare()系统调用用于将当前进程和所在的namespace分离并且加入到新创建的namespace之中. Unshare()的原型定义如下:
int unshare(int flags);
flags的定义如下:
CLONE_FILES
使当前进程的文件描述符不再和其它进程share. 例如, 我们可以使用clone(CLONE_FILES)来创建一个新的进程并使这个新的进程share父进程的文件描述符, 随后如果子进程不想再和父进程share这些文件描述符,可以通过unshare(CLONE_FILES)来终止这些share.
CLONE_FS
使当前进程的文件系统信息(包括当前目录, root目录, umask)不再和其它进程进行share. 它通常与clone()配合使用.
CLONE_SYSVSEM
撤消当前进程的undo SYS V信号量并使当前进程的sys V 信息量不再和其它进程share.
CLONE_NEWIPC
通过创建新的ipc namespace来分离与其它进程share的ipc namespace, 并且包含CLONE_SYSVSEM的作用
CLONE_NEWNET, CLONE_NEWUTS, CLONE_NEWUSER, CLONE_NEWNS, CLONE_NEWPID
与CLONE_NEWIPC类似, 分别使当前进程创建新的namespace, 不再与其它进程share net, uts, user, mount, pid namespace.
在这里需要注意的是, unshare不仅退出当前进程所在的namespace而且还会创建新的namespace, 严格说来, unshare也是创建namespace的一种方式.
Unshare程序在前面已经使用过很多次了, 它实际上就是调用unshare()系统调用, 可以strace进程查看:
# strace -o /tmp/log unshare -p -f /bin/bash
# cat /tmp/log | grep unshare
execve(“/usr/bin/unshare”, [“unshare”, “-p”, “-f”, “/bin/bash”], []) = 0
unshare(CLONE_NEWPID) = 0
在这里可以看到 –p参数对应的操作是unshare(CLONE_NEWPID).
2.5) 销毁namespace
Linux kernel没有提供特定的接口来销毁namespace, 销毁的操作是自动进行的. 在后面的分析中我们可以看到, 每一次引用namespace就会增加一次引用计数, 直至引用计数为0时会将namespace自动删除.
那在用户空间中, 我们可以open /proc/$PID/ns下的文件来增加引用计数, 还可以通过mount bind的操作来增加计数, 如下所示:
[[email protected] ~]# mount –bind /proc/2342/ns/pid /var/log^C
[[email protected] ~]# echo > /tmp/pid-ns
[[email protected] ~]# mount –bind /proc/2342/ns/pid /tmp/pid-ns
[[email protected] ~]# stat /tmp/pid-ns
File: ‘/tmp/pid-ns’
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: 3h/3d Inode: 4026532392 Links: 1
Access: (0444/-r–r–r–) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2015-07-28 14:28:13.724408143 +0800
Modify: 2015-07-28 14:28:13.724408143 +0800
Change: 2015-07-28 14:28:13.724408143 +0800
Birth: –
[[email protected] ~]# stat -L /proc/2342/ns/pid
File: ‘/proc/2342/ns/pid’
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: 3h/3d Inode: 4026532392 Links: 1
Access: (0444/-r–r–r–) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2015-07-28 14:28:13.724408143 +0800
Modify: 2015-07-28 14:28:13.724408143 +0800
Change: 2015-07-28 14:28:13.724408143 +0800
Birth: –
可以看到它们最终对应的是同一个文件.
3) 总结
在这一节里, 我们看到了namespace的生命周期以及各个阶段对应的操作. 这些操作都可在用户空间直接进行, LXC和docker的底层都是基于这些操作. 在进行操作的时候, 有一个基本原则, 那就是只有当前进程才能操作自己所在的namespace, Linux并没有接口来改变另一个进程的namespace.
1)前言
前一篇笔记分析了namespace的生命周期以及其对应的操作, 在其中曾提到每个进程对应的namespace都可以在/prc/$PID/ns下面找到, 可以据此来比较进程是否在同一namespace以及据此来判断加入的目标namespace. 这一节中会来详细分析namespace在proc中的实现.
2) namespace的通用操作
每一个namespace的结构都内嵌了struct ns_common的结构体, 例如 uts namespace:
struct uts_namespace {
struct kref kref;
struct new_utsname name;
struct user_namespace *user_ns;
struct ns_common ns;
};
struct ns_common集合了namespace在proc中的所有抽象, 它的定义如下:
struct ns_common {
atomic_long_t stashed;
const struct proc_ns_operations *ops;
unsigned int inum;
};
事实上/proc/$PID/ns/下每个文件对应一个namespace, 它是一个符号链接, 会指向一个仅kernel可见的被称为nsfs的文件系统中的一个inode. 本文后面会对这个文件系统进行分析. 在这里stashed正是用来存放这个文件的dentry. 在这里的类型为atomic_long_t 而非 struct dentry是因为更改stashed的操作是lockless (原子) 的.
Inum是一个唯一的proc inode number. 虽然它是从proc 文件系统中分配的inode number, 但仅用在nsfs中, 它被用做nsfs的inode number, 只需要保证这个number在nsfs中唯一就可以了.
Ops对应该namespace的操作, 其定义如下:
struct proc_ns_operations {
const char *name;
int type;
struct ns_common *(*get)(struct task_struct *task);
void (*put)(struct ns_common *ns);
int (*install)(struct nsproxy *nsproxy, struct ns_common *ns);
}
name为namespace的名字, type为namespace的类型, 例如user namespace的类型为CLONE_NEWUSER, 它用于在setns()系统调用中用来匹配nstype 参数.
get()用于获得namespace的引用计数, put()执行相反的操作.
Install()用于将进程安装到指定的namesapce里. @ns将会直接安装到@nsproxy. 它在setns()系统调用中被使用.
Struct nsproxy是一个新的数据结构, 有必要来看看它的定义和使用.
struct nsproxy {
atomic_t count;
struct uts_namespace *uts_ns;
struct ipc_namespace *ipc_ns;
struct mnt_namespace *mnt_ns;
struct pid_namespace *pid_ns_for_children;
struct net *net_ns;
};
而struct nsproxy又是内嵌入在task_struct (用来表示进程) 中. 从它的定义中可以看出, 它是进程所在的namespace的集合, 需要注意的是user namespace比较特殊它并没有包含在struct nsproxy中, 后续在分析user namespace的时候再回过头来看它.
Count表示的是nsproxy的引用计数, 当全部namespace被完整clone的时候, 计用计数加1, 例如fork()系统调用的时候.
Namespace有自己单独的引用计数, 这是因为有时候我们只需要操作某个指定的namespace, 例如unshare()用来分离指定的namespace, 这个时候就需要将当前的nsproxy复制, 新的nsproxy->count初始化为1, 增加没有被更改的namespace的引用计数, 再将要更改的namespace进行更新. 如下图所示:
由于struct ns_common 都是内嵌在具体namespace的定义之中, 因此在ns operations里面可以使用container_of() 来将ns转换到具体的namespace定义.
2.1) uts namespace对应的common操作
上面说的都很抽象, 现在以uts namespace为例, 来看看namespace的common操作.
struct uts_namespace init_uts_ns = {
.kref = {
.refcount = ATOMIC_INIT(2),
},
.name = {
.sysname = UTS_SYSNAME,
.nodename = UTS_NODENAME,
.release = UTS_RELEASE,
.version = UTS_VERSION,
.machine = UTS_MACHINE,
.domainname = UTS_DOMAINNAME,
},
.user_ns = &init_user_ns,
.ns.inum = PROC_UTS_INIT_INO,
#ifdef CONFIG_UTS_NS
.ns.ops = &utsns_operations,
#endif
};
在前面已经看到了uts_namespace的定义. Init_uts_ns是系统中原始的也是第一个uts namespace, 它被关联到系统的Init进程, 系统中的其它进程都是在它的基础上进行创建的.
前面提到过, 每个namespace都包含有自己的引用计数, 在这里可以看到init_uts_ns的引用计数被初始化成2, 这是因为引用计数初始值为1, 而它直接关联到init task中 (静态定义)因此需要再加1.
Name表示系统的UTS信息, 用户空间的uname指令就是从这里把结果取出来的.
对于大多数的namespace而言都会有指针指向user namespace, 这是因为对namespace的操作会涉及到权限检查, 而namespace对应的uid. gid等信息都存放在user namespace中. 在这里可以看到init_uts_ns的user namespace是指向系统的原始user namespace.
接下来就是ns_common的初始化了. Inum被静态初始化成了PROC_UTS_INIT_INO, 它的定义为:
enum {
PROC_ROOT_INO = 1,
PROC_IPC_INIT_INO = 0xEFFFFFFFU,
PROC_UTS_INIT_INO = 0xEFFFFFFEU,
PROC_USER_INIT_INO = 0xEFFFFFFDU,
PROC_PID_INIT_INO = 0xEFFFFFFCU,
};
在用户空间进行确认一下:
# ll /proc/1/ns/uts
lrwxrwxrwx 1 root root 0 Jul 28 23:11 /proc/1/ns/uts -> uts:[4026531838]
0xEFFFFFFEU对应的十进制就是4026531838.
可能有一个疑问, 这里的inum是静态定义的, 那么在proc分配inum的时候会不会复用这个inum呢? 当然答案是不会, 这是因为proc inum是从PROC_DYNAMIC_FIRST 开始分配的, 它的定义为
#define PROC_DYNAMIC_FIRST 0xF0000000U
所以所有小于PROC_DYNAMIC_FIRST的值都可以拿来做静态定义. Inum分配算法可参考proc_alloc_inum()函数的代码.
接下来看uts对应的operations, 其定义如下:
const struct proc_ns_operations utsns_operations = {
.name = “uts”,
.type = CLONE_NEWUTS,
.get = utsns_get,
.put = utsns_put,
.install = utsns_install,
};
name和type从字面就可以理解它的含义. 先来看看get操作
static inline void get_uts_ns(struct uts_namespace *ns)
{
kref_get(&ns->kref);
}
static struct ns_common *utsns_get(struct task_struct *task)
{
struct uts_namespace *ns = NULL;
struct nsproxy *nsproxy;
task_lock(task);
nsproxy = task->nsproxy;
if (nsproxy) {
ns = nsproxy->uts_ns;
get_uts_ns(ns);
}
task_unlock(task);
return ns ? &ns->ns : NULL;
}
首先lock task_struct为防止并发操作, 其后从nsproxy中取出uts namespace, 再将其引用计数增加.
Put操作就更简单了, 看代码
static inline struct uts_namespace *to_uts_ns(struct ns_common *ns)
{
return container_of(ns, struct uts_namespace, ns);
}
static inline void put_uts_ns(struct uts_namespace *ns)
{
kref_put(&ns->kref, free_uts_ns);
}
static void utsns_put(struct ns_common *ns)
{
put_uts_ns(to_uts_ns(ns));
}
首先将之前说过的方法将ns转换成uts namespace, 然后再将它的引用数数减1. 或许有人在疑问, 为什么这里不需要持用锁了呢? 这是因为get和put都是配套使用的, 在get的时候已经持用引用计数了, 可确定put操作时uts namespace是合法的.
Install的操作如下示:
static int utsns_install(struct nsproxy *nsproxy, struct ns_common *new)
{
struct uts_namespace *ns = to_uts_ns(new);
if (!ns_capable(ns->user_ns, CAP_SYS_ADMIN) ||
!ns_capable(current_user_ns(), CAP_SYS_ADMIN))
return -EPERM;
get_uts_ns(ns);
put_uts_ns(nsproxy->uts_ns);
nsproxy->uts_ns = ns;
return 0;
}
nsproxy是当前进程的nsporxy copy, new表示的是要安装的uts namespace. 首先是权限检查, 这一部份等分析user namespace的时候再来详细研究.
只需要增加要安装的namespace的引用计数, 然后把旧的namespace的引用计数减掉, 再更新到nsproxy中就可以了.
3) /proc/$PID/ns/ 的实现
接下来看看proc下对应的ns目录下的文件的操作.
3.1) 创建/proc/$PID/ns目录
首先来看看ns目录是如何被生成的. “ns”目录对应的操作被定义在
struct pid_entry tgid_base_stuff[]和struct pid_entry tid_base_stuff[]
前者定义了每个进程在/proc/$PID/中所创建的文件, 后面者对应进程的thread所创建的文件, 位于/proc/$PID/task/目录中.
具体看一下ns目录对应的操作:
DIR(“ns”, S_IRUSR|S_IXUGO, proc_ns_dir_inode_operations, proc_ns_dir_operations)
据此可以持到, ns inode对应的操作为proc_ns_dir_operations , 目录对应的操作为proc_ns_dir_operations.
3.2) 读取/proc/$PID/ns目录
通过readdir或者getgents()读取/proc/$PID/ns就可以看到在它下面的所有文件了. 来看看该目录对应的操作:
const struct file_operations proc_ns_dir_operations = {
.read = generic_read_dir,
.iterate = proc_ns_dir_readdir,
};
最终读取目录的操作都会调用文件系统底层的iterate操作来完成, 来看proc_ns_dir_readdir的实现:
106 static int proc_ns_dir_readdir(struct file *file, struct dir_context *ctx)
107 {
108 struct task_struct *task = get_proc_task(file_inode(file));
109 const struct proc_ns_operations **entry, **last;
110
111 if (!task)
112 return -ENOENT;
113
114 if (!dir_emit_dots(file, ctx))
115 goto out;
116 if (ctx->pos >= 2 + ARRAY_SIZE(ns_entries))
117 goto out;
118 entry = ns_entries + (ctx->pos – 2);
119 last = &ns_entries[ARRAY_SIZE(ns_entries) – 1];
120 while (entry <= last) {
121 const struct proc_ns_operations *ops = *entry;
122 if (!proc_fill_cache(file, ctx, ops->name, strlen(ops->name),
123 proc_ns_instantiate, task, ops))
124 break;
125 ctx->pos++;
126 entry++;
127 }
128 out:
129 put_task_struct(task);
130 return 0;
131 }
114行用来返回”.”和”..”, 这个是每个目录都包含的entry, 分别表示本层目录和上一层目录.
116 行可以看到, 除了”.”和”..”外, 此目录下有ARRAY_SIZE(ns_entries)个文件.
118 行中减2是因为第一项和第二项分别对应为”.”和”..”.
最重要的操作在122行, proc_fill_cache()用来创建dentry和inode, 并将Inode的信息写入到ctx中. Dentry的名称长度对应为第三个参数和第四个参数, 也就是ops->name字符和它的长度. Inode的设置在proc_ns_ instantiate这个callback中完成.
由此可见, 在该目录下读出来的内容应该为ns_entries[]数组中的元素的name字段, 来看看这个数组的定义:
static const struct proc_ns_operations *ns_entries[] = {
#ifdef CONFIG_NET_NS
&netns_operations,
#endif
#ifdef CONFIG_UTS_NS
&utsns_operations,
#endif
#ifdef CONFIG_IPC_NS
&ipcns_operations,
#endif
#ifdef CONFIG_PID_NS
&pidns_operations,
#endif
#ifdef CONFIG_USER_NS
&userns_operations,
#endif
&mntns_operations,
};
正好对应了每一个namespace的名字.
3.3) /prc/$PID/ns/下的文件的操作
再来看看该目录下文件对应的具体操作, 在前面看到了, proc_ns_ instantiate()用来设置文件对应的inode, 来看看它的代码:
81 static int proc_ns_instantiate(struct inode *dir,
82 struct dentry *dentry, struct task_struct *task, const void *ptr)
83 {
……
92 ei = PROC_I(inode);
93 inode->i_mode = S_IFLNK|S_IRWXUGO;
94 inode->i_op = &proc_ns_link_inode_operations;
95 ei->ns_ops = ns_ops;
……
104 }
从这里可以看到, inode对应为S_IFLNK, 也主是说它是一个符号链接, 该文件对应的操作定义在proc_ns_link_inode_operations中:
static const struct inode_operations proc_ns_link_inode_operations = {
.readlink = proc_ns_readlink,
.follow_link = proc_ns_follow_link,
.setattr = proc_setattr,
};
readlink用来读取这个符号链接所指向的文件, follow_link用来找到这个链接所指向文件的inode.
Proc_ns_readlink()最终会调用ns_get_name()来获得它所指向的文件的名称, 其代码如下:
int ns_get_name(char *buf, size_t size, struct task_struct *task,
const struct proc_ns_operations *ns_ops)
{
struct ns_common *ns;
int res = -ENOENT;
ns = ns_ops->get(task);
if (ns) {
res = snprintf(buf, size, “%s:[%u]”, ns_ops->name, ns->inum);
ns_ops->put(ns);
}
return res;
}
首先它调用get()接口来获得该namespace的引用计数以防止在操作的过程中该namespace无效.
可看到对应的名字名 “ns_ops->name:ns->inum”, 也就是我们用ls –l在目录下看到的符号链接的信息.
最终再调用put()释放它的引用计数.
proc_ns_follow_link()最终会调用ns_get_path()来获得指向文件的inode信息, 这个操作涉及到了nsfs文件系统, 先来看看该文件系统的实现然后再回过来看这个函数.
3) nsfs文件系统
要分析ns在proc的操作, nsfs是一个绕不过去的话题. 这个文件系统在前面的分析中多次被提及, 它是/proc/$PID/ns下面的文件的最终指向, 而且这是一个用户没有办法操作的文件系统, 它也没有挂载点, 只是一个内建于内存中的文件系统.
用mount –bind就可以看到它的存在了, 如下示:
# echo > /tmp/tmp
# mount -o bind /proc/1/ns/uts /tmp/tmp
# mount
……
nsfs on /tmp/tmp type nsfs (rw)
在mount show出来的信息就可以看到/tmp/tmp是在nsfs中的. 可以check一下/proc/filesystems, 可看到它并末出现在里面, 因为并没有调用register_filesystem()将nsfs注册为全局可见的文件系统.
下面来看一下这个文件系统的真身. 它的定义以及初始化如下:
static struct file_system_type nsfs = {
.name = “nsfs”,
.mount = nsfs_mount,
.kill_sb = kill_anon_super,
};
void __init nsfs_init(void)
{
nsfs_mnt = kern_mount(&nsfs);
if (IS_ERR(nsfs_mnt))
panic(“can’t set nsfs up\n”);
nsfs_mnt->mnt_sb->s_flags &= ~MS_NOUSER;
}
它在kernel内部被mount, 本质上就是生成一个仅kernel可见的vfsmount结构.
在mount的时候, 会调用”struct file_system_type”中的mount这个callback, 该操作在nsfs对应为
static struct dentry *nsfs_mount(struct file_system_type *fs_type,
int flags, const char *dev_name, void *data)
{
return mount_pseudo(fs_type, “nsfs:”, &nsfs_ops,
&ns_dentry_operations, NSFS_MAGIC);
}
mount_pseudo()生成文件系统的super block, 并且初始化super block下的根目录, 该根目录对应的名字为第二个参数, 也就是”fsfs:”, super block的操作和dentry的操作分别在第三个和第四个参数中被指定, 最后一个参数是文件系统的Magic Number, 用来唯一标识一个文件系统.
3.1) nsfs的super block操作
先来看super block对应的操作, 它的定义如下:
static const struct super_operations nsfs_ops = {
.statfs = simple_statfs,
.evict_inode = nsfs_evict,
};
.statfs这个callback对应statfs系统调用, 它用来返回文件系统的信息, 比如文件系统的magic number, block size等.
.evict_inode在inode被destroy前被调用, 它的代码如下:
static void nsfs_evict(struct inode *inode)
{
struct ns_common *ns = inode->i_private;
clear_inode(inode);
ns->ops->put(ns);
}
逻辑很简单, 清空inode并且释放inode关联的namespace的引用计数.
3.2) nsfs的dentry操作
dentry的操作定义如下:
const struct dentry_operations ns_dentry_operations =
{
.d_prune = ns_prune_dentry,
.d_delete = always_delete_dentry,
.d_dname = ns_dname,
}
.d_prune: dentry在destroy 前被调用.
.d_delete: dentry的引用计数被完全释放时用来判断要不要把此dentry继续留在cache里. 在nsfs中, 该操作始终返回1, 也就是说, 没有引用计数的dentry都会被及时删除.
.d_dname: 用来获得dentry对应的path name. 在nsfs中, path name的表示为
“namespace name:[namespace inode number]”
ns_prune_dentry()的代码如下:
static void ns_prune_dentry(struct dentry *dentry)
{
struct inode *inode = d_inode(dentry);
if (inode) {
struct ns_common *ns = inode->i_private;
atomic_long_set(&ns->stashed, 0);
}
}
在后面的分析可以看到, ns->stashed实际上就是指向nsfs文件系统中的dentry. 在dentry要destroy 前, 先把这个指向关系清除.
3.3) ns_get_path()函数分析
现在分析完了nsfs的所有背景, 可以回过头来看看ns_get_path()的实现了. 该函数取得/proc/$PID/ns/下的符号链接所对应的实际文件.
46 void *ns_get_path(struct path *path, struct task_struct *task,
47 const struct proc_ns_operations *ns_ops)
48 {
49 struct vfsmount *mnt = mntget(nsfs_mnt);
50 struct qstr qname = { .name = “”, };
51 struct dentry *dentry;
52 struct inode *inode;
53 struct ns_common *ns;
54 unsigned long d;
55
56 again:
57 ns = ns_ops->get(task);
58 if (!ns) {
59 mntput(mnt);
60 return ERR_PTR(-ENOENT);
61 }
62 rcu_read_lock();
63 d = atomic_long_read(&ns->stashed);
64 if (!d)
65 goto slow;
66 dentry = (struct dentry *)d;
67 if (!lockref_get_not_dead(&dentry->d_lockref))
68 goto slow;
69 rcu_read_unlock();
70 ns_ops->put(ns);
71 got_it:
72 path->mnt = mnt;
73 path->dentry = dentry;
74 return NULL;
上面的代码是该函数的第一部份, 可以把这部份当成fast path, 在第63行判断dentry是否被cache到了ns->stashed中, 如果被cache就可以直接增加它的lockref, 然后返回.
注意在72行, mnt的信息被指向了nsfs_mnt, 也就是说符号链接直向的是nsfs中的dentry.
另一个值得注意的地方是在这个fast path中, ns_ops->get()和ns_ops->put()都是被配套调用的, 可以推测dentry其实被没有持用namespace的引用计数. 那是如何通过引用/proc/$PID/ns下的文件来保持namespace一直为live呢? 继续看下去.
75 slow:
76 rcu_read_unlock();
77 inode = new_inode_pseudo(mnt->mnt_sb);
78 if (!inode) {
79 ns_ops->put(ns);
80 mntput(mnt);
81 return ERR_PTR(-ENOMEM);
82 }
83 inode->i_ino = ns->inum;
84 inode->i_mtime = inode->i_atime = inode->i_ctime = CURRENT_TIME;
85 inode->i_flags |= S_IMMUTABLE;
86 inode->i_mode = S_IFREG | S_IRUGO;
87 inode->i_fop = &ns_file_operations;
88 inode->i_private = ns;
89
90 dentry = d_alloc_pseudo(mnt->mnt_sb, &qname);
91 if (!dentry) {
92 iput(inode);
93 mntput(mnt);
94 return ERR_PTR(-ENOMEM);
95 }
96 d_instantiate(dentry, inode);
97 dentry->d_fsdata = (void *)ns_ops;
98 d = atomic_long_cmpxchg(&ns->stashed, 0, (unsigned long)dentry);
99 if (d) {
100 d_delete(dentry);
101 dput(dentry);
102 cpu_relax();
103 goto again;
104 }
105 goto got_it;
106 }
这部份对应的是该函数的slow path. 如果dentry没有被cache或者是lockref成为了dead, 就需要生成新的dentry.
76-88行分配并初始化Inode, 该inode的ino为namespace的inode number, 对应的文件操作为ns_file_operations, 它实际上不支持任何操作.
90-95行分配并初始化dentry, 该dentry对应的名称为qname, 定义在第50行, 实际上为空.
98-104用来将dentry缓存到ns->stashed中, 如果有另一个路径抢在它之前更新了ns->stashed, 通过goto again来重新check.
105行, 如果一切正常, 通过goto got_it 直接返回. 从这里可以看到, 对于新创建的dentry, 并没有ns_ops->put(). 也就是说, namespace的引用计数其实是关联在inode上面的. 回忆之前分析的super block的evict_indoe()操作, 在Inode被销毁前, 会将它持有的ns的引用计数释放掉.
4) 小结
这节分析里涉及到了大量的文件系统的概念, 加大了整理和理解代码的难度. 不管怎么样, namespace在proc的操作以及nsfs文件系统都是namespace的框架, 理解了它们对理解namespace的生命周期是很有帮助的.