理解Namespace

2,092 阅读9分钟

本篇文档主要介绍linux操作系统中的namespace这个概念,namespace是容器技术的两大基本技术之一,理解namespace是理解容器本质的重要基础。

两个小明

上学的时候有一些很尴尬的事情,比如撞衫?要是某一天发现班级里的同学和自己穿了一个一模一样的衣服,那一天的快乐就直接消失了。 image.png 撞衫这事情还是可以接受的,毕竟只要记住以后不穿就行了。还有一种非常尴尬的情况就是: 名字一样。这种情况就可能伴随着你的童年。不过这事情也分为两种情况:

  • 和你在一个班级
  • 和你不在一个班级

对于第二种情况,还能接受,至少在你们这个班级,你是唯一的。但是第一种情况就很尴尬了,在这个班级就不唯一了,同学们就会经常使用一些有意思的方式来区分你俩,比如外号。 简单扯了个淡,是想说明一个情况,在现实的世界里,唯一标识个体重要的依据是名字,但是名字这东西 可能会冲突,如果名字冲突了,那么还怎么区分这两个个体呢?别名可能是一个不错的选择。虽然在某个范围内,是有冲突的,比如在班级内,但是班级可以继续划分为小组,两个个体在不同的小组,那么在这个小组内,冲突得到了解决。

隔离

上述的问题在编程世界里,也是频繁的出现,比如给变量起名字,这个工作是被认为编程中最难的工作之一。变量起名字一般都是有规范的,当大家都按照这个规范来的时候,很容易就出现冲突的情况。不仅仅是给变量命名,给代码文件,模块等等的命名都会出现类似的情况。 那么怎么解决这些问题呢?

C++中的NameSpace

学习过C++的人第一次写hello world的时候可能都会这么写:

#include<iostream>
using namespace std;
int main(){
	cout<<"hello world"<<endl;
    return 0;
}

这段代码里面有一个叫做namespace的关键词,随着继续学习的深入,大家理解了namespace的含义:

  • namespace 是一个独立的代码空间,在不同的namespace中可以定义相同的变量,结构体,类等。
  • 不同的namespace中的成员是互相不可见的。
  • 使用using关键字,可以引入namespace中的所有成员,也可以使用::直接访问某个成员.

接下来是个例子:

#include<iostream>
namespace n1{
  int x=10;
}
namespace n2{
  int x=20;
}
int main(){
 std::cout<<n1::x<<" "<<n2::x<<std::endl;
 return 0;
}

可以看到定义了两个命名空间,n1和n2,在这两个空间内定义了两个同名的变量x,如果没有namespace,这将会编译失败,因为命名上的冲突,有了namesapce之后,两个x就隔离开了。 从上面一个例子可以看出来C++是怎么解决这一类问题的,当然C++的namesapce还有其他很多的用处,不仅仅是解决了命名冲突上的问题。现在把视角切换到操作系统上来,操作系统中,运行着大量的进程(Process),这些进程不仅仅命名上的冲突,更多的是资源上的冲突。

虚拟内存

**虚拟内存的构想,或许是冯诺依曼架构之后,计算机的第二次质的飞跃。**虚拟内存通过在操作系统层面增加了一系列对应的功能,完成了虚拟地址到物理地址的转换,这大大的简化了操作系统上层应用的编程难度。虚拟内存让每一个进程都看到的是一个虚拟,完整的地址空间。这样在进程级别实现了内存上的逻辑隔离。

虚拟机

虚拟机这个概念可能大家并不是很陌生,比如JVM(Java 虚拟机)。这是一个进程级别的虚拟。往往我们发现,进程级别的虚拟是不够的,比如我们可以在一个操作系统里面启动多个Java进程,但是这些进程的隔离性并不是很好,因为他们是受到同一个操作系统管理的,使用着相同的系统调用。 在进程之上,可以有着更大力度的虚拟化,比如操作系统。操作系统本质上也是一个进程,早期在一些规格比较大(指的是配置比较好)的机器上,启动一个服务是比较浪费的,所以采用启动多个虚拟机的方式,每个虚拟机都是一个虚拟的服务器,对外提供服务,来达到资源的最大化利用。 虚拟机似乎是一个很不错隔离方案,连操作系统都是隔离的,那运行在里面的进程当然也是隔离的,也不存在什么竞争问题了。如果有进程隔离需求,直接都包装在虚拟机里面,问题得到解决。但是事实并不是那么美好,虚拟机在提供了非常大粒度的隔离环境的同时,也有问题:

  • 虚拟化成本很高。虚拟机本质上还是一个进程,受到宿主机上的操作系统管理,并且构建出一个完整的操作系统。这使得虚拟机的新建和销毁,都是一个比较重量级的操作。

上述的三种技术,都是在围绕这一个话题来说:如何隔离一些需要隔离的东西。namespace提供了代码层面上的隔离,虚拟内存提供了内存上进程级别的隔离,虚拟机提供的是操作系统上的隔离。这些隔离粒度是逐渐增大的。也各有优缺点。

Linux NameSpace

在上一章节,主要围绕着隔离这个话题展开,介绍了一些常用的隔离方案。对于一个云服务厂商来说,隔离是最基本的功能。在隔离的基础上,要求"弹性"(PS: 关于怎么理解弹性这个词,如果不清楚可以暂时不用管,后面会专门介绍什么是弹性)。貌似上述的方案都是不太能支持的,虚拟机是一种很重的隔离方案,弹性不够。 或许从虚拟内存里面,可以看到一丝端倪。虚拟内存提供了一种解决方案:让进程在一个虚拟的地址空间里,随意的使用着这些内存,操作系统负责把这些内存映射到真正的物理内存上去。搞计算机的同学们都有着一个很可敬的特征:燃烧自己,照亮他人。 那么有没有一种方案,让某个进程也是虚拟的独享系统资源,比如独占整个网络资源,IO设备等,然后在操作系统层面做转换。这样不同的进程看到的系统资源都是独占的,这样实现了更加轻量级,细粒度的进程隔离。Linux namespace完成了对这一需求的支持。 Linux 在2.4.X 版本就开始引入namespace这个特性,前后经历过几个大版本,最后在Linux 3.8 左右,才完成了所有的namespace的支持。概括下来,目前Linux支持的namespace有:

  • Mount(mnt) NameSpace. 挂载点命名空间。这个让每个进程拥有独立的挂载点视图,现在说起来可能有点抽象,后面会有具体的实践解释什么是挂载点以及挂载点隔离。
  • Pid(pid) NameSpace. pid命名空间,这让每个进程看到的pid列表是不一样的。
  • Network(net) Namespace.网络命名空间,在网络资源实现了进程间的隔离,比如网卡,端口,socket 文件等。
  • IPC NameSpace. IPC 命名空间,隔离进程间的IPC,比如singal,msq等。
  • UTS(Unix Time-Sharing NameSpace). 用户隔离hostname或者domain等。
  • User ID NameSpace。用于提供授权和鉴权的隔离。
  • Control Group NameSpace。用于提供cgroup的隔离。
  • Time NameSpace。系统时间上的隔离

上述NameSpace 完成了对一些系统资源的隔离需求,灵活的组合他们,可以看到非常神奇的效果。

简单使用

现在使用一个简单的demo展示一下怎么使用上述namespace实现资源的隔离。

演示环境

本次demo 使用golang 编写代码,运行环境:

Linux version 5.15.0-57-generic (buildd@lcy02-amd64-110) 

代码地址:UTS隔离演示

代码解析

package main

import (
	"log"
	"os"
	"os/exec"
	"syscall"
)

func main() {
	cmd := exec.Command("sh")
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWNS,
	}
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		log.Fatal(err)
	}
}

这段代码启动了一个新的进程,这个新的进程执行了sh调用,并且执行调用的参数设置了syscall.CLONE_NEWNS, 表示使用一个新的UTS NameSpace。 启动这个程序,我的启动方式是:

go build -o main main.go

udo ./main

image.png 这样就通过执行main.go 启动了一个新的shell进程。首先来看一下进程的关系: image.png 可以看到main 函数对应的进程是6801,它有一个子进程sh(6806). 这个可以在启动的shell里面查看当前进程号: image.png 那么按照上述的代码逻辑,这两个进程应该是处于不同的UTS NameSpace中。接下来分别查看main进程的UTS NameSpace和sh进程的UTS NameSpace. 这个信息在/proc/${PID}/ns 下面 它们分别是:main函数进程(pid=6801) image.png sh 进程(pid=6806) image.png 可以看到这两个进程的uts确实是不一样的,并且除了uts namespace,其他的namespace都是一样的。这就实现了这两个进程在uts上的隔离。 那么在uts实现了隔离,具体的表现是什么呢?在上面的介绍中可以了解到,uts主要是用来隔离hostname的,那么来实验一下。 首先在main函数对应的进程下,也就是当前系统默认的uts namespace中,看一下hostname image.png 这个hostname的值就是当前虚拟机的name。那么在sh进程内呢? image.png 可以看到也是这个值,因为初始化的时候会采用默认的值,和父进程保持一样,接下来把sh进程对应的hostname修改一下:

hostname -b test

image.png 可以发现shell进程内的hostname已经成了test,但是main进程呢? image.png main进程对应的hostname是没有变化的,说明这两个进程的hostname确实是隔离开的,和预期的是符合的。

总结

本篇文章介绍了Linux中的namespace的概念,namespace提供了比较细粒度的资源隔离方式,比如演示的uts资源。在namespace中的进程是共享这个namesspace的资源的,如果一个namespace中只有一个进程,那么这个进程就是独占这些资源。 资源有很多种类型,不同的类型提供了不同的namespace。我们在新建进程的时候,可以根据需要种资源上做隔离,这十分的灵活。例子中使用了golang 演示了怎么去隔离uts这种资源。 在下一篇文章中,会把每个NameSpace都实践一下,看看每个NameSpace隔离的具体表现是什么。其实这些东西都是Linux 操作系统中的知识,所以学习容器的第一步就是要先把Linux学会。