Skip to content

Latest commit

 

History

History
271 lines (226 loc) · 20.4 KB

namespace的前生今世.md

File metadata and controls

271 lines (226 loc) · 20.4 KB

Abstract

云越来越火了,大公司小公司逐渐逐渐的放弃了传统机房转而建设私有云或者将自己的业务都放到了公有云上,在这股潮流里,k8sdocker这两个词愈发频繁的出现在大众面前。 然而本篇文章不想谈论k8s,因为在业界有一群布道师在积极推广,也不想谈论Docker这个被众多巨头分食殆尽的优秀作品,而是想聊一聊存在于云背后的技术Namespace

Container

对于Service Oriented Coder来说,其中的99%对于系统环境的要求仅仅为能够支持自身服务稳定安全地运行,除非是一些写了大量system()的垃圾代码。然而对于底层支持的运维来说,即使是提供了一个安全可靠的系统出来,然而将多个服务运行在一起的话,一个服务被入侵可能影响到所有服务,一个服务oom就导致所有服务全都异常,这无疑就是灾难与折磨,为了解决这些问题而出现了隔离的需求,也就是虚拟化技术的诞生,VmwareVirtualBox都是一些很好的虚拟化工具,但是同样的也带来了极大的开销。一个非常典型的问题就是一个Hello World想要运行起来真的要为其提供一整套系统环境吗?

在这样的情况下,一个迫切的需求就摆到了世人的面前:有没有能够提供出尽可能多的系统环境却又不产生重新创建系统环境的开销,并且能够让其上运行的服务互不干扰的技术?在当时的时代答案就是容器(Container)。而正是因为对于Container的大力支持且提出了多种新的概念,Docker于08年刚登上世界舞台就在容器领域叱咤风云。

其实不管是虚拟化还是容器其本质需求都是Isolation

但是回首过去,Container是怎样一步一步发展起来的?这个问题其实与这篇文章无关,这完全可以新开一篇文章了没什么必要,因此只提一下其中的FreeBSD JailsLXC。前者通过一系列手段诸如chrootrctlvnetsysctl来创造出一个隔离环境出来,而这个环境就是Container而在那时候被称为Jails,而后者则基本依靠于NamespaceCgroup就完成了对于前者功能的覆盖。

Namespace是实现Container的一种技术手段,但并不代表所有的Container都是依靠Namespace实现的。

What is Namespace?

一个新的功能的诞生可能来源于需求,也可能来源于灵感,而Namespace无疑就是后者。

依照Container的功能来看,Namespace无疑是能够做到各种资源的隔离,那它是什么?怎么实现的?这个其实大部分人都不会去考虑,然而对于安全来说还是得细细考究一下,不管是为了更安全还是为了能够逃逸,这都是绕不开的一步。 依旧是来阐述历史,先理解一切皆文件这个概念。 在Unix看来,不管是文本还是程序还是硬件他们都是资源,都可以打开关闭读取写入,那么为什么不能把他们都抽象成一个拥有统一操作接口的抽象对象呢?这就是文件的由来。但是光有文件并不可行,因为他们只是磁盘上的地址块,对于人来说无法做到直观的交互。在最初的版本的Unix中只有三种类型的文件:

  1. 普通文件:各种文本,程序,链接库等。
  2. 目录:通过匹配Inode提供文件名和文件本身的映射,从而形成树型结构,而最上级的目录文件被称为/(根目录)

目录文件夹是两回事

  1. 特殊文件:诸如一些I/O设备

Unix采用了一种树型结构的分层存储方法管理这三种文件,上述的三种文件在逻辑上被组成了一个以/为根并向下呈现分支状态的结构,而这种针对文件的命名以及分层存储的方法则叫做文件系统

这儿我实际挺晕的,因为非常多的资料都在这儿就已经提到了Namespace,但是我翻完了The UNIX TimeSharing System (1974)都没有在初期的设计中找到关于Namespace的说法,只能去翻阅多个资料,他们是这么阐述的:

  • A Comparison of three Distributed File System Architectures:Vnode, Sprite, and Plan 9 (1994)
File system objects are named in a hierarchical name space that is distributed among multiple servers. 
The name space for the UNIX file system is a hierarchy of directories and leaf nodes.
  • Linux Kernel Development Second Edition (2005)
In Unix, filesystems are mounted at a specific mount point in a global hierarchy known as a namespace
  • In UNIX Everything is a File (2009)
This global namespace is often viewed as a hierarchy of files and directories.

三篇论文的含义都是默认存在Name Space这个概念且具有相同的含义,即name space = hierarchy,那么以最早的论文为参考的话,显然这个时候Unix中还是没有Namespace的概念,此点实际也契合wikiLinux Namespace最早来源于Plan 9的说法,按照后续的资料来看也确实如此。

51926252.png

Plan 9Bell Labs在二十世纪八十年代中期开始研究的分布式操作系统,是Everything is a File的继承者和超越者, Plan 9通过9P来解决本地和远程文件的差异化对文件的概念得到了进一步扩展,不再是传统意义上的磁盘存储单元,而是指的一切计算机资源,而其哲学理念则成了Everything looks like a file(Everything is a filesystem)

08b37605-c17b-440f-84c9-c41cf59c7e1b.png

the foundations of the system are built on two ideas: a per-process name space and a simple message-oriented file system protocol. -- `Plan 9, A Distributed System (1991)`


Finally, wherever possible the system is built around two simple ideas: every resource in the system, either local or remote, is represented by a hierarchical file system; and a user or process assembles a private view of the system by constructing a file name space that connects these resources. [Needham]

Plan 9上将一切计算机资源看成是文件,因为这些资源主观实现上仿照了Unix文件系统便从此被称呼为文件系统(原话是这样说的: Simply stated, all resources are implemented to look like file systems and, henceforth, we shall call them file systems),他们可以是传统意义上的磁盘上的永久存储,也可以是物理设备或者是抽象概念比如进程,这也代表着这些文件系统得由不同的地方实现,比如内核驱动用户态程序甚至是远程服务器Plan 9将除了program memory以外进程能够访问的所有资源都组织到一个name space当中且能够统一访问,这个Name Space是一个单一根目录的文件名分层结构,从这儿后世常用的linux namespace概念才被正式提出,当然也不是凭空创造的,而是来源于一篇更早的文章:Names in Distributed Systems (1989)

name space
the collection of valid names recognised by a name service a precise specification is required, giving the structure of names
e.g. ISBN-10: 1-234567-89-1 namespace identifier: namespace-specific string
 /a/b/c/d filing system, variable length, hierarchical
 puccini.cl.cam.ac.uk DNS machine name, see later for DNS
 e.g. Mach OS 128-bit port name (system-wide UID) 

这个概念被Rob PikeKen Thompson用到了Plan 9上用来统一Unix上所有的绑定思想,在The Use of Name Spaces in Plan 9中有详细的介绍,直接看一下实现:

当用户启动终端或者连接到cpu服务器的时候,将为其进程创建一个新的进程组,然后会准备一个初始namespace,其中至少提供了一个root(/),一些进程需要的二进制文件(/bin/*)和一些本地设备(/dev/*),然后进程组中的进程可以通过`mount`和`bind`这两个系统调用自己添加或者或者编排资源,系统提供了一个系统调用叫做rfork,其中一个参数决定了父子进程的name space是共享还是复制,前者相互影响,后者相互独立。

其中bindmount同时还实现了plan 9的另一个特性 -- union directories用来取代了search paths,直观地看一下Plan 9中的1号进程的Name Space空间视图:

68867641-0839-49da-9639-e8165730a199.png

从这个图上就应该能够感受出在Plan 9Name Space的含义,这是一个资源集的概念,而空间视图表现上则是一个分层树型目录树,因此这也是为什么后来的非常多的文章都直接把Unixhierarchy看作是一个Global Namespace

受到Plan 9Name Space的启发,Al Viro提出了以下两点并着手改进Linux VFS,从一切皆文件逐渐转向了一切文件都是挂载点

  1. A bind mount allows any file or directory to be accessible from any other location.
  2. Filesystem namespaces are completely separate filesystem trees associated with different processes.

上述两点是分时间线完成的,前者在2.4.11-pre4中加入,而后者则一直等到2.4.19的时候才完成,鉴于前者主要是解决的不同类型file system的问题实际上和本篇关系不是很大就只看后者的引入。 在Unix上每个进程看到的都是相同的目录结构,而到了Plan 9中因为每个进程都有自己的name space导致不同进程看到的目录结构可能是不同的,而这样的做的目的实际上是因为MinimalismDistributed,但是在表现上来说却又实现了Isolated,因此这个能力就被移植到了linux上实现,这也是为什么Mount Namespace直接就被称为CLONE_NEWNS

  1. 继承自Plan 9,实现的就是Plan 9针对file system空间视图的隔离能力
  2. 这是第一个在linux上被实现的namespace也没想过会再加入其余类型的namespace

而关于这次能力的引入,社区还是挺兴奋的:[PATCH][CFT] per-process namespaces for Linux

Linux Namespace

终于到了近现代不用再考古了,项目的初期其实大家的想法也并不是统一的各有各的理解,这就导致要反复对比揣摩文献着实累人。

最初我在想为什么Linux不直接沿用Plan 9的方式去实现视图空间的隔离,后来想想也是因为在/proc/*/ns下面一看进程的资源一目了然,这还谈什么IsolatedSecure,那自然还是把相关信息放在内核里的好,在2.4.19Linux的第一个Namespace是这么实现的:

struct namespace {
    atomic_t        count; //此namespace引用数量,防止被释放
    struct vfsmount *    root;  //root mount
    struct list_head    list;   // lists of mount points
    struct rw_semaphore    sem;
};

然后进程结构体task_struct中增加了一个namespace指针:

/* namespace */
    struct namespace *namespace;

那么主要关注的点就是第一个namespace的创建和新的namespace的创建,namespace随着进程产生,那就得直接从kernel初始化那儿开始:

start_kernel -> vfs_caches_init -> mnt_init -> init_mount_tree


init_mount_tree ()
{
    ...
mnt = do_kern_mount("rootfs", 0, "rootfs", NULL);    
namespace = kmalloc(sizeof(*namespace), GFP_KERNEL);
namespace->root = mnt;
init_task.namespace = namespace;
    ...
}

等再有进程被通过do_fork创建的时候,会通过copy_namespace()校验flag,如果有CLONE_NEWNS的话则会进入到新建namespace的流程,否则还是单纯的复制。不过细看copy_namespace的时候就会发现,其实这个函数本身是没有提供新的分层结构的,而是复制父进程的空间视图:

struct namespace *namespace = tsk->namespace;
struct namespace *new_ns;
new_ns = kmalloc(sizeof(struct namespace *), GFP_KERNEL);
new_ns->root = copy_tree(namespace->root, namespace->root->mnt_root);
tsk->namespace = new_ns;

linux上虽然实现的方式变了但是本质上没有变,依然是由mount集合而成的空间视图,那意思就是说只要没对mount做修改的话new namespaceold namespace中的空间视图是一致的,而修改mount则是应用层应该关心的事情,这个从内核源码里的kmalloc(sizeof(struct namespace *), GFP_KERNEL);也应该来看出来是开辟了新的内存空间,只是这个新的内存空间的内容是复制来的而已。 这儿要注意的隔离的是mount,并不是在你不修改任何mount的情况下直接修改或者创建文件而不对其余namespace产生影响,这也是为什么称呼CLONE_NEWNSMount Namespace而不是File Namespace,而从创建新的namespace到能够真正隔离文件则涉及到rootfs的隔离,Docker的实现方式是pivot_root,粗略看了一下Docker的源码后直接挂个它的Demo

#define STACK_SIZE (1024*1024) /* Stack size for cloned child */
#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \
                               } while (0)
int parent_uid;
int parent_gid;
char *rootfs = "/home/lang/Desktop/newrootfs";
 static int
pivot_root(const char *new_root, const char *put_old)
{
    return syscall(SYS_pivot_root, new_root, put_old);
}
static char child_stack[STACK_SIZE];
int child_main(){
    printf("进入子进程:%d\n",getpid());
    if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) == -1)  //标记
        errExit("mount-MS_PRIVATE");  
    if (mount(rootfs, rootfs, NULL, MS_BIND | MS_REC | MS_PRIVATE, NULL) == -1)
        errExit("mount-MS_BIND");
    if (chdir(rootfs) == -1)
        errExit("chdir1");
    if (pivot_root(".", ".") == -1)
        errExit("pivot_root");
    if (umount2(".", MNT_DETACH) == -1)
        perror("umount2");
    if (chdir("/") == -1)
        perror("chdir2");
    if (mount("proc", "/proc", "proc", 0, NULL) == -1)
        errExit("mount-proc");
    char *arg[] = {"/bin/bash", NULL};
    execv("/bin/bash", arg);
    return 1;
}
int main(void){
    printf("创建子进程\n");
    parent_uid = getuid();
    parent_gid = getgid();
    int child_pid = clone(child_main, child_stack + STACK_SIZE, CLONE_NEWNS | CLONE_NEWPID | SIGCHLD, NULL);
    waitpid(child_pid, NULL, 0);
    printf("退出子进程\n");
    return 0;
}

其中写上了标记的那行参照Mount namespaces, mount propagation, and unbindable mounts,如果不将new namespace中的/重新挂载成PRIVATE的话,就会影响到相同peer group中的namespace,这是2.6.15中引入的shared subtree特性,用来解决新资源挂载的问题。

Other Namespace

随着技术的发展,慢慢的又有新的类型的namespace被加入到内核中来,那再在task_struct中用namespace这个指针就有点不合适了,因此在2.6.19里面更换了结构体为nsproxy:

v4.20
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 cgroup_namespace *cgroup_ns;
};

把新加入的namespace又聚合了一层,这样不管是复制还是修改的都是nsproxy使得逻辑变得更加合理,同样增加了新的CLONE_NEWXX来决定新创建的namespace类型,其中还需要单独拿出来说一说的就是PID Namespace,从这儿开始才是正式构成了容器的基础。 PID NamespaceOpenVZ teamIBM的人在2.6.24搞出来的一个新的特性,依旧是一个分层结构但是却和先前的namespace有所不同,即当前PID namespacetasks可以看到新的namespace中的tasks反之不行,有一种半透镜的感觉,那这就意味着一个task可以具备多个PID。 为了实现这个需求,内核里面的PID结构需要有相应的改变才行,在2.6.24以前的实现:

struct pid
{
    atomic_t count;
    /* Try to keep pid_chain in the same cacheline as nr for find_pid */
    int nr;
    struct hlist_node pid_chain;
    /* lists of tasks that use this pid */
    struct hlist_head tasks[PIDTYPE_MAX];
    struct rcu_head rcu;
};

而到了以后的实现:

struct upid {
    /* Try to keep pid_chain in the same cacheline as nr for find_pid */
    int nr;
    struct pid_namespace *ns;
    struct hlist_node pid_chain;
};
struct pid
{
    atomic_t count;
    /* lists of tasks that use this pid */
    struct hlist_head tasks[PIDTYPE_MAX];
    struct rcu_head rcu;
    int level;
    struct upid numbers[1];
};

在结构中多了level层级号和ns指针,而原本的PID号移到了upid中,即不同的namespace中不同,但是实际上的修改却不止这么点,毕竟进程算是操作系统的核心功能,只是这么一点变动就导致需要更改的地方非常多,尤其多了namespace的概念后如果不加注意就非常容易造成逃逸或者是BUG,最典型的莫过于Docker中针对孤儿进程的处理逻辑与V4.4的冲突导致自身失去了收割能力,换句话说就是在PID Namespace上应该还有相当一部分的逻辑漏洞等待着被发掘。

Conclusion

如果说这个时代已经借助Namespace等种种底层隔离技术正式进入了云时代的话,那随着5G的到来,或许分布式时代也将来临甚至说是去中心化也会在大世之中分一杯羹,也许就在不久的将来PC将慢慢的从硬件的集合转向虚拟硬件或者说云硬件,诸如intelNVIDIA这样的厂商也从挤牙膏``秀刀法慢慢地转变成贩卖计算资源这也说不准,而隔离的概念经过编排能力的提高也逐渐智能化?逐渐逐渐的挡在落地之前最大的阻碍大概就是技术菜了吧 : )

说一句心里话5ZWG5Zy65aaC5oiY5Zy677yM5oiY5LqJ6IO95aSf5o6o5Yqo5oqA5pyv55qE6L+b5q2l77yM5L2G5piv5Zyo6I+c5biC5Zy655u45LqS5Y+r6aqC552A5oqi55Sf5oSP5Y+v5LiN5Lya

参考资料