天天看点

Linux Kernel Namespace实现: namespace API介绍 1)前言 2) namespace的生命周期 1)前言 2) namespace的通用操作 3) /proc/$PID/ns/ 的实现 3) nsfs文件系统 4) 小结

1)前言

随着docker的出现, Linux container这种轻量级虚拟化方案越来越在产业里得到大规模的部署和应用. 而Namespace是Linux Container的基础, 了解namespace的实现对了解container和docker有着关键的作用. 本着知其然亦知其所以然的原则, 这个系列的笔记会对namespace的方方面面做一个详尽的分析.

简而言之, namespace就是一种纯软件的隔离方案. Namespace这个词在学习编程的语言的时候学习过, 在程序设计里, 它指变量或者函数的作用范围. 在Kernel里也有类似的作用, 可以把它理解成操作的可见范围, 例如同一个namespace的对象(即进程)可以看到另一个进程的存在和资源(如进程的pid, 进程间通信, 等等).

这一节会介绍namespace在用户空间的接口, 这样可以方便地建立namespace的全局观.

2) namespace的生命周期

namespace的生存周期如下图所示

Linux Kernel Namespace实现: namespace API介绍 1)前言 2) namespace的生命周期 1)前言 2) namespace的通用操作 3) /proc/$PID/ns/ 的实现 3) nsfs文件系统 4) 小结

上图列出了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,如下示:

Linux Kernel Namespace实现: namespace API介绍 1)前言 2) namespace的生命周期 1)前言 2) namespace的通用操作 3) /proc/$PID/ns/ 的实现 3) nsfs文件系统 4) 小结

其下面的文件依次表示每个namespace, 例如user就表示user namespace. 所有文件均为符号链接, 链接指向$namespace:[$namespace-inode-number], 前半部份为namespace的名称,后半部份的数字表示这个namespace的 inode number. 每个namespace的inode number均不同, 因此, 如果多个进程处于同一个namespace. 在该目录下看到的inode number是一样的,否则可以判定为进程在不同的namespace中。

该链接指向的文件比较特殊,它不能直接访问,事实上指向的文件存放在被称为”nsfs”的文件系统中,该文件系统用户不可见。可以用stat()看到指向文件的inode信息:

Linux Kernel Namespace实现: namespace API介绍 1)前言 2) namespace的生命周期 1)前言 2) namespace的通用操作 3) /proc/$PID/ns/ 的实现 3) nsfs文件系统 4) 小结

这个文件在后续分析namespace实现的时候再来详细讲解。

再来看看当前shell的namespace:

Linux Kernel Namespace实现: namespace API介绍 1)前言 2) namespace的生命周期 1)前言 2) namespace的通用操作 3) /proc/$PID/ns/ 的实现 3) nsfs文件系统 4) 小结

可以看到它跟init进程处于同一个namespace里面。

再用unshare来启动一个新的shell

Linux Kernel Namespace实现: namespace API介绍 1)前言 2) namespace的生命周期 1)前言 2) namespace的通用操作 3) /proc/$PID/ns/ 的实现 3) nsfs文件系统 4) 小结

可以看到新的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:

Linux Kernel Namespace实现: namespace API介绍 1)前言 2) namespace的生命周期 1)前言 2) namespace的通用操作 3) /proc/$PID/ns/ 的实现 3) nsfs文件系统 4) 小结

再打开另一个terminal, 将新的进程加入到第一个terminal创建的namespace

Linux Kernel Namespace实现: namespace API介绍 1)前言 2) namespace的生命周期 1)前言 2) namespace的通用操作 3) /proc/$PID/ns/ 的实现 3) nsfs文件系统 4) 小结

先通过 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时权限不够, 如下示:

Linux Kernel Namespace实现: namespace API介绍 1)前言 2) namespace的生命周期 1)前言 2) namespace的通用操作 3) /proc/$PID/ns/ 的实现 3) nsfs文件系统 4) 小结

我们在随后分析namespace实现的时候再来详细分析这个bug.

现在这两个进程都在同样的namespace里面了(除了user namespace外), 我们来看看:

Linux Kernel Namespace实现: namespace API介绍 1)前言 2) namespace的生命周期 1)前言 2) namespace的通用操作 3) /proc/$PID/ns/ 的实现 3) nsfs文件系统 4) 小结

可以看到这两个进程在同一个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进行更新. 如下图所示:

Linux Kernel Namespace实现: namespace API介绍 1)前言 2) namespace的生命周期 1)前言 2) namespace的通用操作 3) /proc/$PID/ns/ 的实现 3) nsfs文件系统 4) 小结

由于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的生命周期是很有帮助的.