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_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------------|--------------------------|--------
| ----------->>>>>>----------| |
----------------------------------------------------------
其实就是几个时间节点:
PREROUTING
包进来时FORWARD
转发INPUT
进入本地OUTPUT
本地生成的包开始路由之前POSTROUTING
包发出去之前
表概念#
filter
nat
mangle
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
–