Linux Namespaces学习

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

这样能看到所有进程信息了

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

上次更新 2021-01-28