• Linux Namespace


    Linux namespaces 介绍

    namespaces是Linux内核用来隔离内核资源的方式。通过namespaces可以让一些进程只能看到与自己相关的那部分资源。而其它的进程也只能看到与他们自己相关的资源。这两拨进程根本感知不到对方的存在。而它具体的实现细节是通过Linux namespaces来实现的。

    总结: Linux namespaces对系统进程进行轻量的虚拟化隔离。

    当前Linux内核只支持6中namespaces:

    ● mnt(mount points, filesystems)

    ● pid(process)

    ● net(network stack)

    ● ipc(System V IPC)

    ● uts(hostname)

    ● user(UIDs)

    下面是Linux Kernel版本迭代过程中对这6中namespaces的支持情况及对应的flag:

    最初打算对Linux内核支持10种namespaces,但是下面的4中没有实现:

    ● security namespace

    ● security keys namespaces

    ● device namespace

    ● time namespace

    接下来先介绍namespace的API,然后在针对Linux内核现在支持的6中namespace分别进行介绍。

    代码测试环境:ubuntu20.04.2,kernel版本:5.4.0-182-generic

    Namespaces API 介绍

    下面3个系统调用API会被用于namespaces:

    ● clone(): 用于创建新的进程同时创建新的namespaces。并且新的进程会被attach到新的namespace里面。

    1. int clone(int (*fn)(void *), void *child_stack,
    2. int flags, void *arg, ...
    3. /* pid_t *ptid, void *newtls, pid_t *ctid */ );

    ● 参数child_func传入子进程运行的程序主函数。

    ● 参数child_stack传入子进程使用的栈空间

    ● 参数flags表示使用哪些CLONE_*标志位

    ● 参数args则可用于传入用户参数

    ClONE_NEW* flag有20多被包含在include/linux/sched.h头文件中。

    ● unshare(): 不会创建新的进程,但是会创建新的namesapce并把当前的进程attach到该namespace里面。

    int unshare(int flags);
    

    ● setns(): 将进程attach到一个已经存在的namespace里面。

    int setns(int fd, int nstype);
    

    ● 参数fd表示我们要加入的namespace的文件描述符。如:/proc/[pid]/ns下面对应的文件描述符。

    ● 参数nstype让调用者可以去检查fd指向的namespace类型是否符合我们实际的要求。如果填0表示不检查。

    namespace 实践

    为了最好的体验还是在 Linux 内核 3.8 以上的系统上进行(这里使用的 Ubuntu 20.04.2kernel版本:5.4.0-182-generic)。为什么不用 docker for windows 或者 docker for mac 呢?因为这两个其实还是是在 linux 虚拟机上运行 docker 的,docker for windows 需要将 linux 虚拟机装在开启 hyper-v 的 win10 专业版上,而 docker for mac 使用通过 HyperKit 运行 linux 虚拟机。为了方便,使用 c语言代码 来演示循序渐进的达到 Docker 的体验。

    UTS Namespace

    UTS namespace提供了主机名和域名的隔离,这样每一个容器就可以拥有独立的主机名和域名,在网络上可以被视为一个独立的节点而非宿主机上的一个进程。

    下面让我们来看下UTS的隔离效果,测试代码如下:

    1. #define _GNU_SOURCE
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #define STACK_SIZE (1024*1024)
    9. static char child_stack[STACK_SIZE];
    10. char* const child_args[] = {
    11. "/bin/bash",
    12. NULL
    13. };
    14. int child_main(void* args){
    15. printf("in child process!\n");
    16. sethostname("changed namespace", 12);
    17. execv(child_args[0], child_args);
    18. return 1;
    19. }
    20. int main(){
    21. printf("program begin: \n");
    22. int child_pid = clone(child_main, child_stack + STACK_SIZE, SIGCHLD|CLONE_NEWUTS, NULL);
    23. waitpid(child_pid, NULL, 0);
    24. printf("quit\n");
    25. return 0;
    26. }

    主要是main中在调用clone函数创建新进程及新namespace的时候,传递了CLONE_NEWUTS flag,用于对主机名和域名的隔离。

    如果没有gcc需要先安装gcc环境

    编译并运行程序会发现主机名发生了变化。

    1. root@double:~# gcc -Wall uts.c -o uts && ./uts
    2. program begin:
    3. in child process!
    4. root@changed name:~# hostname
    5. changed name
    6. root@changed name:~# exit
    7. exit
    8. quit

    每个容器的主机名不同就是使用UTS Namespace机制实现的。

    IPC Namespace

    容器中进程间的通信采用的方式包括: 信号量消息队列共享内存。与虚拟机不同的是,容器内部进程间通信对宿主机来说,实际上是具有相同的PID namespace中的进程间通信,因此需要一个而唯一的标识符来进行区别。申请IPC资源就申请了这样一个全局唯一的32位ID,所以IPC namespace中实际上包含了系统IPC标识符以及实现POSIX消息队列的文件系统。在同一个IPC namespace下的进程彼此可见,而与其他的IPC namespace下的进程则互相不可见。

    下面我们来看下IPC的隔离效果,测试代码如下:

    1. #define _GNU_SOURCE
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #define STACK_SIZE (1024*1024)
    9. static char child_stack[STACK_SIZE];
    10. char* const child_args[] = {
    11. "/bin/bash",
    12. NULL
    13. };
    14. int child_main(void* args){
    15. printf("in child process!\n");
    16. sethostname("changed namespace", 12);
    17. execv(child_args[0], child_args);
    18. return 1;
    19. }
    20. int main(){
    21. printf("program begin: \n");
    22. int child_pid = clone(child_main, child_stack + STACK_SIZE, SIGCHLD | CLONE_NEWUTS | CLONE_NEWIPC, NULL);
    23. waitpid(child_pid, NULL, 0);
    24. printf("quit\n");
    25. return 0;
    26. }

    main函数中调用clone函数创建新进程同时创建新namespaces的时候,传递CLONE_NEWIPCflag, 来实现进程间IPC的隔离。

    在运行程序的时候,为了方便测试进程间通信是否被真正的隔离了,

    1.  首先我们先使用ipcmk -Q命令创建一个queue:

    1. root@double:~# ipcmk -Q
    2. Message queue id: 0

    2.  使用ipcs -q查看queue是否创建成功:

    1. root@double:~# ipcs -q
    2. ------ Message Queues --------
    3. key msqid owner perms used-bytes messages
    4. 0xe594be1e 0 root 644 0 0

    3.  编译并运行ipc.c代码对IPC进行隔离并进行验证:

    1. root@double:~# gcc -Wall ipc.c -o ipc && ./ipc
    2. program begin:
    3. in child process!
    4. root@changed name:~# ipcs -q
    5. ------ Message Queues --------
    6. key msqid owner perms used-bytes messages

    从运行的结果来看,已经找不到原先声明的message queue,实现了IPC的隔离。

    PID Namespace

    PID namespace隔离非常实用,它对进程PID重新标号,即两个不同的namespace下的进程可以拥有同一个PID。每一个PID namespace都有字的计数程序。内核为所有的PID namespace维护了一个树状结构,最顶层的是系统初始时创建的,即root namespace。他创建的新的PID namespace称child namespace。通过这种方式,不同的PID namespace会形成一个等级的体系。所属的父节点可以看到子节点中的进程,并可以通过信号等方式对子节点中的进程产生影响。反过来,子节点不能看到父节点PID namespace中的任何内容。

    由此产生如下结论:

    ● 每个PID namespace中的第一个进程“PID 1“,都会像传统Linux中的init进程一样拥有特权,起特殊作用。

    ● 一个namespace中的进程,不可能通过kill或ptrace影响父节点或者兄弟节点中的进程,因为其他节点的PID在这个namespace中没有任何意义。

    ● 如果你在新的PID namespace中重新挂载/proc文件系统,会发现其下只显示同属一个PID namespace中的其他进程。

    ● 在root namespace中可以看到所有的进程,并且递归包含所有子节点中的进程。

    ● 

    下面我们来看下对PID的隔离效果,测试代码如下:

    1. #define _GNU_SOURCE
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #define STACK_SIZE (1024*1024)
    9. static char child_stack[STACK_SIZE];
    10. char* const child_args[] = {
    11. "/bin/bash",
    12. NULL
    13. };
    14. int child_main(void* args){
    15. printf("in child process!\n");
    16. sethostname("changed namespace", 12);
    17. execv(child_args[0], child_args);
    18. return 1;
    19. }
    20. int main(){
    21. printf("program begin: \n");
    22. int child_pid = clone(child_main, child_stack + STACK_SIZE, SIGCHLD | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID, NULL);
    23. waitpid(child_pid, NULL, 0);
    24. printf("quit\n");
    25. return 0;
    26. }

    main函数中调用clone函数创建新的进程同时创建新的namespace的时候,传递CLONE_NEWPID flag。来实现对PIDg隔离。

    让我们编译并运行代码,看下效果:

    1. root@double:~# vi pid.c
    2. root@double:~# gcc -Wall pid.c -o pid && ./pid
    3. program begin:
    4. in child process!
    5. root@changed name:~# echo $$
    6. 1
    7. root@changed name:~# ps aux

    我们可以看到,子进程的pid是1了,但是,我们会发现,在子进程的shell里输入ps,top等命令,我们还是可以看得到所有进程。说明并没有完全隔离。这是因为,像ps, top这些命令会去读/proc文件系统,所以,因为/proc文件系统在父进程和子进程都是一样的,所以这些命令显示的东西都是一样的。

    所以,我们还需要对文件系统进行隔离。

    Mount Namespace

    Mount namespace通过隔离文件系统挂载点对隔离文件系统提供支持。隔离后,不同的mount namespace中的文件结构发生变化也互不影响。你可以通过/proc/[pid]/mounts查看到所有挂载在当前namesapce中的文件系统,还可以通过/proc/[pid]/mountstats看到mount namespace中文件设备的统计信息,包括挂载的文件名称,文件系统类型,挂载位置等等。

    进程在创建mount namespace的时候,会把当前结构复制给新的namespace。 新的namespace中的所有mount操作都影响自身的文件系统,而对外界不会产生任何影响。这样做就严格地实现了隔离。

    让我们来对文件系统进行隔离,测试的代码如下:

    1. #define _GNU_SOURCE
    2. #include <sys/types.h>
    3. #include <sys/wait.h>
    4. #include <sys/mount.h>
    5. #include <stdio.h>
    6. #include <sched.h>
    7. #include <signal.h>
    8. #include <unistd.h>
    9. #define STACK_SIZE (1024 * 1024)
    10. // sync primitive
    11. int checkpoint[2];
    12. static char child_stack[STACK_SIZE];
    13. char* const child_args[] = {
    14. "/bin/bash",
    15. NULL
    16. };
    17. int child_main(void* arg) {
    18. char c;
    19. // init sync primitive
    20. close(checkpoint[1]);
    21. // setup hostname
    22. sethostname("changed namespace", 12);
    23. // remount "/proc" to get accurate "top" && "ps" output
    24. mount("proc", "/proc", "proc", 0, NULL);
    25. // wait...
    26. read(checkpoint[0], &c, 1);
    27. execv(child_args[0], child_args);
    28. printf("Ooops\n");
    29. return 1;
    30. }
    31. int main() {
    32. // init sync primitive
    33. pipe(checkpoint);
    34. int child_pid = clone(child_main, child_stack+STACK_SIZE,
    35. CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);
    36. // further init here (nothing yet)
    37. // signal "done"
    38. close(checkpoint[1]);
    39. waitpid(child_pid, NULL, 0);
    40. printf("quit!\n");
    41. return 0;
    42. }

    main函数中调用clone函数创建新进程同时创建新的namespace时,需要增加CLONE_NEWNS flag。来实现对Mount namespace的隔离。

    下面让我们编译并运行下程序来验证是否实现了对文件系统的隔离。

    1. root@double:~# gcc -Wall mntns.c -o mnt && ./mnt
    2. root@changed name:~# ps aux
    3. USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
    4. root 1 0.0 0.2 9836 3952 pts/2 S 18:08 0:00 /bin/bash
    5. root 8 0.0 0.1 11488 3288 pts/2 R+ 18:08 0:00 ps aux

    上面,我们可以看到只有两个进程 ,而且pid=1的进程是我们的/bin/bash。我们还可以看到/proc目录下也干净了很多:

    1. root@changed name:~# ls /proc
    2. 1 bus cpuinfo dma filesystems ioports keys kpagecount mdstat mounts partitions scsi stat sysvipc uptime vmstat
    3. 9 cgroups crypto driver fs irq key-users kpageflags meminfo mtrr pressure self swaps thread-self version zoneinfo
    4. acpi cmdline devices execdomains interrupts kallsyms kmsg loadavg misc net sched_debug slabinfo sys timer_list version_signature
    5. buddyinfo consoles diskstats fb iomem kcore kpagecgroup locks modules pagetypeinfo schedstat softirqs sysrq-trigger tty vmallocinfo
    关于mount相关的知识很多。这里就具体的详细介绍了,如果感兴趣可以看下:

    Mount namespaces and shared subtrees

    mount a filesystem

    User Namespace

    注意:User namespace是Linux内核(Linux 3.8)最后支持的namespace,所以有的版本的系统内核可能还没有对该namespace支持。

    User namespace主要隔离了安全相关的标识符和属性,包括用户ID,用户组ID,root目录等。通俗点就是: 一个普通用户的进程通过clone()创建新的进程在新user namespace中可以拥有不同的用户和用户组。这意味着一个进程在容器外属于一个没有特殊权限的普通用户,但是它创建的容器进程却属于拥有所有权限的超级用户,这个技术为容器提供了极大的自由。

    Linux中,特权用户的user ID是0,演示的最终我们将看到user ID非0的进程启动user namespace后user ID可以变为0。使用user namespace的方法和其它的namespace的使用方式没有太大的区别。即调用clone()的时候,需要加入CLONE_NEWUSER标识位。

    让我们来看下user namespace的隔离效果,测试代码如下:

    1. #define _GNU_SOURCE
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. #define STACK_SIZE (1024 * 1024)
    13. static char container_stack[STACK_SIZE];
    14. char* const container_args[] = {
    15. "/bin/bash",
    16. NULL
    17. };
    18. int pipefd[2];
    19. void set_map(char* file, int inside_id, int outside_id, int len) {
    20. FILE* mapfd = fopen(file, "w");
    21. if (NULL == mapfd) {
    22. perror("open file error");
    23. return;
    24. }
    25. fprintf(mapfd, "%d %d %d", inside_id, outside_id, len);
    26. fclose(mapfd);
    27. }
    28. void set_uid_map(pid_t pid, int inside_id, int outside_id, int len) {
    29. char file[256];
    30. sprintf(file, "/proc/%d/uid_map", pid);
    31. set_map(file, inside_id, outside_id, len);
    32. }
    33. void set_gid_map(pid_t pid, int inside_id, int outside_id, int len) {
    34. char file[256];
    35. sprintf(file, "/proc/%d/gid_map", pid);
    36. set_map(file, inside_id, outside_id, len);
    37. }
    38. int container_main(void* arg)
    39. {
    40. printf("Container [%5d] - inside the container!\n", getpid());
    41. printf("Container: eUID = %ld; eGID = %ld, UID=%ld, GID=%ld\n",
    42. (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());
    43. /* 等待父进程通知后再往下执行(进程间的同步) */
    44. char ch;
    45. close(pipefd[1]);
    46. read(pipefd[0], &ch, 1);
    47. printf("Container [%5d] - setup hostname!\n", getpid());
    48. //set hostname
    49. sethostname("container",10);
    50. //remount "/proc" to make sure the "top" and "ps" show container's information
    51. mount("proc", "/proc", "proc", 0, NULL);
    52. execv(container_args[0], container_args);
    53. printf("Something's wrong!\n");
    54. return 1;
    55. }
    56. int main()
    57. {
    58. const int gid=getgid(), uid=getuid();
    59. printf("Parent: eUID = %ld; eGID = %ld, UID=%ld, GID=%ld\n",
    60. (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());
    61. pipe(pipefd);
    62. printf("Parent [%5d] - start a container!\n", getpid());
    63. int container_pid = clone(container_main, container_stack+STACK_SIZE,
    64. CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL);
    65. printf("Parent [%5d] - Container [%5d]!\n", getpid(), container_pid);
    66. //To map the uid/gid,
    67. // we need edit the /proc/PID/uid_map (or /proc/PID/gid_map) in parent
    68. //The file format is
    69. // ID-inside-ns ID-outside-ns length
    70. //if no mapping,
    71. // the uid will be taken from /proc/sys/kernel/overflowuid
    72. // the gid will be taken from /proc/sys/kernel/overflowgid
    73. set_uid_map(container_pid, 0, uid, 1);
    74. set_gid_map(container_pid, 0, gid, 1);
    75. printf("Parent [%5d] - user/group mapping done!\n", getpid());
    76. /* 通知子进程 */
    77. close(pipefd[1]);
    78. waitpid(container_pid, NULL, 0);
    79. printf("Parent - container stopped!\n");
    80. return 0;
    81. }

    在编译并执行代码之前,我们先来看下当前的用户uid和gid.(需以普通用户执行)

    1. $ id
    2. uid=1000(double) gid=1000(double) groups=1000(double)

    现在编译并运行我们的代码来验证user namespace是否隔离成功:

    注意: 如果编译时如下报错:

    1. $ gcc userns.c -Wall -lcap -o userns && ./userns
    2. userns.c:7:10: fatal error: sys/capability.h: No such file or directory
    3. 7 | #include <sys/capability.h>
    4. | ^~~~~~~~~~~~~~~~~~
    5. compilation terminated.

    则在ubuntu编译则需要安装libcap-dev包,如果在centos上编译则需要安装libcap-devel包。

    重新执行

    1. $ gcc userns.c -Wall -lcap -o userns && ./userns
    2. Parent: eUID = 1000; eGID = 1000, UID=1000, GID=1000
    3. Parent [ 9527] - start a container!
    4. Parent [ 9527] - Container [ 9528]!
    5. Parent [ 9527] - user/group mapping done!
    6. Container [ 1] - inside the container!
    7. Container: eUID = 0; eGID = 65534, UID=0, GID=65534
    8. Container [ 1] - setup hostname!

    我们可以看到容器里的用户和命令行提示符是root用户了

    1. root@container:~# id
    2. uid=0(root) gid=65534(nogroup) groups=65534(nogroup)

    Network Namespace

    总结

    容器的隔离实现基本就是通过Linux内核提供的这6种namespace实现。但是容器依旧没有实现完全的环境隔离。比如: SELinux,Cgroups以及/sys/proc/sys/dev/sd*等目录下的资源依据是没有被隔离的。因此我们通常使用的ps, top命令查看到的数据依旧是宿主机的数据。因为它们的数据来源于/proc等目录下的文件。如果想要在可视化的角度来实现这方便的可视化隔离。可以看看之前调研的lxcfs对docker容器隔离

    参考

    浅谈 Linux Namespace | xigang's home

    Docker基础技术:Linux Namespace(下) | 酷 壳 - CoolShell

    http://docs.wixstatic.com/ugd/295986_d73d8d6087ed430c34c21f90b0b607fd.pdf

    http://ramirose.wixsite.com/ramirosen

    Docker背后的内核知识——Namespace资源隔离_语言 & 开发_孙健波_InfoQ精选文章

    Linux Namespace分析——mnt namespace的实现与应用

    Linux内核的namespace机制分析 - kk Blog —— 通用基础

  • 相关阅读:
    HCIP第十一天
    运动耳机买什么样的好?各类运动蓝牙耳机推荐
    第82步 时间序列建模实战:LightGBM回归建模
    设计模式之观察者模式
    proxomx虚机无法启动的奇怪问题
    翻阅必备----Java窗口组件,容器,布局,监听,事件 API大全
    win10 mmdetection3d环境搭建
    spring spring-boot spring-cloud spring-cloud-alibaba之间版本对应关系
    昇思MindSpore携手宝兰德推出智慧工地解决方案,助力工地安全生产管理领域数智化升级
    【数据库系统】连接查询
  • 原文地址:https://blog.csdn.net/huchao_lingo/article/details/140448672