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。
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.htmlpivot_root new_root put_oldc函数: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 /proct参数是指定文件系统类型,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------------|--------------------------|--------
| ----------->>>>>>----------| |
----------------------------------------------------------
其实就是几个时间节点:
PREROUTING包进来时FORWARD转发INPUT进入本地OUTPUT本地生成的包开始路由之前POSTROUTING包发出去之前
表概念#
filternatmangleraw
每个表可能会关注不同的链
现在往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
–