Linux Kernel Namespace实现: namespace API介绍 (Part 1)

 

1)前言

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

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

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

2) namespace的生命周期

namespace的生存周期如下图所示

 

image001

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

081315_1626_13.png

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

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

081315_1626_14.png

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

再来看看当前shell的namespace:

081315_1626_15.png

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

再用unshare来启动一个新的shell

081315_1626_16.png

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

081315_1626_17.png

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

081315_1626_18.png

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

081315_1626_19.png

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

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

081315_1626_20.png

可以看到这两个进程在同一个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 thought on “Linux Kernel Namespace实现: namespace API介绍 (Part 1)

发表评论

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

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> 

*