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”], [/* 33 vars */]) = 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, [root@xiaohome /]#

从上可以看到,  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”], [/* 27 vars */]) = 0

unshare(CLONE_NEWPID)                   = 0

在这里可以看到 –p参数对应的操作是unshare(CLONE_NEWPID).

2.5) 销毁namespace

Linux kernel没有提供特定的接口来销毁namespace, 销毁的操作是自动进行的. 在后面的分析中我们可以看到, 每一次引用namespace就会增加一次引用计数, 直至引用计数为0时会将namespace自动删除.

那在用户空间中, 我们可以open /proc/$PID/ns下的文件来增加引用计数, 还可以通过mount bind的操作来增加计数, 如下所示:

[root@xiaohome ~]# mount –bind /proc/2342/ns/pid /var/log^C

[root@xiaohome ~]# echo > /tmp/pid-ns

[root@xiaohome ~]# mount –bind /proc/2342/ns/pid /tmp/pid-ns

[root@xiaohome ~]# 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: –

[root@xiaohome ~]# 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);       /* make sure ->d_prune() does nothing */

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

Linux Kernel Namespace实现: namespace API介绍相关推荐

  1. linux kernel中的栈的介绍

    目录 1.linux kernel中的中断irq的栈stack (1).arm32体系的irq的栈 (2).arm64体系的irq的栈 2.linux kernel中的栈stack (1).概念介绍: ...

  2. linux kernel的spin_lock的详细介绍(以arm64为例)

    1.spin_lock的调用流程: static __always_inline void spin_lock(spinlock_t *lock) {raw_spin_lock(&lock-& ...

  3. linux kernel的异常量表介绍(irq,fiq,swi,svc...)

    文章目录 1.linux kernel - arch64的异常向量表-(irq,fiq,svc......) 2.linux kernel - arch的异常向量表-(irq,fiq,swi..... ...

  4. Linux kernel Namespace源码分析

    2019独角兽企业重金招聘Python工程师标准>>> 学习一下linux kernel namespace的代码还是很有必要的,让你对docker容器的namespace隔离有更深 ...

  5. Linux kernel 中模块化的平台驱动代码介绍

    介绍 在linux kernel中通过module_platform_driver来实现模块化平台驱动.大量的设备驱动程序都基于该种方式来实现,使用频次非常的高,在linux kernel 5.4.1 ...

  6. Linux内核scatterlist API介绍 DMA SG搬移

    Linux内核scatterlist API介绍 1. 前言 我们在那些需要和用户空间交互大量数据的子系统(例如MMC[1].Video.Audio等)中,经常看到scatterlist的影子.对我们 ...

  7. linux kernel内存管理之/proc/meminfo下参数介绍

    一.前言 /proc/meminfo是了解Linux系统内存状态的主要接口,里面统计了当前系统各类内存的使用状况,需要注意的是:这是从内核的角度来统计.我们常用的free,vmstat等指令都是通过/ ...

  8. linux内核知识之namespace

    namespace概念: namespace是linux自带的功能用来隔离内核资源的机制,如进程pid,主机名与域名,网络设备端口等.什么是容器?容器其实就是一个虚拟化的独立的沙箱环境,和宿主机或者其 ...

  9. Linux kernel中断子系统之(五):驱动申请中断API【转】

    转自:http://www.wowotech.net/linux_kenrel/request_threaded_irq.html 一.前言 本文主要的议题是作为一个普通的驱动工程师,在撰写自己负责的 ...

最新文章

  1. verycd重整——linux教程
  2. 10分钟让你快速掌握Excel的16项重要技巧
  3. NYOJ 10 skiing
  4. 【ArcGIS|空间分析|网络分析】8 查找能够为需求点对提供服务的最佳路径
  5. 图像处理-RGB24转YUV420遇到的坑以及执行效率对比
  6. 新能力 | 云开发CMS内容管理系统,5分钟搞定小程序管理后台
  7. Android9 点击按键KeyEvent.KEYCODE_CAMERA没反应
  8. Kafka eagel 网页能打开,但是登录不上
  9. 开源社已加入群聊,思否 AIGC Hackathon 扩列
  10. html显示隐藏表格内外边框
  11. 计算机蓝屏代码0x000000ED,电脑蓝屏代码0x000000ed解决步骤
  12. 20220630学习打卡
  13. 消失的两个数字(1~N缺两个数)
  14. Struts2面试常见问题
  15. 五大定律助你公司走向成功-民兴商学院
  16. 日期转换--接收日期与数据库存储不兼容问题时间段查询
  17. LightOJ 1079 Just another Robbery (概率dp+背包)
  18. R语言学习笔记(十):重抽样与自助法
  19. ESP8266 WiFi模块介绍
  20. Pytorch 构建简单Neural Networks

热门文章

  1. 英学者研究60亿次通话记录发现:好友再多也没用,最好朋友就4个
  2. What is 软件工程
  3. WINDOWS SERVER 2003 AD中的5种操作主机
  4. ECShop 模板库项目功能详解
  5. onmouseover和onmouseout在GridView中应用
  6. 网络工程师学习资料:路由器配置案例分析
  7. 553 mail from must equal authorized user解决方法
  8. 智点财务软件记账凭证的录入
  9. [20150409]只读表空间与延迟块清除.txt
  10. 基础知识《二》java的基本类型