ILD

Linux namespace 学习第一篇
作者:Yuan Jianpeng 邮箱:yuanjianpeng@xiaomi.com
发布时间:2022-1-20 站点:Inside Linux Development

初识namespace

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结束。


namespace场景下的procfs

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。



file descriptor referring to a 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]

https://stackoverflow.com/questions/70713514/ctrlc-can-not-interrupt-process-in-new-namespace-by-sudo-unshare-fp-bash


[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


Copyright © linuxdev.cc 2017-2024. Some Rights Reserved.