Linux Namespaces学习


  • namespaces是linux内核的一个功能。顾名思义可以划分各种空间,提供某种程度的隔离,在系统各种“资源”下划分空间(组)。

  • 例如pid的ns。pid一般是从1开始,形成一颗树,pid是唯一的。 ns可以开辟一个新的空间,里面又是从1开始形成一颗新的树,两棵树存在相同的pid,但不会相互干扰。

  • https://en.wikipedia.org/wiki/Linux_namespaces https://man7.org/linux/man-pages/man7/namespaces.7.html

  • 所有空间类型: | Namespace | Flag | Page | Isolates | | :———— | :———— | :———— | :———— | | Cgroup | CLONE_NEWCGROUP | cgroup_namespaces(7) | Cgroup root directory | | IPC | CLONE_NEWIPC | ipc_namespaces(7) | System V IPC, POSIX message queues | | Network | CLONE_NEWNET | network_namespaces(7) | Network devices, stacks, ports, etc. | | Mount |CLONE_NEWNS |mount_namespaces(7) | Mount points | | PID | CLONE_NEWPID | pid_namespaces(7) | Process IDs | |Time |CLONE_NEWTIME | time_namespaces(7) | Boot and monotonic clocks | | User | CLONE_NEWUSER | user_namespaces(7) | User and group IDs | | UTS | CLONE_NEWUTS | uts_namespaces(7) | Hostname and NIS domain name |

  • linux默认给每个类型起一个ns。其他应用可根据需要起新的ns,或者加入某个ns。

  • ns生命周期 通常随ns里最后一个进程结束而结束。但也有几个例外情况。见https://man7.org/linux/man-pages/man7/namespaces.7.html

  • namespaces是容器技术的一个基础。还有一个是cgroups,可精细地限制各种资源入cpu、内存、网络等。 docker主要是利用这两个功能实现的。


查看当前进程的ns


   xc@test:~/temp/ctn$ ls -l /proc/$$/ns
   total 0
   lrwxrwxrwx 1 xc xc 0 Aug 29 16:59 cgroup -> 'cgroup:[4026531835]'
   lrwxrwxrwx 1 xc xc 0 Aug 29 16:59 ipc -> 'ipc:[4026531839]'
   lrwxrwxrwx 1 xc xc 0 Aug 29 16:59 mnt -> 'mnt:[4026531840]'
   lrwxrwxrwx 1 xc xc 0 Aug 29 16:59 net -> 'net:[4026531992]'
   lrwxrwxrwx 1 xc xc 0 Aug 29 16:59 pid -> 'pid:[4026531836]'
   lrwxrwxrwx 1 xc xc 0 Aug 29 16:59 pid_for_children -> 'pid:[4026531836]'
   lrwxrwxrwx 1 xc xc 0 Aug 29 16:59 user -> 'user:[4026531837]'
   lrwxrwxrwx 1 xc xc 0 Aug 29 16:59 uts -> 'uts:[4026531838]'

方括号里的是ns的id。两个进程的id相同就代表该类型在同一个ns。


unshare命令

  • unshare可以指定一个命令,同时新起ns,让命令在新的ns里运行。 详见unshare -h

    sudo unshare -u bash

    unshare运行一个bash,u参数新起uts空间,再查看当前进程ns,会发现uts空间发生改变。4026531838变成4026533018

root@test:/home/xc# ls -l /proc/$$/ns
total 0
lrwxrwxrwx 1 root root 0 Aug 29 17:23 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Aug 29 17:23 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 Aug 29 17:23 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 root root 0 Aug 29 17:23 net -> 'net:[4026531992]'
lrwxrwxrwx 1 root root 0 Aug 29 17:23 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Aug 29 17:23 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Aug 29 17:23 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Aug 29 17:23 uts -> 'uts:[4026533018]'

clone函数

  • https://man7.org/linux/man-pages/man2/clone.2.html

  • int clone(int (*fn)(void *), void *stack, int flags, void *arg); 代码层面创建一个新进程,与fork类似。但是能提供更多控制能力。比如能指定父子进程是否共享虚拟地址空间等等。 可指定需要新建的空间,与上述空间类型对应。


可以用代码模拟一下容器

#include <iostream>
#include <sys/wait.h> // waitpid
#include <unistd.h> // execv

const int STACK_SIZE = 1024 * 1024;

char STACK[STACK_SIZE];

char* const args[] = {"/bin/bash", NULL};

int fun(void *)
{
   std::cout<<"fun"<<std::endl;

   auto result = execv("/bin/bash", args); // 容器内执行bash

   return 0;
}

int main()
{
   std::cout<<"main"<<std::endl;

    auto ctn_pid = clone(fun, STACK + STACK_SIZE, SIGCHLD | CLONE_NEWUTS , NULL); // 新建CLONE_NEWUTS空间

    std::cout<<"pid = "<<ctn_pid<<std::endl; // 子进程pid

    waitpid(ctn_pid, NULL, 0); // 等待容器进程结束

   std::cout<<"parent over"<<std::endl;
}

这是个小框架。起一个新进程,只新建CLONE_NEWUTS空间。子进程里运行bash,相当于用bash跟容器环境交互。


UTS空间


  • https://man7.org/linux/man-pages/man7/uts_namespaces.7.html

  • 在新的CLONE_NEWUTS空间里用hostname更改主机名不会影响原来的主机名。 新建时默认hostname同原主机

运行程序后输入

   hostname // 显示当前
   hostname wtf // 修改
   hostname // 显示当前。变为wtf。
  • 再连上一个ssh终端,查看hostname发现没变。 如果把CLONE_NEWUTS这个flag去掉,即不新建uts空间,会看到容器里改hostname后实际hostname会变。

下面会在这个代码框架基础上做实验。


PID空间


  • https://man7.org/linux/man-pages/man7/pid_namespaces.7.html

  • pid空间可实现不同空间的进程可以有相同的pid,可挂起/恢复空间里的进程等。 每个pid空间有个父空间,所有空间是一个树形结构。

  • 可加上CLONE_NEWPID新建pid空间,运行ps aux或top看结果。

  • 发现问题:做了pid新空间ps和htop这些命令还是会显示所有进程,并且是实际pid。如何做成docker一样只显示内部pid? ps查的是/proc目录里的进程信息,只能识别mount者的pid空间。 挂载proc系统的进程是在容器外部的,所以看到的是容器外部主机的进程表。 必须要新起mount空间,在容器内部重新mount一下proc系统才行。


MOUNT空间

  • http://manpages.ubuntu.com/manpages/bionic/man7/mount_namespaces.7.html

  • mount空间可以使不同空间的挂载点相互隔离。 每个进程的挂载信息见 /proc/[pid]/mounts /proc/[pid]/mountinfo /proc/[pid]/mountstats 如果用clone,子进程默认会复制父进程的挂载信息。 如果用unshare,子进程默认会复制调用者前一个挂载空间的信息。


Root Filesystem/Root Directory

  • http://www.linfo.org/root_filesystem.html 根文件系统/根目录。类unix系统都有个跟目录,包含/boot, /dev, /etc, /bin等等基本文件

  • 下载一个最小rootfs看一下 http://dl-cdn.alpinelinux.org/alpine/v3.10/releases/x86_64/alpine-minirootfs-3.10.1-x86_64.tar.gz

  • 解压后可看到linux典型的目录结构 bin/  dev/  etc/  home/  lib/  media/  mnt/  opt/  proc/  run/  sbin/  srv/  sys/  tmp/  usr/  var/

  • 很多docker镜像都是基于某个linux最小系统。现在用这个rootfs来模拟一下。

  • pivot_root命令/接口 https://man7.org/linux/man-pages/man2/pivot_root.2.html

  • pivot_root new_root put_old c函数:int pivot_root(const char *new_root, const char *put_old); 把根文件系统的挂载设为put_old目录,把new_root设为当前根文件系统挂载。

   sudo unshare -m bash      // 新mount空间
   mount --bind alpine ./alpine
   cd alpine                 // 进入alpine
   mkdir put_old             // 创建put_old目录
   pivot_root . put_old      // 此时根目录已经变为alpine目录。终端命令的路径可能也发生改变。比如直接输ls可能找不到命令,得用/bin/ls。
                          // 原来得根目录映射到了put_old。ls put_old会显示根目录。
   cd /                      // 此时进入/根目录,看到的会是原先alpine目录。
  • 这时用ps是看不到信息的,因为已经在新的根目录,/proc是空的,ps读不到信息。 需要把proc系统mount到/proc才行。 mount -t proc proc /proc 这样能看到所有进程信息了

  • 思考总结一下。linux应该是自己维护一个proc系统,开机mount到/proc目录,ps就只管读目录里的数据。 所以容器里得手动mount一下proc,相当于模拟一下开机流程。查资料可证实。

  • ps相关信息 https://unix.stackexchange.com/questions/262177/how-does-the-ps-command-work https://man7.org/linux/man-pages/man1/ps.1.html https://en.wikipedia.org/wiki/Procfs#Linux https://www.kernel.org/doc/Documentation/filesystems/proc.txt

  • 再看mount命令 https://man7.org/linux/man-pages/man8/mount.8.html

  • 标准形式 mount -t type device dir 实际非常复杂

  • mount -t proc proc /proc t参数是指定文件系统类型,proc指特殊的proc系统。第二个proc据悉如果是proc系统的话会忽略,可以填任何字符。最后挂载目为/proc。

把命令转为代码:

   #include <iostream>
   #include <sys/wait.h> // waitpid
   #include <unistd.h> // execv
   #include <sys/mount.h> // mount
   #include <syscall.h> // syscall
   #include <sys/stat.h> // mkdir

   const int STACK_SIZE = 1024 * 1024;

   char STACK[STACK_SIZE];

   char* const args[] = {"/bin/sh", NULL};

   void init_mns()
   {
       auto put_old = "put_old";

       int result = -1;

       // pivot_root有一系列限制。详情见文档。这里mount一下确保不是MS_SHARED属性。
       // 否则pivot_root会报EINVAL Invalid argument,非常恶心。
       result = mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL);
       std::cout<<"mount result "<<result<<std::endl;
       if(result == -1)
       {
           std::cout<<"errno  "<<errno<<std::endl;
           exit(1);
       }

       // bind mount
       // https://unix.stackexchange.com/questions/198590/what-is-a-bind-mount
       result = mount("alpine", "alpine", "ext4", MS_BIND, "");
       std::cout<<"mount result "<<result<<std::endl;
       if(result == -1)
       {
           std::cout<<"errno  "<<errno<<std::endl;
           exit(1);
       }

       result = chdir("alpine");
       std::cout<<"chdir result "<<result<<std::endl;
       if(result == -1)
       {
           std::cout<<"errno  "<<errno<<std::endl;
           exit(1);
       }

       result = mkdir(put_old, 0777);
       std::cout<<"mkdir result "<<result<<std::endl;
       if(result == -1)
       {
           std::cout<<"errno  "<<errno<<std::endl;
           //exit(1);
       }

       // pivot_root
       result = syscall(SYS_pivot_root, ".", put_old);
       //result = pivot_root(".", put_old);
       std::cout<<"SYS_pivot_root result "<<result<<std::endl;
       if(result == -1)
       {
           std::cout<<"errno  "<<errno<<std::endl;
           exit(1);
       }

       result = chdir("/");
       std::cout<<"chdir result "<<result<<std::endl;
       if(result == -1)
       {
           std::cout<<"errno  "<<errno<<std::endl;
           exit(1);
       }

       result = mkdir("/proc", 0555);
       std::cout<<"mkdir result "<<result<<std::endl;
       if(result == -1)
       {
           std::cout<<"errno  "<<errno<<std::endl;
           //exit(1);
       }

       // mount proc系统到/proc目录
       result = mount("proc", "/proc", "proc", 0, "");
       std::cout<<"mount result "<<result<<std::endl;
       if(result == -1)
       {
           std::cout<<"errno  "<<errno<<std::endl;
           exit(1);
       }

       // umount 清理老的根目录
       result = umount2(put_old, MNT_DETACH);
       std::cout<<"umount2 result "<<result<<std::endl;
       if(result == -1)
       {
           std::cout<<"errno  "<<errno<<std::endl;
           exit(1);
       }

       result = rmdir(put_old);
       std::cout<<"rmdir result "<<result<<std::endl;
       if(result == -1)
       {
           std::cout<<"errno  "<<errno<<std::endl;
           exit(1);
       }
   }

   int fun(void *)
   {
       std::cout<<"fun"<<std::endl;

       init_mns();

       auto result = execv("/bin/sh", args); // 容器内执行sh。不能再执行bash了,因为这个最小系统没有bash。

       return 0;
   }

   int main()
   {
       std::cout<<"main"<<std::endl;

       auto ctn_pid = clone(fun, STACK + STACK_SIZE, SIGCHLD | CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS, NULL); // 新建uts pid mount空间

       std::cout<<"pid = "<<ctn_pid<<std::endl; // 子进程pid

       waitpid(ctn_pid, NULL, 0); // 等待容器进程结束

       std::cout<<"parent over"<<std::endl;
   }

运行后发现跟容器差不多了。ps和top只显示容器内的进程。


网络空间

  • https://man7.org/linux/man-pages/man7/network_namespaces.7.html https://man7.org/linux/man-pages/man4/veth.4.html

  • 网络空间隔离了网络相关的系统资源:ip协议栈、路由表、防火墙信息、/proc/net、/sys/class/net、/proc/sys/net、端口等等。 一个网络设备只能存在于唯一的网络空间。

veth

  • 一对虚拟网络设备(virtual network(veth) device pair)(虚拟网卡)可提供类似pipe的机制来让不同的网络空间互通。 当一个网络空间死掉时,它包含的虚拟设备也会被销毁。

   __________      __________
   |         |     |         |
   |  容器v1 |     |  容器v2 |
   |         |     |         |
   |——v1_in——|     |——v2_in——|
        |               |
   |——v1_out——————————v2_out——|
   |        \       /         |
   |          bridge          |
   |            |      宿主机 |
   |———————————eth0———————————|
                |
                |
             外部网络
  • 这是个常见的容器网络结构⬆️ 每个容器新起一个网络空间、新起一对veth,in端连接容器空间,out端连接宿主机空间的一个网桥。 这样容器之间就可以通信。 再把网桥联通宿主机的网络设备,容器就可以与外网互联了。


网桥(linux bridge)


  • 网桥是一个链接层的虚拟设备,用来联通各种网络设备。

  • 下面用shell命令配置容器网络 注意docker会大改iptables配置。不熟悉的话会很耗时。最好找个干净的机器做实验。

  • ip命令(iproute2) https://www.man7.org/linux/man-pages/man8/ip.8.html https://access.redhat.com/sites/default/files/attachments/rh_ip_command_cheatsheet_1214_jcs_print.pdf

  • ip netns 网络空间管理 https://www.man7.org/linux/man-pages/man8/ip-netns.8.html

  • ip link https://www.man7.org/linux/man-pages/man8/ip-link.8.html

  • 查看网络接口列表 sudo ip link list

  • 查看网络空间 sudo ip netns list 默认为空

  • 创建ns sudo ip netns add v1 sudo ip netns add v2 sudo ip netns list 可看到新的空间

  • 创建两对veth sudo ip link add v1_out type veth peer name v1_in sudo ip link add v2_out type veth peer name v2_in ip link list 可看到多了v1_out和v1_in,注意他们的形式:v1_in@v1_out和v1_out@v1_in。

  • 另起两个终端。在新的空间里执行bash(进入新的网络空间) sudo ip netns exec v1 bash sudo ip netns exec v2 bash

  • 在新空间里执行ip link list,看到只有一个lo接口。

  • 在宿主空间里分配in端给容器 sudo ip link set v1_in netns v1 sudo ip link set v2_in netns v2 sudo ip link list 可以看到v1_in,v2_in没有了

  • 容器空间ip link list,可以看到出现了v1_in,v2_in

  • 这些接口默认都是DOWN的状态 down的状态下接口无法工作,比如回环设备lo默认是关的,ping 127.0.0.1不通。

  • 启动lo ip link set dev lo up 发现127.0.0.1可以ping通

  • 容器空间里给in端添加地址 ip addr add 192.168.2.1/24 dev v1_in ip addr add 192.168.2.2/24 dev v2_in 24代表掩码255.255.255.0(二进制从头开始24个1)

  • 容器空间启动v1_in v2_in ip link set dev v1_in up ip link set dev v2_in up 这时ifconfig可以看到v1_in和v2_in的详情

  • 列出所有网桥 sudo ip link list type bridge

  • 添加网桥vbr sudo ip link add vbr type bridge

  • 把out端挂上vbr sudo ip link set v1_out master vbr sudo ip link set v2_out master vbr

  • 宿主空间启动out端 sudo ip link set dev v1_out up sudo ip link set dev v2_out up

  • 给vbr一个ip sudo ip addr add 192.168.2.100/24 brd + dev vbr

  • 启动 sudo ip link set dev vbr up

  • 从v1容器ping 192.168.2.2 可以ping通

  • 如果主机上装了docker,会改路由规则,默认会drop。ping不通 给vbr添加一下规则后可ping通。 sudo iptables -A FORWARD -i vbr -j ACCEPT

  • 现在容器相互可ping通,容器和宿主机可ping通。 但是容器中 ping 8.8.8.8 ping 163.com 不通。连不到外网

  • 容器里查看路由 ip ro 192.168.2.0/24 dev v2_in proto kernel scope link src 192.168.2.1 没有到外界的路由。 ip route命令详解 http://linux-ip.net/html/tools-ip-route.html

  • 两个容器里添加default路由,默认往vbr的地址192.168.2.100转。 ip route add default via 192.168.2.100

  • 到此还是不行。因为vbr跟外界不通。 需要用iptables创建规则,把vbr跟外界联通。


Iptables

https://linux.die.net/man/8/iptables

链概念

     ------kernel----------------------------------------------
     |                                                        |
   --|-->PREROUTING---->选择路径---->FORWARD---->POSTROUTING---|----->
     |                     |                          ^       |
     |                     |                          |       |
     |                   INPUT                      OUTPUT    |
     |                     |                          |       |
     ----------------------|--------------------------|--------
                           |                          |
     ------user------------|--------------------------|--------
     |                     ----------->>>>>>----------|       |
     ----------------------------------------------------------

其实就是几个时间节点:

  1. PREROUTING 包进来时

  2. FORWARD 转发

  3. INPUT 进入本地

  4. OUTPUT 本地生成的包开始路由之前

  5. POSTROUTING 包发出去之前

表概念

  1. filter

  2. nat

  3. mangle

  4. raw

每个表可能会关注不同的链

现在往nat表添加规则。把新建的网桥vbr做一个nat转换。从而跟外网联通。

sudo iptables -t nat -A POSTROUTING -s 192.168.2.100/24 -j MASQUERADE

其中: -t nat修改nat表 -A POSTROUTING往POSTROUTING链追加规则 -s 192.168.2.100/24源地址。这里的源定为vbr的地址 -j MASQUERADE规则的目的。

MASQUERADE,自动识别目标地址,不用手动指定(貌似) http://www.billauer.co.il/ipmasq-html.html https://www.oreilly.com/openbook/linag2/book/ch11.html

sudo iptables -t nat -L 查表可看到

   Chain POSTROUTING (policy ACCEPT)
   target     prot opt source               destination         
   MASQUERADE  all  --  192.168.2.0/24       anywhere 

但是网络还是不行,因为没开启转发。

cat /proc/sys/net/ipv4/ip_forward 看到是0

sudo sysctl -w net.ipv4.ip_forward=1

开启后可以ping外网了

到此网络就通了


代码实现 暂无


Python镜像


现在有网了,想试下运行pip。

怎么做一个python最小rootf?

之前用的是alpine linux系统。 https://alpinelinux.org/about/

这是个非常流行的最小linux系统,docker镜像大小只有几Mb。只包含busybox等小工具。

那么自然很多软件都会在这个镜像基础上做自己的镜像。

对于python官方镜像,有alpine、buster、slim等等,就是区分了几种常用的基础镜像。 buster和slim都是从Debian镜像衍生出来。

python的alpine镜像只有40多mb。如果要安装其他软件、用到apt等等,那只好用debian基础的,1G多。

拉取镜像 sudo docker pull python:3.8.5-alpine

创建容器 sudo docker create python:3.8.5-alpine

docker export可以导出镜像到tar包 sudo docker export -o python-alpine.tar 8ce8

解压 sudo tar -xvf python-alpine.tar -C /home/xc/python-alpine


镜像格式。存储格式。待续

fs 存储 ufs 层级结构 存储驱动:overlay2


参考

http://ifeanyi.co/posts/linux-namespaces-part-1/ https://platform9.com/blog/container-namespaces-deep-dive-container-networking/ https://rancher.com/learning-paths/introduction-to-container-networking/ https://dev.to/polarbit/how-docker-container-networking-works-mimic-it-using-linux-network-namespaces-9mj https://ops.tips/blog/using-network-namespaces-and-bridge-to-isolate-servers/ http://linux-ip.net/html/tools-ip-route.html https://blogs.igalia.com/dpino/2016/04/10/network-namespaces/ https://www.cnblogs.com/bakari/p/10443484.html http://www.billauer.co.il/ipmasq-html.html


brctl 命令 brctl已过时。 见https://www.man7.org/linux/man-pages/man8/brctl.8.html

列出所有网桥 sudo brctl show

添加网桥 sudo brctl addbr wtfbr

启动网桥 sudo ip link set dev wtfbr up

把宿主机的eth0挂上网桥。我第一次尝试时服务器崩溃重启。 sudo brctl addif wtfbr eth0

把宿主机的v1_out挂上网桥 sudo brctl addif wtfbr v1_out