Linux namespace概述(一)

-
-
2024-04-06

 

目录


本系列文章不介绍关于Linux namespace的全部,只介绍其中重要的一部分,有了基础之后,更多的内容可去参考man手册,man手册的解释非常详细。

Linux namespace概述

# namespace概念和细节相关man文档
man namespaces
man uts_namespaces
man network_namespaces
man ipc_namespaces
man pid_namespaces
man mount_namespaces
man user_namespaces
man time_namespaces
man cgroup_namespaces

# namespace管理工具
man unshare     # 创建namespace
man nscreate    # 创建namespace,老版本的内核没有该工具
man nsenter     # 切换namespace
man lsns        # 查看当前已创建的namespace

Linux namespace介绍

操作系统通过虚拟内存技术,使得每个用户进程都认为自己拥有所有的物理内存,这是操作系统对内存的虚拟化。操作系统通过分时调度系统,每个进程都能被【公平地】调度执行,即每个进程都能获取到CPU,使得每个进程都认为自己在进程活动期间拥有所有的CPU时间,这是操作系统对CPU的虚拟化。

从这两种虚拟化方式可推知,当使用某种虚拟化技术去管理进程时,进程会认为自己拥有某种物理资源的全部。

虚拟内存和分时系统均是对物理资源进行虚拟化,其实操作系统中还有很多非物理资源,比如用户权限系统资源、网络协议栈资源、文件系统挂载路径资源等。通过Linux的namespace功能,可以对这些非物理全局资源进行虚拟化。

Linux namespace是在当前运行的系统环境中创建(隔离)另一个进程的运行环境出来,并在此运行环境中将一些必要的系统全局资源进行【虚拟化】。进程可以运行在指定的namespace中,因此,namespace中的每个进程都认为自己拥有所有这些虚拟化的全局资源。

目前,Linux已经支持8种全局资源的虚拟化(每种资源都是随着Linux内核版本的迭代而逐渐加入的,因此有些内核版本可能不具备某种namespace):

  • cgroup namespace:该namespace可单独管理自己的cgroup,cgroup可用于限制进程的资源使用量(比如CPU、内存、网络带宽)
  • ipc namespace:该namespace有自己的IPC,比如共享内存、信号量等
  • network namespace:该namespace有自己的网络资源,包括网络协议栈、网络设备、路由表、防火墙、端口等
  • mount namespace:该namespace有自己的挂载信息,即拥有独立的目录层次
  • pid namespace:该namespace有自己的进程号,使得namespace中的进程PID单独编号,比如可以PID=1
  • time namespace:该namespace有自己的启动时间点信息和单调时间,比如可设置某个namespace的开机时间点为1年前启动,再比如不同的namespace创建后可能流逝的时间不一样
  • user namespace:该namespace有自己的用户权限管理机制(比如独立的UID/GID),使得namespace更安全
  • uts namespace:该namepsace有自己的主机信息,包括主机名(hostname)、NIS domain name

用户可以同时创建具有多种资源类型的namespace,比如创建一个同时具有uts、pid和user的namespace。

理解Linux namespace

用户可以创建指定类型的namespace并将程序放入该namespace中运行,这表示从当前的系统运行环境中隔离一个进程的运行环境,在此namespace中运行的进程将认为自己享有该namespace中的独立资源。

实际上,即使用户没有手动创建Linux namespace,Linux系统开机后也会创建一个默认的namespace,称为root namespace,所有进程默认都运行在root namespace中,每个进程都认为自己拥有该namespace中的所有系统全局资源。

当创建额外的命名空间后,进程将只能看到该命名空间下的资源。当然,由于存在多种类型的命名空间,所以⼀个进程不单单只属于某⼀个命名空间,⽽属于每个类型的⼀个命名空间,该进程能看到的也是它所属于的各类命名空间中的资源。

回顾一下Linux的开机启动流程,内核加载成功后将初始化系统运行环境,这个运行环境就是root namespace环境,系统运行环境初始化完成后,便可以认为操作系统已经开始工作了。

每一个namespace都基于当前内核,无论是默认的root namespace还是用户创建的每一个namespace,都基于当前内核工作。所以可以认为namespace是内核加载后启动的一个特殊系统环境,用户进程可以在此环境中独立享用资源。更严格地说,root namespace直接基于内核,而用户创建的namespace运行环境基于当前所在的namespace。之所以用户创建的namespace不直接基于内核环境,是因为每一个namespace可能都会修改某些运行时内核参数。

比如,用户创建的uts namespace1中修改了主机名为ns1,然后在namespace中创建uts namespace2时,namespace2默认将共享namespace1的其他资源并拷贝namespace1的主机名资源,因此namespace2的主机名初始时也是ns1。当然,namespace2是隔离的,可以修改其主机名为ns2,这不会影响其他namespace,修改后,将只有namespace2中的进程能看到其主机名为ns2。

可以通过如下方式查看某个进程运行在哪一个namespace中,即该进程享有的独立资源来自于哪一个namespace。

# ls -l /proc/<PID>/ns
$ ls -l /proc/$$/ns | awk '{print $1,$(NF-2),$(NF-1),$NF}'
lrwxrwxrwx  cgroup            ->  cgroup:[4026531835]
lrwxrwxrwx  ipc               ->  ipc:[4026531839]
lrwxrwxrwx  mnt               ->  mnt:[4026531840]
lrwxrwxrwx  net               ->  net:[4026531992]
lrwxrwxrwx  pid               ->  pid:[4026531836]
lrwxrwxrwx  pid_for_children  ->  pid:[4026531836]
lrwxrwxrwx  user              ->  user:[4026531837]
lrwxrwxrwx  uts               ->  uts:[4026531838]

$ sudo ls -l /proc/1/ns | awk '{print $1,$(NF-2),$(NF-1),$NF}'
lrwxrwxrwx  cgroup            ->  cgroup:[4026531835]
lrwxrwxrwx  ipc               ->  ipc:[4026531839]
lrwxrwxrwx  mnt               ->  mnt:[4026531840]
lrwxrwxrwx  net               ->  net:[4026531992]
lrwxrwxrwx  pid               ->  pid:[4026531836]
lrwxrwxrwx  pid_for_children  ->  pid:[4026531836]
lrwxrwxrwx  user              ->  user:[4026531837]
lrwxrwxrwx  uts               ->  uts:[4026531838]

这些文件表示当前进程打开的namespace资源,每一个文件都是一个软链接,所指向的文件是一串格式特殊的名称。冒号后面中括号内的数值表示该namespace的inode。如果不同进程的namespace inode相同,说明这些进程属于同一个namespace。

从结果上来看,每个进程都运行在多个namespace中,且pid=1和pid=$$(当前Shell进程)两个进程的namespace完全一样,说明它们运行在相同的环境下(root namespace)。

 

理解uts namespace

uts(UNIX Time-Sharing System) namespace可隔离hostname和NIS Domain name资源,使得一个宿主机可拥有多个主机名或Domain Name。换句话说,可让不同namespace中的进程看到不同的主机名(换句话说,至少在主机名这方面,可以让进程认为自己是运行在独立的操作系统中)。

例如,使用unshare命令(较新版本Linux内核还支持nscreate命令)创建一个新的uts namespace:

BASH

# -u或--uts表示创建一个uts namespace
# 这个namespace中运行/bin/bash程序
$ hostname
longshuai-vm      # 当前root namespace的主机名为longshuai-vm
$ sudo unshare -u /bin/bash
root@longshuai-vm:/home/longshuai#   # 进入了新的namespace中的shell
                                     # 其主机名初始时也是longshuai-vm,
                                     # 其拷贝自上级namespace资源

上面指定运行的是/bin/bash程序,这会进入交互式模式,当执行exit时,bash退出,回到当前的namespace中。也可以指定在namespace中运行其他程序,例如unshare -u sleep 3表示在uts namespace中睡眠3秒后退出并回到当前namespace。

因为是uts namespace,所以可在此namespace中修改主机名:

BASH

# 修改该namespace的主机名为ns1
# 修改后会立即生效,但不会显示在当前Shell提示符下
# 需重新加载Shell环境
root@longshuai-vm:/home/longshuai# hostname ns1
root@longshuai-vm:/home/longshuai# hostname
ns1
root@longshuai-vm:/home/longshuai# exec $SHELL
root@ns1:/home/longshuai#

namespace中修改的主机名不会直接修改主机名配置文件(如/etc/hostname),而是修改内核属性/proc/sys/kernel/hostname

BASH

root@ns1:/home/longshuai# cat /proc/sys/kernel/hostname
ns1
root@ns1:/home/longshuai# cat /etc/hostname 
longshuai-vm

 

创建了新的namespace并在其中运行/bin/bash进程后,再去关注一下进程关系:

BASH

# ns1中的bash进程PID
root@ns1:/home/longshuai# echo $$
14279

# bash进程(PID=14279)和grep进程运行在ns1 namespace中,
# 其父进程sudo(PID=14278)运行在ns1的上级namespace即root namespace中
root@ns1:/home/longshuai# pstree -p | grep $$
    |-sshd(10848)---bash(10850)---sudo(14278)---bash(14279)-+-grep(14506)

# 运行在ns1中当前bash进程(PID=14279)的namespace
root@ns1:/home/longshuai# ls -l /proc/14279/ns
lrwxrwxrwx ... cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx ... ipc -> 'ipc:[4026531839]'
lrwxrwxrwx ... mnt -> 'mnt:[4026531840]'
lrwxrwxrwx ... net -> 'net:[4026531992]'
lrwxrwxrwx ... pid -> 'pid:[4026531836]'
lrwxrwxrwx ... pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx ... user -> 'user:[4026531837]'
lrwxrwxrwx ... uts -> 'uts:[4026532588]'  # 注意这一行,和sudo进程的uts inode不同

# 父进程sudo(PID=14278)不在ns1中,它的namespace信息
root@ns1:/home/longshuai# ls -l /proc/14278/ns
lrwxrwxrwx ... cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx ... ipc -> 'ipc:[4026531839]'
lrwxrwxrwx ... mnt -> 'mnt:[4026531840]'
lrwxrwxrwx ... net -> 'net:[4026531992]'
lrwxrwxrwx ... pid -> 'pid:[4026531836]'
lrwxrwxrwx ... pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx ... user -> 'user:[4026531837]'
lrwxrwxrwx ... uts -> 'uts:[4026531838]'   # 注意这一行,和PID=1的uts inode相同

回到创建uts namespace时敲下的unshare命令:

BASH

sudo unshare -u /bin/bash

从进程关系...---sudo(14278)---bash(14279)可知两个进程PID是连续的,说明unshare程序对应的进程被/bin/bash程序通过execve()替换了。

详细的过程如下:sudo进程运行在当前namespace中,它将fork一个新进程来运行unshare程序,unshare程序加载完成后,将创建一个新的uts namespace,unshare进程自身将加入到这个uts namespace中,unshare进程内部再exec加载/bin/bash,于是unshare进程被替换为/bin/bash进程,/bin/bash进程也将运行在uts namespace中

当namespace中的/bin/bash进程退出,该namespace中将没有任何进程,该namespace将自动销毁。注意,在默认情况下,namespace中必须要有至少一个进程,否则将被自动被销毁。但也有一些手段可以让namespace持久化,即使已经没有任何进程在其中运行。

如果在ns1中再创建一个namespace ns2,这个ns2初始时将共享ns1的其他资源并拷贝ns1的主机名资源,其初始主机名也为ns1。

BASH

$ sudo unshare -u /bin/bash    # 在root namespace环境下创建一个namespace
root@longshuai-vm:/home/longshuai# hostname ns1 # 修改主机名为ns1
root@longshuai-vm:/home/longshuai# hostname
ns1

# 在ns1中创建一个namespace
############ 注意没有sudo
root@longshuai-vm:/home/longshuai# unshare -u /bin/bash 
root@ns1:/home/longshuai# hostname    # 初始主机名拷贝自上级namespace的主机名ns1
ns1
root@ns1:/home/longshuai# hostname ns2
root@ns1:/home/longshuai# hostname  # 修改主机名为ns2
ns2
root@ns1:/home/longshuai# exit
exit

root@longshuai-vm:/home/longshuai# hostname  # ns2修改主机名不影响ns1
ns1
root@longshuai-vm:/home/longshuai# exit
exit

[~]->$ hostname      # ns1修改主机名不影响root namespace
longshuai-vm

注意,即使root namespace当前用户为longshuai,但因为使用了sudo创建ns1,进入ns1后其用户名为root,所以在ns1中执行unshare命令创建新的namespace不需要再使用sudo。

BASH

$ echo $USER      # 当前root namespace的用户为longshuai
longshuai

$ sudo unshare -u /bin/bash
root@longshuai-vm:/home/longshuai# echo $USER  # ns中的用户名变为root
root
root@longshuai-vm:/home/longshuai# id;echo $HOME;echo ~
uid=0(root) gid=0(root) groups=0(root)
/root
/root

 

理解mount namespace

用户通常使用mount命令来挂载普通文件系统,但实际上mount能挂载的东西非常多,甚至连现在功能完善的Linux系统,其内核的正常运行也都依赖于挂载功能,比如挂载根文件系统/。其实所有的挂载功能和挂载信息都由内核负责提供和维护,mount命令只是发起了mount()系统调用去请求内核。

mount namespace可隔离出一个具有独立挂载点信息的运行环境,内核知道如何去维护每个namespace的挂载点列表。即每个namespace之间的挂载点列表是独立的,各自挂载互不影响

内核将每个进程的挂载点信息保存在/proc/<pid>/{mountinfo,mounts,mountstats}三个文件中:

BASH

$ ls -1 /proc/$$/mount*
/proc/26276/mountinfo
/proc/26276/mounts
/proc/26276/mountstats

具有独立的挂载点信息,意味着每个mnt namespace可具有独立的目录层次,这在容器中起了很大作用:容器可以挂载只属于自己的文件系统。

当创建mount namespace时,内核将拷贝一份当前namespace的挂载点信息列表到新的mnt namespace中,此后两个mnt namespace就没有了任何关系(不是真的毫无关系,参考后文shared subtrees)。

创建mount namespace的方式是使用unshare命令的--mount, -m选项:

BASH

# 创建mount+uts namespace
# -m或--mount表示创建mount namespace
# 可同时创建具有多种namespace类型的namespace
unshare --mount --uts <program>

 

下面做一个简单的试验,在root namespace中挂载1.iso文件到/mnt/iso1目录,在新建的mount+uts namespace中挂载2.iso到/mnt/iso2目录:

BASH

[~]->$ cd
[~]->$ mkdir iso
[~]->$ cd iso
[iso]->$ mkdir -p iso1/dir1
[iso]->$ mkdir -p iso2/dir2  
[iso]->$ mkisofs -o 1.iso iso1  # 将iso1目录制作成镜像文件1.iso
[iso]->$ mkisofs -o 2.iso iso2  # 将iso2目录制作成镜像文件2.iso
[iso]->$ ls
1.iso  2.iso  iso1  iso2
[iso]->$ sudo mkdir /mnt/{iso1,iso2}
[iso]->$ ls -l /proc/$$/ns/mnt
lrwxrwxrwx 1 ... /proc/26276/ns/mnt -> 'mnt:[4026531840]'

# 在root namespace中挂载1.iso到/mnt/iso1目录
[iso]->$ sudo mount 1.iso /mnt/iso1  
mount: /mnt/iso: WARNING: device write-protected, mounted read-only.
[iso]->$ mount | grep iso1
/home/longshuai/iso/1.iso on /mnt/iso1 type iso9660

# 创建mount+uts namespace
[iso]->$ sudo unshare -m -u /bin/bash
# 虽然这个namespace是mount+uts的namespace
# 但注意mnt namespace和uts namespace的inode并不一样
root@longshuai-vm:/home/longshuai/iso# ls -l /proc/$$/ns
lrwxrwxrwx ... cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx ... ipc -> 'ipc:[4026531839]'
lrwxrwxrwx ... mnt -> 'mnt:[4026532588]'
lrwxrwxrwx ... net -> 'net:[4026531992]'
lrwxrwxrwx ... pid -> 'pid:[4026531836]'
lrwxrwxrwx ... pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx ... user -> 'user:[4026531837]'
lrwxrwxrwx ... uts -> 'uts:[4026532589]'

# 修改主机名为ns1
root@longshuai-vm:/home/longshuai/iso# hostname ns1
root@longshuai-vm:/home/longshuai/iso# exec $SHELL

# 在namespace中,可以看到root namespace中的挂载信息
root@ns1:/home/longshuai/iso# mount | grep 'iso1' 
/home/longshuai/iso/1.iso1 on /mnt/iso1 type iso9660

# namespace中挂载2.iso2
root@ns1:/home/longshuai/iso# mount 2.iso2 /mnt/iso2/
mount: /mnt/iso2: WARNING: device write-protected, mounted read-only.
root@ns1:/home/longshuai/iso# mount | grep 'iso[12]'
/home/longshuai/iso/1.iso1 on /mnt/iso1 type iso9660
/home/longshuai/iso/2.iso2 on /mnt/iso2 type iso9660

# 在namespace中卸载iso1
root@ns1:/home/longshuai/iso# umount /mnt/iso1/
root@ns1:/home/longshuai/iso# mount | grep 'iso[12]' 
/home/longshuai/iso/2.iso2 on /mnt/iso2 type iso9660
root@ns1:/home/longshuai/iso# ls /mnt/iso1/
root@ns1:/home/longshuai/iso# ls /mnt/iso2
dir2

#### 打开另一个Shell终端窗口
# iso1挂载仍然存在,且没有iso2的挂载信息
[iso]->$ mount | grep iso
/home/longshuai/iso/1.iso1 on /mnt/iso1 type iso9660
[iso]->$ ls /mnt/iso2
[iso]->$ ls /mnt/iso1
dir1

以上是mount namespace的基本内容,只有一个关键点:创建mnt namespace时会拷贝当前namespace的挂载点信息,之后两个namespace就没有关系了。

mnt namespace: shared subtrees

Linux的每个挂载点都具有一个决定该挂载点是否共享子挂载点的属性,称为shared subtrees。该属性以决定某挂载点之下新增或移除子挂载点时,是否同步影响它【副本】挂载点。如不了解该属性的作用,参考mount bind和shared subtrees详解

简单说说shared subtrees特性,该特性有什么用呢?以mnt namespace为例来简单介绍一下shared subtrees特性。

假设基于root namespace创建了一个mnt namespace(ns1),那么ns1将具有当前root namespace的挂载点信息拷贝。如果此时新插入了一块磁盘并对其分区格式化,然后在root namespace中对其进行挂载,默认情况下,在ns1中将看不到新挂载的文件系统。这种默认行为可以通过修改shared subtrees属性来改变。

其实,用户创建namespace,其目的一般是希望创建完全隔离的运行环境,所以默认情况下,拷贝挂载点信息时不会拷贝shared subtrees属性,而是将mount namespace中的所有挂载点的shared subtrees属性设置为private。

所以,假如namespace ns1中/mnt/foo是一个挂载点目录,基于ns1创建了一个mnt namespace ns2,在默认情况下:

  • 如果此时在ns1中新增一个挂载点/mnt/foo/bar,将不会影响到ns2中的/mnt/foo
  • 如果此时在ns2中新增一个挂载点/mnt/foo/baz,也不会影响到ns1中的/mnt/foo
  • 移除挂载点操作也一样

但这种默认行为可以改变。

unshare有一个选项--propagation private|shared|slave|unchanged可控制创建mnt namespace时挂载点的共享方式。

  • private:表示新创建的mnt namespace中的挂载点的shared subtrees属性都设置为private,即ns1和ns2的挂载点互不影响
  • shared:表示新创建的mnt namespace中的挂载点的shared subtrees属性都设置为shared,即ns1或ns2中新增或移除子挂载点都会同步到另一方
  • slave:表示新创建的mnt namespace中的挂载点的shared subtrees属性都设置为slave,即ns1中新增或移除子挂载点会影响ns2,但ns2不会影响ns1
  • unchanged:表示拷贝挂载点信息时也拷贝挂载点的shared subtrees属性,也就是说挂载点A原来是shared,在mnt namespace中也将是shared
  • 不指定--progapation选项时,创建的mount namespace中的挂载点的shared subtrees默认值是private

例如:

BASH

# root namespace:ns0
$ sudo mount --bind foo bar
$ sudo mount --make-shared bar    # 挂载点设置为shared

# 创建mnt namespace:ns1,且也拷贝shared属性
$ PS1="ns1$ " sudo unshare -m -u --propagation unchanged sh
[ns1]$ grep 'foo' /proc/self/mountinfo 
944 682 8:5 foo bar rw,relatime shared:1

# 在ns1中bar挂载点下新增子挂载点,子挂载点将同步到ns0
# 因为foo和bar已绑定,且bar的属性是shared,所以还会同步到foo目录下
[ns1]$ sudo mount --bind baz bar/subfoo
[ns1]$ tree foo bar 
foo
└── subfoo
    └── subbaz
bar
└── subfoo
    └── subbaz
[ns1]$ grep 'foo' /proc/self/mountinfo
944 682 8:5 foo bar rw,relatime shared:1
945 944 8:5 baz bar/subfoo rw,relatime shared:1
947 682 8:5 baz foo/subfoo rw,relatime shared:1

# 第二个窗口会话中查看ns0的挂载点信息
# 已经同步过来了
$ grep 'foo' /proc/self/mountinfo
622 29 8:5  foo bar rw,relatime shared:1
948 622 8:5 baz bar/subfoo rw,relatime shared:1
946 29 8:5  baz foo/subfoo rw,relatime shared:1
$ tree foo bar 
foo
└── subfoo
    └── subbaz
bar
└── subfoo
    └── subbaz

 

文章作者: 骏马金龙

文章链接: https://www.junmajinlong.com/virtual/namespace/ns_overview/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 骏马金龙

“您的支持是我持续分享的动力”

微信收款码
微信
支付宝收款码
支付宝


目录