Xrouter平台支持X86上运行,来加快开发速度。最开始没有使用namespace,而只是使用chroot,切换到rootfs,然后拉起相关业务:
$ chroot . /bin/sh -c "/etc/rcS S boot; /bin/sh -i; /etc/rcS K shutdown"
但是有个缺点,就是没有做namespace隔离。在终端上,ps或者mount,可以看到host上所有的进程和挂载点。于是搜索namespace相关的工具,发现了unshare。
unshare - run program with some namespaces unshared from parent
unshare工具是unshare系统调用的同名工具。它支持在一些新的namespace运行程序。可以unshare的namespace有:
mount namespace
mount和unmount将不影响系统的其它部分。除非文件系统被显式地标记为shared(mount --mark-shared)。
UTS namespace
设置hostname和domainname不影响系统的其它部分。
IPC namespace
POSIX message queues,System V message queues, semaphore sets,shared memory segments
有独立的命令空间。
network namespace
IPv4 IPv6 stacks, IP routing tables, firewall rules, /proce/net /sys/class/net目录树,sockets等。
有独立的命名空间
PID namespace
独立的pid namespace,新的pid namespace的第一个进程id为1。
cgroup namespace
进程有一个虚拟的/proc/self/cgroup
user namespace
进程有一个独立的pid 和 gid。
这里想不研究unshare的用法,只是简单带过,于是使用下面的方法,来隔离mount和pid。
$ unshare -mfp chroot . /bin/sh -c "/etc/rcS S boot; /bin/sh -i; /etc/rcS K shutdown"
-m 隔离mount namespace
-p 隔离pid namespace
-f
fork子进程执行命令。unshare通常使用exec直接用命令替换自己。但是隔离pid的时候,和其它namespace不同。当前进程不会在新的pid namespace,因为进程不能修改自己的pid,而是在fork的时候,才创建新的pid。
如果没有-f这个选项
$ sudo unshare -p /bin/bash
bash: fork: Cannot allocate memory
直接失败了。流程是这样的,unshare调用unshare(CLONE_NEWPID)创建新的pid namespace,然后exec执行bash。此时没有fork,bash仍然在旧的namespace,bash内部会fork一些进程来执行任务,fork第一个进程,此进程在新的namespace, pid为1。这个进程会退出,此时1号进程没了,bash,在fork第二个的时候,由于没有1号进程,而出现上述错误。
如果使用busybox编译的sh。则能成功,因为它不创建子进程来执行一些任务。
$ sudo unshare -p ./bin/sh
# ls
bin boot_on_host dev etc lib lib64 linuxrc proc run sbin sys tmp usr web
# ls
./bin/sh: can't fork: Cannot allocate memory
可以看到第一次ls成功了,它是1号进程。第二次ls失败了,因为第一个ls是1号进程退出了。
如果查看./bin/sh的pid namespace,可以看到自己和儿子是不同的pid namespace。
# ls /proc/3402725/ns/pid* -l
lrwxrwxrwx 1 root root 0 Jan 19 19:54 /proc/3402725/ns/pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 19 19:54 /proc/3402725/ns/pid_for_children -> 'pid:[4026532163]'
但是-f有个缺陷,就是unshare命令会忽略SIGINT和SIGQUIT,导致sh执行命令后,命令无法通过CTRL+C结束。
1 创建一个pid namespace
$ sudo unshare -fp ./bin/sh
# ps
PID TTY TIME CMD
3402631 pts/4 00:00:00 sleep
3402820 pts/4 00:00:00 sudo
3402821 pts/4 00:00:00 unshare
3402822 pts/4 00:00:00 sh
3402823 pts/4 00:00:00 ps
# cat /proc/self/status
Name: cat
Umask: 0022
State: R (running)
Tgid: 3402857
Ngid: 0
Pid: 3402857
没有使用chroot,和主机相同的文件系统。所有/proc还是外面默认的/proc。ps是通过/proc来分析进程。
所以看到的进程的pid还是外面默认的pid。
2 使用-m选项:
$ sudo unshare -mfp ./bin/sh
# ps
PID TTY TIME CMD
3402631 pts/4 00:00:00 sleep
3402984 pts/4 00:00:00 sudo
3402985 pts/4 00:00:00 unshare
3402986 pts/4 00:00:00 sh
3402987 pts/4 00:00:00 ps
可以看到还是外面默认的procfs。可见创建新的mount namespace后,ps还是外面的pid。这是由于没有chroot,访问的还是外面的/proc。
3 创建一个pid namespace,然后mount一个procfs
为了使用新mount的proc,我们使用chroot。
$ sudo unshare -fp chroot . /bin/sh
/ # mount /proc
/ # ps
PID USER TIME COMMAND
1 root 0:00 /bin/sh
3 root 0:00 ps
可以看到 ps 看到 PID变成了自己pid namespace的pid,而且只能看到当前的pid namespace的进程。
在外面使用mount命令,能查看到此mount:
none on /work/git/Xrouter/staging/x86/rootfs/proc type proc (rw,relatime)
因为没有使用-m选项,是一个mount namespace。但是proc的内容已经变了,pid使用的是新的pid namepsace。
此时如果退出当前unshare,没有主动umount proc,那么此proc仍然处于挂载的状态。在外面使用mount命令还可以看到,并且可以使用umount卸载。
如果是-m选项挂载的mount,那么在mount namespace销毁的时候,会自动umount。由于是mount namespace隔离,销毁不销毁在外面一直都是看不到的。
4 在外面mount proc
/work/git/Xrouter/staging/x86/rootfs$ sudo unshare -fp ./bin/sh
#
在另外一个终端执行mount
/work/git/Xrouter/staging/x86/rootfs$ sudo mount -t proc none proc/
再在前面一个unshare终端ps
# ps
PID TTY TIME CMD
3402631 pts/4 00:00:00 sleep
3403059 pts/4 00:00:00 sudo
3403060 pts/4 00:00:00 unshare
3403061 pts/4 00:00:00 sh
3403065 pts/4 00:00:00 ps
可以看到没有pid 1,使用的是外面默认pid namespace proc的内容。
总结:
mount namespace只是影响此mount在外面的可见性。以及是否自动umount。而procfs里面的内容是由挂载进程的pid namespace决定的。谁挂载就使用谁的pid namespace。
这个知识点,是由net namespace引入学习到的。尝试使用net namespace,然后将eth1,加入到sh中。
使用下面的脚本:
1 2 3 4 5 6 7 | #!/bin/sh # run this script at rootfs dir ip netns add net0 ip link set eth1 netns net0 unshare --net= /run/netns/net0 -mfp chroot . /bin/sh -i ip netns del net0 |
根据man ip netns
ip netns add NAME - create a new named network namespace
If NAME is available in /var/run/netns/ this command creates a
new network namespace and assigns NAME.
根据unshare的帮助手册
-n, --net[=file]
Unshare the network namespace. If file is specified,
then a persistent namespace is created by a bind mount.
$ sudo ./boot_on_host
# ifconfig -a
lo Link encap:Local Loopback
LOOPBACK 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:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
没有eth1。
在外面的终端,也找不到eth1
$ ifconfig eth1
eth1: error fetching interface information: Device not found
eth1消失了。
而且在退出的时候,执行ip netns del net0报错了。
Cannot remove namespace file "/run/netns/net0": Device or resource busy
刚开始用这些命令的时候,对net namespace以及底层的内在原理不了解,很懵逼。于是开始分析。
这个过程比较曲折,现在直接将达到正确结果的分析道路。
首先strace ip netns add net0。
mkdir("/run/netns", 0755) = -1 EEXIST (File exists)
mount("", "/run/netns", 0x5653fd0a667f, MS_REC|MS_SHARED, NULL) = 0
openat(AT_FDCWD, "/run/netns/net0", O_RDONLY|O_CREAT|O_EXCL, 000) = 5
close(5) = 0
openat(AT_FDCWD, "/proc/self/ns/net", O_RDONLY|O_CLOEXEC) = 5
unshare(CLONE_NEWNET) = 0
mount("/proc/self/ns/net", "/run/netns/net0", 0x5653fd0a667f, MS_BIND, NULL) = 0
setns(5, CLONE_NEWNET) = 0
close(5) = 0
首先创建/run/netns,然后挂载一个tmpfs到/run/netns。ip netns使用此目录作为netns的工作目录。
然后创建/run/nets/net0文件。
接下来就是实际netns的工作了:
首先打开 /proc/self/ns/net,读取旧的net namespace到fd。
然后unshare(CLONE_NEWNET)创建新的net namespace。
最关键的一步来了:将/proc/self/ns/net挂载到/run/netns/net0。
最后调用sestns()将net namespace恢复成旧的net namespace。
根据 man 7 namesapces
每个进程都有一个 /proc/[pid]/ns/ 子目录,包含了setns维护的namespace,例如:
$ ls -l /proc/$$/ns
total 0
lrwxrwxrwx. 1 mtk mtk 0 Apr 28 12:46 cgroup -> cgroup:[4026531835]
lrwxrwxrwx. 1 mtk mtk 0 Apr 28 12:46 ipc -> ipc:[4026531839]
lrwxrwxrwx. 1 mtk mtk 0 Apr 28 12:46 mnt -> mnt:[4026531840]
lrwxrwxrwx. 1 mtk mtk 0 Apr 28 12:46 net -> net:[4026531969]
lrwxrwxrwx. 1 mtk mtk 0 Apr 28 12:46 pid -> pid:[4026531836]
lrwxrwxrwx. 1 mtk mtk 0 Apr 28 12:46 pid_for_children -> pid:[4026531834]
lrwxrwxrwx. 1 mtk mtk 0 Apr 28 12:46 user -> user:[4026531837]
lrwxrwxrwx. 1 mtk mtk 0 Apr 28 12:46 uts -> uts:[4026531838]
自己或其它进程打开上述目录的文件,将返回该进程对应namespace的描述符。只要描述符保持打开,那么这个namespace就存在,即使namespace中的所有进程都结束了。这个描述可以传递给setns()将当前进程的namespace设置为描述符所表述的namespace。
将/proc/[pid]/ns/下面的namespace文件bind mount到文件系统中的其它一个文件,将保持这个namespace alive,即使这个namespace中的所有进程都退出。而且mount到的这个文件本身变成一个namespace文件,其它进程可以打开它,来调用setns设置这个namespace。
Linux 3.7之前,这些文件是hard links。3.8之后,这些文件是symbolic link。如果namespace相同,那么
符号链接指向的文件的device id和inode相同。因此可以用stat比较stat.st_dev和stat.st_ino来确定是否是同一个namespace。符号链接的内容是一个包含namespace type和inode number的字符串:
$ readlink /proc/$$/ns/uts
uts:[4026531838]
注意bind mount到一个其它文件后,那个文件不是一个符号链接,只能通过stat来读取其device和inode。
如果创建一个软链接执行那个文件,这个软链接也不会像procfs那样显示一个包含type和inode的字符串。
$ sudo unshare --net=net0 bash
# ls -l net0
-r--r--r-- 1 root root 0 Jan 20 11:27 net0
# stat net0
File: net0
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: 4h/4d Inode: 4026532279 Links: 1
Access: (0444/-r--r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2022-01-20 11:27:14.535281301 +0800
Modify: 2022-01-20 11:27:14.535281301 +0800
Change: 2022-01-20 11:27:14.535281301 +0800
Birth: -
# ls /proc/self/ns/net -l
lrwxrwxrwx 1 root root 0 Jan 20 11:27 /proc/self/ns/net -> 'net:[4026532279]'
# ln -s net0 link_net0
# ls -l link_net0
lrwxrwxrwx 1 root root 4 Jan 20 11:28 link_net0 -> net0
学习到这里,上面的使用方法就有问题了:
1 ip netns创建一个net namespace。并且绑定到/run/netns/net0。
2 unshare也做了同样的工作,创建一个net namespace,并且绑定到/run/netns/net0
所以它们不是一个namespace,自然unshare起的namespace中看不到eth1。
3 ip netns del net0的时候,实际上是执行umount和unlink。
umount卸载了unshare的mount。但是还有一层mount,所以unlink的时候失败。
参考:
[1]
man 2 unshare
man 1 unshare
man 2 setns
man 7 namespaces
man 7 mount_namespaces
man 7 pid_namespaces
man 7 cgroup_namespaces
man 1 nsenter
man 8 lsns
man 2 ioctl_ns
[2]
[3]
https://unix.stackexchange.com/questions/449948/move-network-device-between-linux-network-namespaces
[4]
YY哥. Deep dive into Linux network namespace
https://hustcat.github.io/deep-dive-into-net-namespace/
https://hustcat.github.io/about/
[5] Container and virtualization tools. https://linuxcontainers.org/
[6]
Ralph Mönchmeyer.
Fun with veth-devices, Linux bridges and VLANs in unnamed Linux network namespaces – I
https://linux-blog.anracom.com/tag/unshare/
[7]
Namespaces in operation, part 7: Network namespaces
https://lwn.net/Articles/580893/
[8]
[PATCH 3/8] ns proc: Add support for the network namespace.
https://linux.kernel.narkive.com/0UyHPB9K/patch-3-8-ns-proc-add-support-for-the-network-namespace