Linux Kernel Namespace实现: namespace在proc中的实现 (Part 2)

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进行更新. 如下图所示:

image001

 

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

发表评论

电子邮件地址不会被公开。

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> 

*