首页 > 系统相关 >Linux Namespace

Linux Namespace

时间:2023-09-20 22:08:29浏览次数:30  
标签:container001 Namespace ubuntu namespace dev Linux root bash

1 Linux Namespace概述

Namespace是对全局系统资源的一种封装隔离,使得处于不同namespace的进程拥有独立的全局资源,改变一个namespace中的系统资源只会影响当前namespace里的进程,对其他namespace中的进程没有影响。

1.1Linux内核支持的namespaces

目前,Linux内核里面实现了7种不同类型的namespace。

名称        宏定义             隔离内容
Cgroup      CLONE_NEWCGROUP   Cgroup root directory (since Linux 4.6)
IPC         CLONE_NEWIPC      System V IPC, POSIX message queues (since Linux 2.6.19)
Network     CLONE_NEWNET      Network devices, stacks, ports, etc. (since Linux 2.6.24)
Mount       CLONE_NEWNS       Mount points (since Linux 2.4.19)
PID         CLONE_NEWPID      Process IDs (since Linux 2.6.24)
User        CLONE_NEWUSER     User and group IDs (started in Linux 2.6.23 and completed in Linux 3.8)
UTS         CLONE_NEWUTS      Hostname and NIS domain name (since Linux 2.6.19)

**注意:**由于Cgroup namespace在4.6的内核中才实现,并且和cgroup v2关系密切,现在普及程度还不高,比如docker现在就还没有用它,所以在namespace这个系列中不会介绍Cgroup namespace

1.2 查看进程所属的namespaces

系统中的每个进程都有/proc/pid/ns/这样一个目录,里面包含了这个进程所属namespace的信息,里面每个文件的描述符都可以用来作为setns函数的参数

#查看当前bash进程所属的namespace
dev@ubuntu:~$ ls -l /proc/$$/ns     
total 0
lrwxrwxrwx 1 dev dev 0 7月 7 17:24 cgroup -> cgroup:[4026531835] #(since Linux 4.6)
lrwxrwxrwx 1 dev dev 0 7月 7 17:24 ipc -> ipc:[4026531839]       #(since Linux 3.0)
lrwxrwxrwx 1 dev dev 0 7月 7 17:24 mnt -> mnt:[4026531840]       #(since Linux 3.8)
lrwxrwxrwx 1 dev dev 0 7月 7 17:24 net -> net:[4026531957]       #(since Linux 3.0)
lrwxrwxrwx 1 dev dev 0 7月 7 17:24 pid -> pid:[4026531836]       #(since Linux 3.8)
lrwxrwxrwx 1 dev dev 0 7月 7 17:24 user -> user:[4026531837]     #(since Linux 3.8)
lrwxrwxrwx 1 dev dev 0 7月 7 17:24 uts -> uts:[4026531838]       #(since Linux 3.0)

1.3 跟namespace相关的API

clone: 创建一个新的进程并把他放到新的namespace中
setns: 将当前进程加入到已有的namespace中
unshare: 使当前进程退出指定类型的namespace,并加入到新创建的namespace;相当于创建并加入新的namespace

clone和unshare的区别

  • unshare是使当前进程加入新的namespace
  • clone是创建一个新的子进程,然后让子进程加入新的namespace,而当前进程保持不变

1.4 其他

当一个namespace中的所有进程都退出时,该namespace将会被销毁。当然还有其他方法让namespace一直存在,假设我们有一个进程号为1000的进程,以ipc namespace为例:

  • 1.通过mount --bind命令。例如mount --bind /proc/1000/ns/ipc/other/file,就算属于这个ipc namespace的所有进程都退出了,只要/other/file还在,这个ipc namespace就一直存在,其他进程就可以利用/other/file,通过setns函数加入到这个namespace
  • 2.在其他namespace的进程中打开/proc/1000/ns/ipc文件,并一直持有这个文件描述符不关闭,以后就可以用setns函数加入这个namespace。

2 UTS namespace(CLONE_NEWUTS)

UTS namespace用来隔离系统的hostname以及NIS domain name。

这两个资源可以通过sethostname(2)和setdomainname(2)函数来设置,以及通过uname(2),gethostname(2),getdomainname(2)函数来获取。

2.1 创建新的UTS namespace

#define _GNU_SOURCE
#include <sched.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define NOT_OK_EXIT(code, msg); {if(code == -1){perror(msg); exit(-1);} }

//子进程从这里开始执行
static int child_func(void *hostname)
{
    //设置主机名
    sethostname(hostname, strlen(hostname));

    //用一个新的bash来替换掉当前子进程,
    //执行完execlp后,子进程没有退出,也没有创建新的进程,
    //只是当前子进程不再运行自己的代码,而是去执行bash的代码,
    //详情请参考"man execlp"
    //bash退出后,子进程执行完毕
    execlp("bash", "bash", (char *) NULL);

    //从这里开始的代码将不会被执行到,因为当前子进程已经被上面的bash替换掉了

    return 0;
}

static char child_stack[1024*1024]; //设置子进程的栈空间为1M

int main(int argc, char *argv[])
{
    pid_t child_pid;

    if (argc < 2) {
        printf("Usage: %s <child-hostname>\n", argv[0]);
        return -1;
    }

    //创建并启动子进程,调用该函数后,父进程将继续往后执行,也就是执行后面的waitpid
    child_pid = clone(child_func,  //子进程将执行child_func这个函数
                    //栈是从高位向低位增长,所以这里要指向高位地址
                    child_stack + sizeof(child_stack),
                    //CLONE_NEWUTS表示创建新的UTS namespace,
                    //这里SIGCHLD是子进程退出后返回给父进程的信号,跟namespace无关
                    CLONE_NEWUTS | SIGCHLD,
                    argv[1]);  //传给child_func的参数
    NOT_OK_EXIT(child_pid, "clone");

    waitpid(child_pid, NULL, 0); //等待子进程结束

    return 0;    //这行执行完之后,父进程结束
}

代码:

  • 父进程创建新的子进程,并且设置CLONE_NEWUTS,这样就会创建新的UTS namespace并且让子进程属于这个新的namespace,然后父进程一直等待子进程退出
  • 子进程在设置好新的hostname后被bash替换掉
  • 当bash退出后,子进程退出,接着父进程也退出

输出效果:

#------------------------第一个shell窗口------------------------
#将上面的代码保存为namespace_uts_demo.c, 
#然后用gcc将它编译成可执行文件namespace_uts_demo
dev@ubuntu:~/code$ gcc namespace_uts_demo.c -o namespace_uts_demo   

#启动程序,传入参数container001
#创建新的UTS namespace需要root权限,所以用到sudo
dev@ubuntu:~/code$ sudo ./namespace_uts_demo container001

#新的bash被启动,从shell的提示符可以看出,hostname已经被改成了container001
#这里bash的提示符是‘#’,表示bash有root权限,
#这是因为我们是用sudo来运行的程序,于是我们程序创建的子进程有root权限
root@container001:~/code#

#用hostname命令再确认一下
root@container001:~/code# hostname
container001

#pstree是用来查看系统中进程之间父子关系的工具
#下面的输出过滤掉了跟namespace_uts_demo无关的内容
#本次操作是通过ssh客户端远程连接到Linux主机进行的,
#所以bash(24429)的父进程是一系列的sshd进程,
#我们在bash(24429)里面执行了sudo ./namespace_uts_demo container001
#所以有了sudo(27332)和我们程序namespace_uts_d(27333)对应的进程,
#我们的程序自己clone了一个新的子进程,由于clone的时候指定了参数CLONE_NEWUTS,
#所以新的子进程属于一个新的UTS namespace,然后这个新进程调用execlp后被bash替换掉了,
#于是有了bash(27334), 这个bash进程拥有所有当前子进程的属性, 
#由于我们的pstree命令是在bash(27334)里面运行的,
#所以这里pstree(27345)是bash(27334)的子进程
root@container001:~/code# pstree -pl
systemd(1)───sshd(24351)───sshd(24428)───bash(24429)───sudo(27332)──
─namespace_uts_d(27333)───bash(27334)───pstree(27345)

#验证一下我们运行的bash进程是不是bash(27334)
#下面这个命令可以输出当前bash的PID
root@container001:~/code# echo $$
27334

#验证一下我们的父进程和子进程是否不在同一个UTS namespace
root@container001:~/code# readlink /proc/27333/ns/uts
uts:[4026531838]
root@container001:~/code# readlink /proc/27334/ns/uts
uts:[4026532445]
#果然不属于同一个UTS namespace,说明新的uts namespace创建成功

#默认情况下,子进程应该继承父进程的namespace
#systemd(1)是我们程序父进程namespace_uts_d(27333)的祖先进程,
#他们应该属于同一个namespace
root@container001:~/code# readlink /proc/1/ns/uts
uts:[4026531838]

#所有bash(27334)里面执行的进程应该和bash(27334)属于同样的namespace
#self指向当前运行的进程,在这里即readlink进程
root@container001:~/code# readlink /proc/self/ns/uts
uts:[4026532445]

#------------------------第二个shell窗口------------------------
#重新打开一个新的shell窗口,确认这个shell和上面的namespace_uts_d(27333)属于同一个namespace
dev@ubuntu:~/code$ readlink /proc/$$/ns/uts
uts:[4026531838]

#老的namespace中的hostname还是原来的,不受新的namespace影响
dev@ubuntu:~/code$ hostname     
ubuntu
#有兴趣的同学可以在两个shell窗口里面分别用命令hostname设置hostname试试,
#会发现他们两个之间相互不受影响,这里就不演示了


#------------------------第一个shell窗口------------------------
#继续回到原来的shell,试试在container001里面再运行一下那个程序会怎样
root@container001:~/code# ./namespace_uts_demo container002

#创建了一个新的UTS namespace,hostname被改成了container002
root@container002:~/code#
root@container002:~/code# hostname
container002

#新的UTS namespace
root@container002:~/code# readlink /proc/$$/ns/uts
uts:[4026532455]

#进程间的关系和上面的差不多,在后面又生成了namespace_uts_d(27354)和bash(27355)
root@container002:~/code# pstree -pl
systemd(1)───sshd(24351)───sshd(24428)───bash(24429)───sudo(27332)──
─namespace_uts_d(27333)───bash(27334)───namespace_uts_d(27354)──
─bash(27355)───pstree(27367)

#退出bash(27355)后,它的父进程namespace_uts_d(27354)也接着退出,
#于是又回到了进程bash(27334)中,hostname于是也回到了container001
#注意: 在bash(27355)退出的过程中,并没有任何进程的namespace发生变化,
#只是所有属于namespace container002的进程都执行完退出了
root@container002:~/code# exit
exit
root@container001:~/code#
root@container001:~/code# hostname
container001

2.2 将当前进程加入指定的namespace

代码:

#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

#define NOT_OK_EXIT(code, msg); {if(code == -1){perror(msg); exit(-1);} }

int main(int argc, char *argv[])
{
    int fd, ret;

    if (argc < 2) {
        printf("%s /proc/PID/ns/FILE\n", argv[0]);
        return -1;
    }

    //获取namespace对应文件的描述符
    fd = open(argv[1], O_RDONLY);
    NOT_OK_EXIT(fd, "open");

    //执行完setns后,当前进程将加入指定的namespace
    //这里第二个参数为0,表示由系统自己检测fd对应的是哪种类型的namespace
    ret = setns(fd, 0);
    NOT_OK_EXIT(ret, "open");

    //用一个新的bash来替换掉当前子进程
    execlp("bash", "bash", (char *) NULL);

    return 0;
}

输出效果:

#--------------------------第一个shell窗口----------------------
#重用上面创建的namespace container001
#先确认一下hostname是否正确,
root@container001:~/code# hostname
container001

#获取bash的PID
root@container001:~/code# echo $$
27334

#得到bash所属的UTS namespace
root@container001:~/code# readlink /proc/27334/ns/uts
uts:[4026532445]



#--------------------------第二个shell窗口----------------------
#重新打开一个shell窗口,将上面的代码保存为文件namespace_join.c并编译
dev@ubuntu:~/code$ gcc namespace_join.c -o namespace_join

#运行程序前,确认下当前bash不属于namespace container001
dev@ubuntu:~/code$ hostname
ubuntu
dev@ubuntu:~/code$ readlink /proc/$$/ns/uts
uts:[4026531838]

#执行程序,使其加入第一个shell窗口中的bash所在的namespace
#27334是第一个shell窗口中bash的pid
dev@ubuntu:~/code$ sudo ./namespace_join /proc/27334/ns/uts
root@container001:~/code#

#加入成功,bash提示符里面的hostname以及UTS namespace的inode number和第一个shell窗口的都一样
root@container001:~/code# hostname
container001
root@container001:~/code# readlink /proc/$$/ns/uts
uts:[4026532445]

2.3 退出当前namespace并加入新创建的namespace

代码:

#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

#define NOT_OK_EXIT(code, msg); {if(code == -1){perror(msg); exit(-1);} }

static void usage(const char *pname)
{
    char usage[] = "Usage: %s [optins]\n"
                   "Options are:\n"
                   "    -i   unshare IPC namespace\n"
                   "    -m   unshare mount namespace\n"
                   "    -n   unshare network namespace\n"
                   "    -p   unshare PID namespace\n"
                   "    -u   unshare UTS namespace\n"
                   "    -U   unshare user namespace\n";
    printf(usage, pname);
    exit(0);
}

int main(int argc, char *argv[])
{
    int flags = 0, opt, ret;

    //解析命令行参数,用来决定退出哪个类型的namespace
    while ((opt = getopt(argc, argv, "imnpuUh")) != -1) {
        switch (opt) {
            case 'i': flags |= CLONE_NEWIPC;        break;
            case 'm': flags |= CLONE_NEWNS;         break;
            case 'n': flags |= CLONE_NEWNET;        break;
            case 'p': flags |= CLONE_NEWPID;        break;
            case 'u': flags |= CLONE_NEWUTS;        break;
            case 'U': flags |= CLONE_NEWUSER;       break;
            case 'h': usage(argv[0]);               break;
            default:  usage(argv[0]);
        }
    }

    if (flags == 0) {
        usage(argv[0]);
    }

    //执行完unshare函数后,当前进程就会退出当前的一个或多个类型的namespace,
    //然后进入到一个或多个新创建的不同类型的namespace
    ret = unshare(flags);
    NOT_OK_EXIT(ret, "unshare");

    //用一个新的bash来替换掉当前子进程
    execlp("bash", "bash", (char *) NULL);

    return 0;
}

输出效果:

#将上面的代码保存为文件namespace_leave.c并编译
dev@ubuntu:~/code$ gcc namespace_leave.c -o namespace_leave

#查看当前bash所属的UTS namespace
dev@ubuntu:~/code$ readlink /proc/$$/ns/uts
uts:[4026531838]

#执行程序, -u表示退出并加入新的UTS namespace
dev@ubuntu:~/code$ sudo ./namespace_leave -u
root@ubuntu:~/code#

#再次查看UTS namespace,已经变了,说明已经离开原来的namespace并加入了新的namespace
#细心的同学可能已经发现这里的inode number刚好和上面namespace container002的相同,
#这说明在container002被销毁后,inode number被回收再利用了
root@ubuntu:~/code# readlink /proc/$$/ns/uts
uts:[4026532455]

#反复执行几次,得到类似的结果
root@ubuntu:~/code# ./namespace_leave -u
root@ubuntu:~/code# readlink /proc/$$/ns/uts
uts:[4026532456]
root@ubuntu:~/code# ./namespace_leave -u
root@ubuntu:~/code# readlink /proc/$$/ns/uts
uts:[4026532457]
root@ubuntu:~/code# ./namespace_leave -u
root@ubuntu:~/code# readlink /proc/$$/ns/uts
uts:[4026532458]

2.4 总结

  • namespace的本质就是把原来所有进程全局共享的资源拆分成了很多个一组一组进程共享的资源
  • 当一个namespace里面的所有进程都退出时,namespace也会被销毁,所以抛开进程谈namespace没有意义
  • UTS namespace就是进程的一个属性,属性值相同的一组进程就属于同一个namespace,跟这组进程之间有没有亲戚关系无关。
  • clone和unshare都有创建加入新的namespace的功能,他们的主要区别是:
  • unshare是使当前进程加入新创建的namespace
  • clone是创建一个新的子进程,然后让子进程加入新的namespace
  • UTS namespace没有嵌套关系,即不存在说一个namespace是另一个namespace的父namespace

3 IPC namespace(CLONE_NEWIPC)

IPC namespace用来隔离System VIPC objects和POSIX message queues。其中System V IPC objects包含Message queues、Semaphore sets和Shared memory segments。

3.1 namespace相关tool

  • nsenter:加入指定进程的指定类型的namespace,然后执行参数中指定的命令。
  • unshare:离开当前指定类型的namespace,创建且加入新的namespace,然后执行参数中指定的命令。

3.2 示例

这里将以消息队列为例,演示一下隔离效果,在本例中将用到两个ipc相关的命令

  • ipcmk 创建shared memory segments,message queues,和semaphore arrays
  • ipcs 查看shared memory segments,message queues,和semaphore arrarys

为了使演示更直观,在创建新的ipc namespace的时候,同时也创建新的uts namespace,然后为新的uts namespace设置新hostname,这样就能通过shell提示符一眼看出这是属于新的namespace的bash

#--------------------------第一个shell窗口----------------------
#记下默认的uts和ipc namespace number
dev@ubuntu:~$ readlink /proc/$$/ns/uts /proc/$$/ns/ipc
uts:[4026531838]
ipc:[4026531839]

#确认hostname
dev@ubuntu:~$ hostname
ubuntu

#查看现有的ipc Message Queues,默认情况下没有message queue
dev@ubuntu:~$ ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

#创建一个message queue
dev@ubuntu:~$ ipcmk -Q
Message queue id: 0
dev@ubuntu:~$ ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages
0x12aa0de5 0          dev        644        0            0


#--------------------------第二个shell窗口----------------------
#重新打开一个shell窗口,确认和上面的shell是在同一个namespace,
#能看到上面创建的message queue
dev@ubuntu:~$ readlink /proc/$$/ns/uts /proc/$$/ns/ipc
uts:[4026531838]
ipc:[4026531839]
dev@ubuntu:~$ ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages
0x12aa0de5 0          dev        644        0            0

#运行unshare创建新的ipc和uts namespace,并且在新的namespace中启动bash
#这里-i表示启动新的ipc namespace,-u表示启动新的utsnamespace
dev@ubuntu:~$ sudo unshare -iu /bin/bash
root@ubuntu:~#

#确认新的bash已经属于新的ipc和uts namespace了
root@ubuntu:~# readlink /proc/$$/ns/uts /proc/$$/ns/ipc
uts:[4026532455]
ipc:[4026532456]

#设置新的hostname以便和第一个shell里面的bash做区分
root@ubuntu:~# hostname container001
root@ubuntu:~# hostname
container001

#当hostname改变后,bash不会自动修改它的命令行提示符
#所以运行exec bash重新加载bash
root@ubuntu:~# exec bash
root@container001:~#
root@container001:~# hostname
container001

#现在各个bash进程间的关系如下
#bash(24429)是shell窗口打开时的bash
#bash(27668)是运行sudo unshare创建的bash,和bash(24429)不在同一个namespace
root@container001:~# pstree -pl
├──sshd(24351)───sshd(24428)───bash(24429)───sudo(27667)───bash(27668)───pstree(27695)

#查看message queues,看不到原来namespace里面的消息,说明已经被隔离了
root@container001:~# ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

#创建一条新的message queue
root@container001:~# ipcmk -Q
Message queue id: 0
root@container001:~# ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages
0x54b08fc2 0          root       644        0            0

#--------------------------第一个shell窗口----------------------
#回到第一个shell窗口,看看有没有受到新namespace的影响
dev@ubuntu:~$ ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages
0x12aa0de5 0          dev        644        0            0
#完全无影响,还是原来的信息

#试着加入第二个shell窗口里面bash的uts和ipc namespace
#-t后面跟pid用来指定加入哪个进程所在的namespace
#这里27668是第二个shell中正在运行的bash的pid
#加入成功后将运行/bin/bash
dev@ubuntu:~$ sudo nsenter -t 27668 -u -i /bin/bash

#加入成功,bash的提示符也自动变过来了
root@container001:~# readlink /proc/$$/ns/uts /proc/$$/ns/ipc
uts:[4026532455]
ipc:[4026532456]

#显示的是新namespace里的message queues
root@container001:~# ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages
0x54b08fc2 0          root       644        0            0

4 mount namespaces(CLONE_NEWNS)

Mount namespace用来隔离文件系统的挂载点,使得不同的mount namespace拥有自己独立的挂载点信息,不同的namespace之间不会相互影响,这对于构建用户或者容器自己的文件系统目录非常有用。

当前进程所在mount namespace里的所有挂载信息可以在/proc/pid/mounts、/proc/pid/mountinfo和/proc/pid/mountstats里面找到。

Mount namespaces是第一个被加入Linux的namespace,由于当时没想到还会引入其它的namespace,所以取名为CLONE_NEWNS,而没有叫CLONE_NEWMOUNT。

每个mount namespace都拥有一份自己的挂载点列表,当用clone或者unshare函数创建新的mount namespace时,新创建的namespace将拷贝一份老namespace里的挂载点列表,但从这之后,他们就没有关系了,通过mount和umount增加和删除各自namespace里面的挂载点都不会相互影响。

4.1 演示

#--------------------------第一个shell窗口----------------------
#先准备两个iso文件,用于后面的mount测试
dev@ubuntu:~$ mkdir iso
dev@ubuntu:~$ cd iso/
dev@ubuntu:~/iso$ mkdir -p iso01/subdir01
dev@ubuntu:~/iso$ mkdir -p iso02/subdir02
dev@ubuntu:~/iso$ mkisofs -o ./001.iso ./iso01
dev@ubuntu:~/iso$ mkisofs -o ./002.iso ./iso02
dev@ubuntu:~/iso$ ls
001.iso  002.iso  iso01  iso02
#准备目录用于mount
dev@ubuntu:~/iso$ sudo mkdir /mnt/iso1 /mnt/iso2

#查看当前所在的mount namespace
dev@ubuntu:~/iso$ readlink /proc/$$/ns/mnt
mnt:[4026531840]

#mount 001.iso 到 /mnt/iso1/
dev@ubuntu:~/iso$ sudo mount ./001.iso /mnt/iso1/
mount: /dev/loop1 is write-protected, mounting read-only

#mount成功
dev@ubuntu:~/iso$ mount |grep /001.iso
/home/dev/iso/001.iso on /mnt/iso1 type iso9660 (ro,relatime)

#创建并进入新的mount和uts namespace
dev@ubuntu:~/iso$ sudo unshare --mount --uts /bin/bash
#更改hostname并重新加载bash
root@ubuntu:~/iso# hostname container001
root@ubuntu:~/iso# exec bash
root@container001:~/iso#

#查看新的mount namespace
root@container001:~/iso# readlink /proc/$$/ns/mnt
mnt:[4026532455]

#老namespace里的挂载点的信息已经拷贝到新的namespace里面来了
root@container001:~/iso# mount |grep /001.iso
/home/dev/iso/001.iso on /mnt/iso1 type iso9660 (ro,relatime)

#在新namespace中mount 002.iso
root@container001:~/iso# mount ./002.iso /mnt/iso2/
mount: /dev/loop0 is write-protected, mounting read-only
root@container001:~/iso# mount |grep iso
/home/dev/iso/001.iso on /mnt/iso1 type iso9660 (ro,relatime)
/home/dev/iso/002.iso on /mnt/iso2 type iso9660 (ro,relatime)

#umount 001.iso
root@container001:~/iso# umount /mnt/iso1
root@container001:~/iso# mount |grep iso
/home/dev/iso/002.iso on /mnt/iso2 type iso9660 (ro,relatime)

#/mnt/iso1目录变为空
root@container001:~/iso# ls /mnt/iso1
root@container001:~/iso#

#--------------------------第二个shell窗口----------------------
#打开新的shell窗口,老namespace中001.iso的挂载信息还在
#而在新namespace里面mount的002.iso这里看不到
dev@ubuntu:~$ mount |grep iso
/home/dev/iso/001.iso on /mnt/iso1 type iso9660 (ro,relatime)
#iso1目录里面也有内容
dev@ubuntu:~$ ls /mnt/iso1
subdir01
#说明两个namespace中的mount信息是隔离的

4.2 Shared subtrees

在某些情况下,比如系统添加了一个新的硬盘,这个时候如果mount namespace是完全隔离的,想要在各个namespace里面用这个硬盘,就需要在每个namespace里面手动mount这个硬盘,这个是很麻烦的,这时Shared subtrees就可以帮助我们解决这个问题。

4.3 演示

#--------------------------第一个shell窗口----------------------
#准备4个虚拟的disk,并在上面创建ext2文件系统,用于后续的mount测试
dev@ubuntu:~/iso$ cd && mkdir disks && cd disks
dev@ubuntu:~/disks$ dd if=/dev/zero bs=1M count=32 of=./disk1.img
dev@ubuntu:~/disks$ dd if=/dev/zero bs=1M count=32 of=./disk2.img
dev@ubuntu:~/disks$ dd if=/dev/zero bs=1M count=32 of=./disk3.img
dev@ubuntu:~/disks$ dd if=/dev/zero bs=1M count=32 of=./disk4.img
dev@ubuntu:~/disks$ mkfs.ext2 ./disk1.img
dev@ubuntu:~/disks$ mkfs.ext2 ./disk2.img
dev@ubuntu:~/disks$ mkfs.ext2 ./disk3.img
dev@ubuntu:~/disks$ mkfs.ext2 ./disk4.img
#准备两个目录用于挂载上面创建的disk
dev@ubuntu:~/disks$ mkdir disk1 disk2
dev@ubuntu:~/disks$ ls
disk1  disk1.img  disk2  disk2.img  disk3.img  disk4.img


#显式的分别以shared和private方式挂载disk1和disk2
dev@ubuntu:~/disks$ sudo mount --make-shared ./disk1.img ./disk1
dev@ubuntu:~/disks$ sudo mount --make-private ./disk2.img ./disk2
dev@ubuntu:~/disks$ cat /proc/self/mountinfo |grep disk| sed 's/ - .*//'
164 24 7:1 / /home/dev/disks/disk1 rw,relatime shared:105
173 24 7:2 / /home/dev/disks/disk2 rw,relatime

#查看mount namespace编号
dev@ubuntu:~/disks$ readlink /proc/$$/ns/mnt
mnt:[4026531840]

#--------------------------第二个shell窗口----------------------
#重新打开一个新的shell窗口
dev@ubuntu:~$ cd ./disks
#创建新的mount namespace
#默认情况下,unshare会将新namespace里面的所有挂载点的类型设置成private,
#所以这里用到了参数--propagation unchanged,
#让新namespace里的挂载点的类型和老namespace里保持一致。
#--propagation参数还支持private|shared|slave类型,
#和mount命令的那些--make-private参数一样,
#他们的背后都是通过调用mount(...)函数传入不同的参数实现的
dev@ubuntu:~/disks$ sudo unshare --mount --uts --propagation unchanged /bin/bash
root@ubuntu:~/disks# hostname container001
root@ubuntu:~/disks# exec bash
root@container001:~/disks# 

#确认已经是在新的mount namespace里面了
root@container001:~/disks# readlink /proc/$$/ns/mnt
mnt:[4026532463]

#由于前面指定了--propagation unchanged,
#所以新namespace里面的/home/dev/disks/disk1也是shared,
#且和老namespace里面的/home/dev/disks/disk1属于同一个peer group 105
#因为在不同的namespace里面,所以这里挂载点的ID和原来namespace里的不一样了
root@container001:~/disks# cat /proc/self/mountinfo |grep disk| sed 's/ - .*//'
221 177 7:1 / /home/dev/disks/disk1 rw,relatime shared:105
222 177 7:2 / /home/dev/disks/disk2 rw,relatime

#分别在disk1和disk2目录下创建disk3和disk4,然后挂载disk3,disk4到这两个目录
root@container001:~/disks# mkdir ./disk1/disk3 ./disk2/disk4
root@container001:~/disks# mount ./disk3.img ./disk1/disk3/
root@container001:~/disks# mount ./disk4.img ./disk2/disk4/
root@container001:~/disks# cat /proc/self/mountinfo |grep disk| sed 's/ - .*//'
221 177 7:1 / /home/dev/disks/disk1 rw,relatime shared:105
222 177 7:2 / /home/dev/disks/disk2 rw,relatime
223 221 7:3 / /home/dev/disks/disk1/disk3 rw,relatime shared:107
227 222 7:4 / /home/dev/disks/disk2/disk4 rw,relatime

#--------------------------第一个shell窗口----------------------
#回到第一个shell窗口

#可以看出由于/home/dev/disks/disk1是shared,且两个namespace里的这个挂载点都属于peer group 105,
#所以在新namespace里面挂载的disk3,在老的namespace里面也看的到
#但是看不到disk4的挂载信息,那是因为/home/dev/disks/disk2是private的
dev@ubuntu:~/disks$ cat /proc/self/mountinfo |grep disk| sed 's/ - .*//'
164 24 7:1 / /home/dev/disks/disk1 rw,relatime shared:105
173 24 7:2 / /home/dev/disks/disk2 rw,relatime
224 164 7:3 / /home/dev/disks/disk1/disk3 rw,relatime shared:107

#我们可以随时修改挂载点的propagation type
#这里我们通过mount命令将disk3改成了private类型
dev@ubuntu:~/disks$ sudo mount --make-private /home/dev/disks/disk1/disk3
dev@ubuntu:~/disks$ cat /proc/self/mountinfo |grep disk3| sed 's/ - .*//'
224 164 7:3 / /home/dev/disks/disk1/disk3 rw,relatime

#--------------------------第二个shell窗口----------------------
#回到第二个shell窗口,disk3的propagation type还是shared,
#表明在老的namespace里面对propagation type的修改不会影响新namespace里面的挂载点
root@container001:~/disks# cat /proc/self/mountinfo |grep disk3| sed 's/ - .*//'
223 221 7:3 / /home/dev/disks/disk1/disk3 rw,relatime shared:107

5 pid namespace(CLONE_NEWPID)

PID namespaces用来隔离进程的ID空间,使得不同pid namespace里的进程ID可以重复相互之间不影响。

PID namespace可以嵌套,也就是说有父子关系,在当前namespace里面创建的所有新的namespace都是当前namespace的子namespace。父namespace里面可以看到所有子孙后代namespace里的进程信息,而子namespace里看不到祖先或者兄弟namespace里的进程信息。

目前PID namespace最多可以嵌套32层,由内核中的宏MAX_PID_NS_LEVEL来定义

Linux下的每个进程都有一个对应的/proc/PID目录,该目录包含了大量的有关当前进程的信息。对一个PID namespace而言,/proc/目录只包含当前namespace和它所有子孙后代namespace里的进程的信息。

在Linux系统中,进程ID从1开始往后不断增加,并且不能重复(当然进程退出后,ID会被回收再利用),进程ID为1的进程是内核启动的第一个应用层进程,一般是init进程(现在采用systemd的系统第一个进程是systemd),具有特殊意义,当系统中一个进程的父进程退出时,内核会指定init进程成为这个进程的新父进程,而当init进程退出时,系统也将退出。

除了在init进程里指定了handler的信号外,内核会帮init进程屏蔽掉其他任何信号,这样可以防止其他进程不小心kill掉init进程导致系统挂掉。不过有了PID namespace后,可以通过在父namespace中发送SIGKILL或者SIGSTOP信号来终止namespace中的ID为1的进程。

由于ID为1的进程的特殊性,所以每个PID namespace的第一个进程的ID都是1。当这个进程运行停止后,内核将会给这个namespace里的所有其它进程发送SIGKILL信号,致使其他所有进程都停止,于是namespace被销毁掉。

5.1 简单示例

#查看当前pid namespace的ID
dev@ubuntu:~$ readlink /proc/self/ns/pid
pid:[4026531836]

#启动新的pid namespace
#这里同时也启动了新的uts和mount namespace
#新的uts是为了设置一个新的hostname,便于和老的namespace区分
#新的mount namespace是为了方便我们修改新namespace里面的mount信息,
#因为这样不会对老namespace造成影响
#这里--fork是为了让unshare进程fork一个新的进程出来,然后再用bash替换掉新的进程
#这是pid namespace本身的限制,进程所属的pid namespace在它创建的时候就确定了,不能更改,
#所以调用unshare和nsenter后,原来的进程还是属于老的namespace,
#而新fork出来的进程才属于新的namespace
dev@ubuntu:~$ sudo unshare --uts --pid --mount --fork /bin/bash
root@ubuntu:~# hostname container001
root@ubuntu:~# exec bash
root@container001:~#

#查看进程间关系,当前bash(31646)确实是unshare的子进程
root@container001:~# pstree -pl
├─sshd(955)─┬─sshd(17810)───sshd(17891)───bash(17892)───sudo(31644)──
─unshare(31645)───bash(31646)───pstree(31677)
#他们属于不同的pid namespace
root@container001:~# readlink /proc/31645/ns/pid
pid:[4026531836]
root@container001:~# readlink /proc/31646/ns/pid
pid:[4026532469]

#但为什么通过这种方式查看到的namespace还是老的呢?
root@container001:~# readlink /proc/$$/ns/pid
pid:[4026531836]

#由于我们实际上已经是在新的namespace里了,并且当前bash是当前namespace的第一个进程
#所以在新的namespace里看到的他的进程ID是1
root@container001:~# echo $$
1
#但由于我们新的namespace的挂载信息是从老的namespace拷贝过来的,
#所以这里看到的还是老namespace里面的进程号为1的信息
root@container001:~# readlink /proc/1/ns/pid
pid:[4026531836]
#ps命令依赖/proc目录,所以ps的输出还是老namespace的视图
root@container001:~# ps ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 7月07 ?       00:00:06 /sbin/init
root         2     0  0 7月07 ?       00:00:00 [kthreadd]
 ...
root     31644 17892  0 7月14 pts/0   00:00:00 sudo unshare --uts --pid --mount --fork /bin/bash
root     31645 31644  0 7月14 pts/0   00:00:00 unshare --uts --pid --mount --fork /bin/bash

#所以我们需要重新挂载我们的/proc目录
root@container001:~# mount -t proc proc /proc

#重新挂载后,能看到我们新的pid namespace ID了
root@container001:~# readlink /proc/$$/ns/pid
pid:[4026532469]
#ps的输出也正常了
root@container001:~# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 7月14 pts/0   00:00:00 bash
root        44     1  0 00:06 pts/0    00:00:00 ps -ef

5.2 PID namespace嵌套

  • 调用unshare或者setns函数后,当前进程的namespace不会发生变化,不会加入到新的namespace,而它的子进程会加入到新的namespace,而它的子进程会加入到新的namespace。也就是说进程属于哪个namespace是在进程创建的时候决定的,并且以后再也无法更改。
  • 在一个PID namespace里的进程,它的父进程可能不在当前namespace中,而是在外面的namespace里面(这里外面的namespace指当前namespace的祖先namespace),这类进程的ppid都是0。比如新namespace里面的第一个进程,他的父进程就在外面的namespace里。通过setns的方式加入到新namespace中的进程的父进程也在外面的namespace中。
  • 可以在祖先namespace中看到子namespace的所有进程信息,且可以发信号给子namespace的进程,但进程在不同namespace中的PID是不一样的。

5.3 嵌套示例

#--------------------------第一个shell窗口----------------------
#记下最外层的namespace ID
dev@ubuntu:~$ readlink /proc/$$/ns/pid
pid:[4026531836]

#创建新的pid namespace, 这里--mount-proc参数是让unshare自动重新mount /proc目录
dev@ubuntu:~$ sudo unshare --uts --pid --mount --fork --mount-proc /bin/bash
root@ubuntu:~# hostname container001
root@ubuntu:~# exec bash
root@container001:~# readlink /proc/$$/ns/pid
pid:[4026532469]

#再创建新的pid namespace
root@container001:~# unshare --uts --pid --mount --fork --mount-proc /bin/bash
root@container001:~# hostname container002
root@container001:~# exec bash
root@container002:~# readlink /proc/$$/ns/pid
pid:[4026532472]

#再创建新的pid namespace
root@container002:~# unshare --uts --pid --mount --fork --mount-proc /bin/bash
root@container002:~# hostname container003
root@container002:~# exec bash
root@container003:~# readlink /proc/$$/ns/pid
pid:[4026532475]

#目前namespace container003里面就一个bash进程
root@container003:~# pstree -p
bash(1)───pstree(22)
#这样我们就有了三层pid namespace,
#他们的父子关系为container001->container002->container003

#--------------------------第二个shell窗口----------------------
#在最外层的namespace中查看上面新创建的三个namespace中的bash进程
#从这里可以看出,这里显示的bash进程的PID和上面container003里看到的bash(1)不一样
dev@ubuntu:~$ pstree -pl|grep bash|grep unshare
|-sshd(955)-+-sshd(17810)---sshd(17891)---bash(17892)---sudo(31814)--
-unshare(31815)---bash(31816)---unshare(31842)---bash(31843)--
-unshare(31864)---bash(31865)
#各个unshare进程的子bash进程分别属于上面的三个pid namespace
dev@ubuntu:~$ sudo readlink /proc/31816/ns/pid
pid:[4026532469]
dev@ubuntu:~$ sudo readlink /proc/31843/ns/pid
pid:[4026532472]
dev@ubuntu:~$ sudo readlink /proc/31865/ns/pid
pid:[4026532475]

#PID在各个namespace里的映射关系可以通过/proc/[pid]/status查看到
#这里31865是在最外面namespace中看到的pid
#45,23,1分别是在container001,container002和container003中的pid
dev@ubuntu:~$ grep pid /proc/31865/status
NSpid:  31865   45     23      1

#创建一个新的bash并加入container002
dev@ubuntu:~$ sudo nsenter --uts --mount --pid -t 31843 /bin/bash
root@container002:/#

#这里bash(23)就是container003里面的pid 1对应的bash
root@container002:/# pstree -p
bash(1)───unshare(22)───bash(23)
#unshare(22)属于container002
root@container002:/# readlink /proc/22/ns/pid
pid:[4026532472]
#bash(23)属于container003
root@container002:/# readlink /proc/23/ns/pid
pid:[4026532475]

#为什么上面pstree的结果里面没看到nsenter加进来的bash呢?
#通过ps命令我们发现,我们新加进来的那个/bin/bash的ppid是0,难怪pstree里面显示不出来
#从这里可以看出,跟最外层namespace不一样的地方就是,这里可以有多个进程的ppid为0
#从这里的TTY也可以看出哪些命令是在哪些窗口执行的,
#pts/0对应第一个shell窗口,pts/1对应第二个shell窗口
root@container002:/# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 04:39 pts/0    00:00:00 bash
root        22     1  0 04:39 pts/0    00:00:00 unshare --uts --pid --mount --fork --mount-proc /bin/bash
root        23    22  0 04:39 pts/0    00:00:00 bash
root        46     0  0 04:52 pts/1    00:00:00 /bin/bash
root        59    46  0 04:53 pts/1    00:00:00 ps -ef

#--------------------------第三个shell窗口----------------------
#创建一个新的bash并加入container001
dev@ubuntu:~$ sudo nsenter --uts --mount --pid -t 31816 /bin/bash
root@container001:/#

#通过pstree和ps -ef我们可看到所有三个namespace中的进程及他们的关系
#bash(1)───unshare(22)属于container001
#bash(23)───unshare(44)属于container002
#bash(45)属于container003,而68和84两个进程分别是上面两次通过nsenter加进来的bash
#同上面ps的结果比较我们可以看出,同样的进程在不同的namespace里面拥有不同的PID
root@container001:/# pstree -pl
bash(1)───unshare(22)───bash(23)───unshare(44)───bash(45)
root@container001:/# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 04:37 pts/0    00:00:00 bash
root        22     1  0 04:39 pts/0    00:00:00 unshare --uts --pid --mount --fork --mount-proc /bin/bash
root        23    22  0 04:39 pts/0    00:00:00 bash
root        44    23  0 04:39 pts/0    00:00:00 unshare --uts --pid --mount --fork --mount-proc /bin/bash
root        45    44  0 04:39 pts/0    00:00:00 bash
root        68     0  0 04:52 pts/1    00:00:00 /bin/bash
root        84     0  0 05:00 pts/2    00:00:00 /bin/bash
root        95    84  0 05:00 pts/2    00:00:00 ps -ef

#发送信号给contain002中的bash
root@container001:/# kill 68

#--------------------------第二个shell窗口----------------------
#回到第二个窗口,发现bash已经被kill掉了,说明父namespace是可以发信号给子namespace中的进程的
root@container002:/# exit
dev@ubuntu:~$

5.4 "init"示例

当一个进程的父进程被kill掉后,该进程将会被当前namespace中pid为1的进程接管,而不是被最外层的系统级别的init进程接管。

当pid为1的进程停止运行后,内核将会给这个namespace及其子孙namespace里的所有其他进程发送SIGKILL信号,致使其他所有进程都停止,于是当前namespace及其子孙后代的namespace都被销毁掉。

#还是继续以上面三个namespace为例
#--------------------------第一个shell窗口----------------------
#在003里面启动两个新的bash,使他们的继承关系如下
root@container003:~# bash
root@container003:~# bash
root@container003:~# pstree
bash───bash───bash───pstree

#利用unshare、nohup和sleep的组合,模拟出我们想要的父子进程
#unshare --fork会使unshare创建一个子进程
#nohup sleep sleep 3600&会让这个子进程在后台运行并且sleep一小时
root@container003:~# unshare --fork nohup sleep 3600&
[1] 77

#于是我们得到了我们想要的进程间关系结构
root@container003:~# pstree -p
bash(1)───bash(26)───bash(36)─┬─pstree(80)
                              └─unshare(77)───sleep(78)

#如我们所期望的,kill掉unshare(77)后, sleep就被当前pid namespace的bash(1)接管了
root@container003:~# kill 77
root@container003:~# pstree -p
bash(1)─┬─bash(26)───bash(36)───pstree(82)
        └─sleep(78)

#重新回到刚才的状态,后面将尝试在第三个窗口中kill掉这里的unshare进程
root@container003:~# kill 78
root@container003:~# unshare --fork nohup sleep 3600&
root@container003:~# pstree -p
bash(1)───bash(26)───bash(36)─┬─pstree(85)
                              └─unshare(83)───sleep(84)

#--------------------------第三个shell窗口----------------------
#来到第三个窗口
root@container001:/# pstree -p
bash(1)───unshare(22)───bash(23)───unshare(44)───bash(45)───bash(113)─
──bash(123)───unshare(170)───sleep(171)

#kill掉sleep(171)的父近程unshare(170),
root@container001:/# kill 170
#结果显示sleep(171)被bash(45)接管了,而不是bash(1),
#进一步说明container003里的进程只会被container003里的pid 1进程接管,
#而不会被外面container001的pid 1进程接管
root@container001:/# pstree -p
bash(1)───unshare(22)───bash(23)───unshare(44)──
─bash(45)─┬─bash(113)───bash(123)
          └─sleep(171)

#kill掉container002中pid 1的bash进程,在container001中,对应的是bash(23)
root@container001:/# kill 23
#根本没反应,说明bash不接收TERM信号(kill默认发送SIGTERM信号)
root@container001:/# pstree -p
bash(1)───unshare(22)───bash(23)───unshare(44)──
─bash(45)─┬─bash(113)───bash(123)
          └─sleep(171)

#试试SIGSTOP,貌似也不行      
root@container001:/# kill -SIGSTOP 23
root@container001:/# pstree -p
bash(1)───unshare(22)───bash(23)───unshare(44)──
─bash(45)─┬─bash(113)───bash(123)
          └─sleep(171)

#最后试试杀手锏SIGKILL,马到成功
root@container001:/# kill -SIGKILL 23
root@container001:/# pstree -p
bash(1)

#--------------------------第一个shell窗口----------------------
#container003和container002的bash退出了,
#第一个shell窗口直接退到了container001的bash
root@container003:~# Killed
root@container001:~#

#--------------------------第二个shell窗口----------------------
#通过nsenter方式加入到container002的bash也被kill掉了
root@container002:/# Killed
dev@ubuntu:~$

#从结果可以看出,container002的“init”进程被杀死后,
#内核将会发送SIGKILL给container002里的所有进程,
#这样导致container002及它所有子孙namespace里的进程都杀死,
#同时container002和container003也被销毁

5.5 其他

  • 通常情况下,如果PID namespace中的进程都退出了,这个namespace将会被销毁,但就如前面“Namespace概述”里介绍的,有两种情况会导致就算进程都退出了,这个namespace还会存在。但对于PID namespace来说,就算namespace还在,由于里面没有“init”进程,Kernel不允许其他进程加入到这个namespace,所以这个存在的namespace没有意义
  • 当一个PID通过UNIX domian socket在不同的PID namespace中传输时,PID将会自动转换成目的namespace中的PID

6 network namespace(CLONE_NEWET)

network namespace用来隔离网络设备,IP地址,端口等。每个namespace将会有自己独立的网络栈,路由表,防火墙规则,socket等。

每个新的network namespace默认有一个本地回环接口,除了lo接口外,所有的其他网络设备(物理/虚拟网络接口,网桥等)只能属于一个network namespace。每个socket也只能属于一个network namespace。

当新的network namespace被创建时,lo接口默认是关闭的,需要自己手动启动起

标记为“local devices”的设备不能从一个namespace移动到另一个namespace,比如loopback,bridge,ppp等,我们可以通过ethtool -k命令来查看设备的netns-local属性。

#这里“on”表示该设备不能被移动到其他network namespace
dev@ubuntu:~$ ethtool -k lo|grep netns-local
netns-local: on [fixed]

6.1 示例

本实例将演示如何创建新的network namespace并同外面的namespace进行通信。

#--------------------------第一个shell窗口----------------------
#记录默认network namespace ID
dev@ubuntu:~$ readlink /proc/$$/ns/net
net:[4026531957]

#创建新的network namespace
dev@ubuntu:~$ sudo unshare --uts --net /bin/bash
root@ubuntu:~# hostname container001
root@ubuntu:~# exec bash
root@container001:~# readlink /proc/$$/ns/net
net:[4026532478]

#运行ifconfig啥都没有
root@container001:~# ifconfig
root@container001:~#

#启动lo (这里不详细介绍ip这个tool的用法,请参考man ip)
root@container001:~# ip link set lo up
root@container001:~# ifconfig
lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

root@container001:~# ping 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.070 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.015 ms

#获取当前bash进程的PID
root@container001:~# echo $$
15812

#--------------------------第二个shell窗口----------------------
#创建新的虚拟以太网设备,让两个namespace能通讯
dev@ubuntu:~$ sudo ip link add veth0 type veth peer name veth1

#将veth1移动到上面第一个窗口中的namespace
#这里15812是上面bash的PID
dev@ubuntu:~$ sudo ip link set veth1 netns 15812

#为veth0分配IP并启动veth0
dev@ubuntu:~$ sudo ip address add dev veth0 192.168.8.1/24
dev@ubuntu:~$ sudo ip link set veth0 up
dev@ubuntu:~$ ifconfig veth0
veth0     Link encap:Ethernet  HWaddr 9a:4d:d5:96:b5:36
          inet addr:192.168.8.1  Bcast:0.0.0.0  Mask:255.255.255.0
          inet6 addr: fe80::984d:d5ff:fe96:b536/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:8 errors:0 dropped:0 overruns:0 frame:0
          TX packets:8 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:648 (648.0 B)  TX bytes:648 (648.0 B)

#--------------------------第一个shell窗口----------------------
#为veth1分配IP地址并启动它
root@container001:~# ip address add dev veth1 192.168.8.2/24
root@container001:~# ip link set veth1 up
root@container001:~# ifconfig veth1
veth1     Link encap:Ethernet  HWaddr 6a:dc:59:79:3c:8b
          inet addr:192.168.8.2  Bcast:0.0.0.0  Mask:255.255.255.0
          inet6 addr: fe80::68dc:59ff:fe79:3c8b/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:8 errors:0 dropped:0 overruns:0 frame:0
          TX packets:8 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:648 (648.0 B)  TX bytes:648 (648.0 B)

#连接成功
root@container001:~# ping 192.168.8.1
PING 192.168.8.1 (192.168.8.1) 56(84) bytes of data.
64 bytes from 192.168.8.1: icmp_seq=1 ttl=64 time=0.098 ms
64 bytes from 192.168.8.1: icmp_seq=2 ttl=64 time=0.023 ms

到目前为止,两个namespace之间可以网络通信了,但在container001里还是不能访问外网。下面将通过NAT的方式让container001能够上外网。

#--------------------------第二个shell窗口----------------------
#回到上面示例中的第二个窗口

#确认IP forward是否已经开通,这里1表示开通了
#如果你的机器上是0,请运行这个命令将它改为1: sudo sysctl -w net.ipv4.ip_forward=1
dev@ubuntu:~$ cat /proc/sys/net/ipv4/ip_forward
1

#添加NAT规则,这里ens32是机器上连接外网的网卡
#关于iptables和nat都比较复杂,这里不做解释
dev@ubuntu:~$ sudo iptables -t nat -A POSTROUTING -o ens32 -j MASQUERADE

#--------------------------第一个shell窗口----------------------
#回到第一个窗口,添加默认网关
root@container001:~# ip route add default via 192.168.8.1
root@container001:~# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.8.1     0.0.0.0         UG    0      0        0 veth1
192.168.8.0     0.0.0.0         255.255.255.0   U     0      0        0 veth1

#这样就可以访问外网了
#由于测试环境的限制,所以采用下面的方式检测网络是否畅通
#如果网络没有什么限制的话,随便ping一个外部的IP测试就可以了
root@container001:~# curl -I www.google.com
HTTP/1.1 200 OK
Date: Fri, 15 Jul 2016 08:12:03 GMT

6.2 ip netns

在单独操作network namespace时,ip netns是一个很方便的工具,并且它可以给namespace取一个名字,然后根据名字来操作namespace。那么给namespace取名字并且根据名字来管理namespace里面的进程是怎么实现的呢?

#开始之前,获取一下默认network namespace的ID
dev@ubuntu:~$ readlink /proc/$$/ns/net
net:[4026531957]

#创建一个用于绑定network namespace的文件,
#ip netns将所有的文件放到了目录/var/run/netns下,
#所以我们这里重用这个目录,并且创建一个我们自己的文件netnamespace1
dev@ubuntu:~$ sudo mkdir -p /var/run/netns
dev@ubuntu:~$ sudo touch /var/run/netns/netnamespace1

#创建新的network namespace,并在新的namespace中启动新的bash
dev@ubuntu:~$ sudo unshare --net bash
#查看新的namespace ID
root@ubuntu:~# readlink /proc/$$/ns/net
net:[4026532448]

#bind当前bash的namespace文件到上面创建的文件上
root@ubuntu:~# mount --bind /proc/$$/ns/net /var/run/netns/netnamespace1
#通过ls -i命令可以看到文件netnamespace1的inode号和namespace的编号相同,说明绑定成功
root@ubuntu:~# ls -i /var/run/netns/netnamespace1
4026532448 /var/run/netns/netnamespace1

#退出新创建的bash
root@ubuntu:~# exit
exit
#可以看出netnamespace1的inode没变,说明我们使用了bind mount后
#虽然新的namespace中已经没有进程了,但这个新的namespace还存在
dev@ubuntu:~$ ls -i /var/run/netns/netnamespace1
4026532448 /var/run/netns/netnamespace1

#上面的这一系列操作等同于执行了命令: ip netns add netnamespace1
#下面的nsenter命令等同于执行了命令: ip netns exec netnamespace1 bash

#我们可以通过nsenter命令再创建一个新的bash,并将它加入netnamespace1所关联的namespace(net:[4026532448])
dev@ubuntu:~$ sudo nsenter --net=/var/run/netns/netnamespace1 bash
root@ubuntu:~# readlink /proc/$$/ns/net
net:[4026532448]

从上面可以看出,给namespace取名字其实就是创建一个文件,然后通过mount --bind将新创建的namespace文件和该文件绑定,就算该namespace里的所有进程都退出了,内核还是会保留该namespace,以后我们还可以通过这个绑定的文件来加入该namespace。

通过这种办法,我们也可以给其他类型的namespace取名字。

7 user namespace(CLONE_NEWUSER)

User namespace用来隔离user权限相关的Linux资源,包括user IDs and group IDs,keys,和capabilities.

这是目前实现的namespace中最复杂的一个,因为user和权限息息相关,而权限又事关容器的安全,所以稍有不慎,就会出安全问题。

user namespace可以嵌套(目前内核控制最多32层),除了系统默认的user namespace外,所有的user namespace都有一个父user namespace,每个user namespace都可以有零到多个子user namespace。当在一个进程中调用unshare或者clone创建新的user namespace时,当前进程原来所在的user namespace为父user namespace,新的user namespace为子user namespace。

在不同的user namespace中,同样一个用户的user ID和group ID可以不一样,换句话说,一个用户可以在父user namespace中时普通用户,在子user namespace中是超级用户(超级用户只相对于子user namespace所拥有的资源,无法访问其他user namespace中需要超级用户才能访问资源)。

从Linux 3.8开始,创建新的user namespace不需要root权限。

7.1 创建user namespace

#--------------------------第一个shell窗口----------------------
#先记录下目前的id,gid和user namespace
dev@ubuntu:~$ id
uid=1000(dev) gid=1000(dev) groups=1000(dev),4(adm),24(cdrom),27(sudo)
dev@ubuntu:~$ readlink /proc/$$/ns/user
user:[4026531837]

#创建新的user namespace
dev@ubuntu:~$ unshare --user /bin/bash
nobody@ubuntu:~$ readlink /proc/$$/ns/user
user:[4026532464]
nobody@ubuntu:~$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)

为什么上面例子中显示的用户名是nobody,它的id和gid都是65534?

这是因为我们还没有映射父user namespace的user ID和group ID到子user namespace中来,这一步是必须的,因为这样系统才能控制一个user namespace里的用户在其他user namespace中的权限。(比如给其他user namespace中的进程发送信号,或者访问属于其他user namespace挂载的文件)

如果没有映射的化,当在新的user namespace中用getuid()和getgit()或取user id和group id时,系统将返回文件/proc/sys/kernel/overflowuid中定义的user ID以及/proc/sys/kernel/overflowgid中定义的group ID,他们的默认值都时65534。也就是说如果没有指定映射关系的化,会默认映射到ID 65534

下面看看这个user能干些什么

#--------------------------第一个shell窗口----------------------
#ls的结果显示/root目录属于nobody
nobody@ubuntu:~$ ls -l /|grep root
drwx------   3 nobody nogroup  4096 7月   8 18:39 root
#但是当前的nobody账号访问不了,说明这两个nobody不是一个ID,他们之间没有映射关系
nobody@ubuntu:~$ ls /root
ls: cannot open directory '/root': Permission denied

#这里显示/home/dev目录属于nobody
nobody@ubuntu:~$ ls -l /home/
drwxr-xr-x 11 nobody nogroup 4096 7月   8 18:40 dev
#touch成功,说明虽然没有显式的映射ID,但还是能访问父user namespace里dev账号拥有的资源
#说明他们背后还是有映射关系
nobody@ubuntu:~$ touch /home/dev/temp01
nobody@ubuntu:~$

7.2 映射user ID和group ID

通常情况下,创建新的user namespace后,第一件事就是映射user和group ID。映射ID的方法是添加配置到/proc/PID/uid_map和/proc/PID/gid_map(这里的PID是新user namespace中的进程ID,刚开始这两个文件都是空的)。

这两个文件里面的配置格式如下(可以有多条):

ID-inside-ns   ID-outside-ns   length

举个例子,0 1000 256这条配置就表示父user namespace中的1000~1256映射到新user namespace中的0~256

系统默认的user namespace没有父user namespace,但为了保持一致,kernel提供了一个虚拟的uid和gid map文件,看起来是这样子的: dev@ubuntu:~$ cat /proc/$$/uid_map 0 0 4294967295

那么谁可以向这个文件中写配置呢?

/proc/PID/uid_map和/proc/PID/gid_map的拥有者是创建新user namespace的这个user,所以和这个user在一个user namespace的root账户可以写。但这个user自己有没有写map文件权限还要看它有没有CAP_SETUID和CAP_SETGID的capability。

注意:只能向map文件写一次数据,但可以一次写多条,并且最多只能5条

原来的Linux就分root和非root,很多操作只能root完成,比如修改一个文件的ower,后来Linux将root的一些权限分解了,变成了各种capability,只要拥有了相应的capability,就能做相应的操作,不需要root账户的权限。

下面我们来看看如何用dev账户映射uid和gid

#--------------------------第一个shell窗口----------------------
#获取当前bash的pid
nobody@ubuntu:~$ echo $$
24126

#--------------------------第二个shell窗口----------------------
#dev是map文件的owner
dev@ubuntu:~$ ls -l /proc/24126/uid_map /proc/24126/gid_map
-rw-r--r-- 1 dev dev 0 7月  24 23:11 /proc/24126/gid_map
-rw-r--r-- 1 dev dev 0 7月  24 23:11 /proc/24126/uid_map

#但还是没有权限写这个文件
dev@ubuntu:~$ echo '0 1000 100' > /proc/24126/uid_map
bash: echo: write error: Operation not permitted
dev@ubuntu:~$ echo '0 1000 100' > /proc/24126/gid_map
bash: echo: write error: Operation not permitted
#当前用户运行的bash进程没有CAP_SETUID和CAP_SETGID的权限
dev@ubuntu:~$ cat /proc/$$/status | egrep 'Cap(Inh|Prm|Eff)'
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000

#为/binb/bash设置capability,
dev@ubuntu:~$ sudo setcap cap_setgid,cap_setuid+ep /bin/bash
#重新加载bash以后我们看到相应的capability已经有了
dev@ubuntu:~$ exec bash
dev@ubuntu:~$ cat /proc/$$/status | egrep 'Cap(Inh|Prm|Eff)'
CapInh: 0000000000000000
CapPrm: 00000000000000c0
CapEff: 00000000000000c0


#再试一次写map文件,成功了
dev@ubuntu:~$ echo '0 1000 100' > /proc/24126/uid_map
dev@ubuntu:~$ echo '0 1000 100' > /proc/24126/gid_map
dev@ubuntu:~$
#再写一次就失败了,因为这个文件只能写一次
dev@ubuntu:~$ echo '0 1000 100' > /proc/24126/uid_map
bash: echo: write error: Operation not permitted
dev@ubuntu:~$ echo '0 1000 100' > /proc/24126/gid_map
bash: echo: write error: Operation not permitted

#后续测试不需要CAP_SETUID了,将/bin/bash的capability恢复到原来的设置
dev@ubuntu:~$ sudo setcap cap_setgid,cap_setuid-ep /bin/bash
dev@ubuntu:~$ getcap /bin/bash
/bin/bash =

#--------------------------第一个shell窗口----------------------
#回到第一个窗口,id已经变成0了,说明映射成功
nobody@ubuntu:~$ id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)

#--------------------------第二个shell窗口----------------------
#回到第二个窗口,确认map文件的owner,这里24126是新user namespace中的bash
dev@ubuntu:~$ ls -l /proc/24126/
......
-rw-r--r-- 1 dev dev 0 7月  24 23:13 gid_map
dr-x--x--x 2 dev dev 0 7月  24 23:10 ns
-rw-r--r-- 1 dev dev 0 7月  24 23:13 uid_map
......

#--------------------------第一个shell窗口----------------------
#重新加载bash,提示有root权限了
nobody@ubuntu:~$ exec bash
root@ubuntu:~#

#0000003fffffffff表示当前运行的bash拥有所有的capability
root@ubuntu:~# cat /proc/$$/status | egrep 'Cap(Inh|Prm|Eff)'
CapInh: 0000000000000000
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff
#--------------------------第二个shell窗口----------------------
#回到第二个窗口,发现owner已经变了,变成了root
#目前还不清楚为什么有这样的机制
dev@ubuntu:~$ ls -l /proc/24126/
......
-rw-r--r-- 1 root root 0 7月  24 23:13 gid_map
dr-x--x--x 2 root root 0 7月  24 23:10 ns
-rw-r--r-- 1 root root 0 7月  24 23:13 uid_map
......
#虽然不能看目录里有哪些文件,但是可以读里面文件的内容
dev@ubuntu:~$ ls -l /proc/24126/ns
ls: cannot open directory '/proc/24126/ns': Permission denied
dev@ubuntu:~$ readlink /proc/24126/ns/user
user:[4026532464]


#--------------------------第一个shell窗口----------------------
#和第二个窗口一样的结果
root@ubuntu:~# ls -l /proc/24126/ns
ls: cannot open directory '/proc/24126/ns': Permission denied
root@ubuntu:~# readlink /proc/24126/ns/user
user:[4026532464]

#仍然不能访问/root目录,因为他的拥有着是nobody
root@ubuntu:~# ls -l /|grep root
drwx------   3 nobody nogroup  4096 7月   8 18:39 root
root@ubuntu:~# ls /root
ls: cannot open directory '/root': Permission denied

#对于原来/home/dev下的内容,显示的owner已经映射过来了,由dev变成了新namespace中的root,
#当前root用户可以访问他里面的内容
root@ubuntu:~# ls -l /home
drwxr-xr-x 8 root root 4096 7月  21 18:35 dev
root@ubuntu:~# touch /home/dev/temp01
root@ubuntu:~#

#试试设置主机名称
root@ubuntu:~# hostname container001
hostname: you must be root to change the host name
#修改失败,说明这个新user namespace中的root账号在父user namespace里面不好使
#这也正是user namespace所期望达到的效果,当访问其他user namespace里的资源时,
#是以其他user namespace中的相应账号的权限来执行的,
#比如这里root对应父user namespace的账号是dev,所以改不了系统的hostname


标签:container001,Namespace,ubuntu,namespace,dev,Linux,root,bash
From: https://blog.51cto.com/u_15128872/7542682

相关文章

  • Metasploitable-Linux靶机配置
    Metasploitable和kali是课上下载好的。直接打开进入欢迎界面用默认账号登录了之后,修改root密码(sudopasswdroot),登录root账号,查看本机IP(ifconfig)切换到kali,把kali的IP改为192.168.72.129,且能ping通192.168.72.131在kali的浏览器中输入靶机的IP,可以看到靶机的界面......
  • linux中grep与find的区别,Linux三剑客【grep、sed和awk】
    在使用linux时,经常需要进行文件查找。其中查找的命令主要有find和grep。两个命令是有区的。区别:(1)find命令是根据文件的属性进行查找,如文件名,文件大小,所有者,所属组,是否为空,访问时间,修改时间等。(2)grep是根据文件的内容进行查找,会对文件的每一行按照给定的模式(patter)......
  • linux第四周技术博客
    这周我们学习了Linux的文件的操作之前我们已经在/home目录中创建了/swxy目录我们首先来学习touch命令,用户可以通过touch命令来创建一个空白文件,也可以设置文件属性cd/home/swxytouchabc.txtecho‘abc'>>abc.txt我们可以看到在swxy这个目录中已经创建了abc.txt这个文件,......
  • 1-Linux操作系统 的介绍和安装教程
    一、Linux的介绍1)常见的操作系统Windows,它微软公司开发的一款桌面操作系统(闭源系统)。版本有dos、win98、winNT、winXP、win7、winvista、win8、win10。服务器操作系统:winserver2003、winserver2008、winserver2012。Mac,苹果公司开发的一款操作系统(闭源系统),目前最......
  • linux网络配置
    linux网络配置一:网络配置的相关概念1:网关网关就是连接不同网段的,可以让不同网段的主机进行通信,就相当于是一个网段鹅出口,必须通过这个出口出去,才能与外界进行通信,在linux中有默认的网关,NAT模式中默认的网关就是以.2结尾比如Ip为192.168.10.10它的网关就是192.168.10.2......
  • 《Linux命令行与shell脚本编程大全.第3版》电子书PDF+源代码
    精通Linux命令行与shell脚本编程,尽在本书中本书是关于Linux命令行和shell命令的全面参考资料,涵盖详尽的动手教程和实际应用指南,并提供相关参考信息和背景资料,带你从Linux命令行基础入手,直到写出自己的shell。时隔四年后的这一版本,针对Linux的新特性和实践,进行了全面更新:使用......
  • Linux用户和组命令
    用户和组配置文件1.用户:Linux基于用户身份对资源进行控制用户账号root用户程序用户:不需要登录系统,服务于应用程序,维护系统的运行普通用户:可以登录系统的一般用户组账号基本组(私有组):当用户创建文件和文件夹时,默认的属组,只能有一个附加组(公共组):用户可以有多个附加组UID和GIDUI......
  • linux 中字符串处理函数 ${i%%.*}
     001、[root@pc1test2]#ls[root@pc1test2]#str1="ab.cd_kk.mn_jjy"##测试字符串[root@pc1test2]#echo${str1%%.*}##从左侧开始,删除.号右侧所有的内容ab[root@pc1test2]#echo${str1%.*}##从左侧开始,删除最后一个.右侧的内容ab.......
  • Linux环境下sentence-transformers 之 all-MiniLM-L6-v2模型安装与使用
    好记性不如烂笔头系列一、背景:1、之前使用chatgpt接口生成embeddings的向量维度为1536维,数据库中占用较大,所以找寻低维度的向量生成方法,减少数据占用2、在huggingface上发现all-mpnet-base-v2及all-MiniLM-L6-v2两个模型不错,前者会生成768维的向量,后者会生成384维的向量 二......
  • 6、linux命令之`cp`与`scp`
    目录linux命令之cp与scp1、cp2、scp3、参数linux命令之cp与scpcp与scp命令的都是复制文件或目录,区别就是,cp只能在自己服务内进行copy,而scp可以跨服务器进行copy。1、cp语法:cp[srcFile][tgtFile]示例:cp/u01/dmps/*.dmp/u01/dmps/2、scp将本地的/home/testuser/test.......