Docker原理浅析(2)

这篇博客详细探讨了Linux中的Namespace概念,通过实例解释了如何使用Clone、setns和unshare等API来创建和管理namespace。文章覆盖了UTS、IPC、mount、pid、network和user等不同类型的namespace,阐述了它们在隔离系统资源方面的作用。同时,介绍了如何利用这些namespace创建简单的容器,包括设置容器的根目录和配置。
摘要由CSDN通过智能技术生成

namespace

前言

首先我们先运行一个docker项目,来感受一个pid namespace的例子

docker run -it busybox /bin/sh
/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    7 root      0:00 ps

根据上面的命令,可以看出容器内部的进程是从1号开始的
而这种技术就是上节所说的namespace,它其实是linux创建新进程的一个可选参数,而在linux系统中创建现成的系统调用是clone(),比如:


int pid = clone(main_function, stack_size, SIGCHLD, NULL); 

这个系统调用会为我们创建一个新的进程,并且返回它的进程号pid

而当我们用clone系统调用创建一个新进程时,就可以在参数中指定CLONE_NEWPID参数,比如:


int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL); 

这样我们就做到了,在这个进程空间里,它的pid是1

概述

Namespace是对全局系统资源的一种封装隔离,使得处于不同namespace的进程拥有独立的全局系统资源,改变一个namespace中的系统资源只会影响当前namespace里的进程,对其他namespace中的进程没有影响。目前,Linux内核里面实现了7种不同类型的namespace:

名称        宏定义             隔离内容
Cgroup      CLONE_NEWCGROUP   Cgroup root directory (since Linux 4.6)
IPC         CLONE_NEWIPC      System V IPC, POSIX message queues (since Linux 2.6.19)
Network     CLONE_NEWNET      Network devices, stacks, ports, etc. (since Linux 2.6.24)
Mount       CLONE_NEWNS       Mount points (since Linux 2.4.19)
PID         CLONE_NEWPID      Process IDs (since Linux 2.6.24)
User        CLONE_NEWUSER     User and group IDs (started in Linux 2.6.23 and completed in Linux 3.8)
UTS         CLONE_NEWUTS      Hostname and NIS domain name (since Linux 2.6.19)

而我们如果想要查看进程所属的namespaces,每个进程都有/proc/[pid]/ns/这样一个目录,里面包含了这个进程所属namespace的信息,里面每个文件的描述符都可以用来作为setns函数(后面会介绍)的参数。

#查看当前bash进程所属的namespace
/ # ls -l /proc/$$/ns
total 0
lrwxrwxrwx    1 root     root             0 Oct 29 01:49 cgroup -> cgroup:[4026531835]
lrwxrwxrwx    1 root     root             0 Oct 29 01:49 ipc -> ipc:[4026532331]
lrwxrwxrwx    1 root     root             0 Oct 29 01:49 mnt -> mnt:[4026532329]
lrwxrwxrwx    1 root     root             0 Oct 29 01:49 net -> net:[4026532334]
lrwxrwxrwx    1 root     root             0 Oct 29 01:49 pid -> pid:[4026532332]
lrwxrwxrwx    1 root     root             0 Oct 29 01:49 user -> user:[4026531837]
lrwxrwxrwx    1 root     root             0 Oct 29 01:49 uts -> uts:[4026532330]

注:以ipc:[4026532331]为例,ipc是namespace的类型,4026532331是inode number,如果两个进程的ipc namespace的inode number一样,说明他们属于同一个namespace。这条规则对其他类型的namespace也同样适用。

跟namcespace相关的API
Clone(创建一个新的进程并把他放到新的namespace中)
int clone(int (*child_func)(void *), void *child_stack
            , int flags, void *arg);

flags: 
    指定一个或者多个上面的CLONE_NEW*(当然也可以包含跟namespace无关的flags), 
    这样就会创建一个或多个新的不同类型的namespace, 
    并把新创建的子进程加入新创建的这些namespace中。
setns(将当前进程加入到已有的namespace中)
int setns(int fd, int nstype);

fd: 
    指向/proc/[pid]/ns/目录里相应namespace对应的文件,
    表示要加入哪个namespace

nstype:
    指定namespace的类型(上面的任意一个CLONE_NEW*):
    1. 如果当前进程不能根据fd得到它的类型,如fd由其他进程创建,
    并通过UNIX domain socket传给当前进程,
    那么就需要通过nstype来指定fd指向的namespace的类型
    2. 如果进程能根据fd得到namespace类型,比如这个fd是由当前进程打开的,
    那么nstype设置为0即可
unshare(使当前进程退出指定类型的namespace,并加入到新创建的namespace(相当于创建并加入新的namespace))
int unshare(int flags);

flags:
    指定一个或者多个上面的CLONE_NEW*,
    这样当前进程就退出了当前指定类型的namespace并加入到新创建的namespace

clone和unshare的区别:

clone和unshare的功能都是创建并加入新的namespace, 他们的区别是:

  • unshare是使当前进程加入新的namespace

  • clone是创建一个新的子进程,然后让子进程加入新的namespace,而当前进程保持不变

UTS namespace(CLONE_NEWUTS)

UTS(UNIX Time Sharing) namespace用来隔离系统的hostname以及NIS domain name。(Network Information System)

创建新的UTS namespace
#define _GNU_SOURCE
#include <sched.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define NOT_OK_EXIT(code, msg); {if(code == -1){perror(msg); exit(-1);} }

//子进程从这里开始执行
static int child_func(void *hostname)
{
    //设置主机名
    sethostname(hostname, strlen(hostname));

    //用一个新的bash来替换掉当前子进程,
    //执行完execlp后,子进程没有退出,也没有创建新的进程,
    //只是当前子进程不再运行自己的代码,而是去执行bash的代码,
    //详情请参考"man execlp"
    //bash退出后,子进程执行完毕
    execlp("bash", "bash", (char *) NULL);

    //从这里开始的代码将不会被执行到,因为当前子进程已经被上面的bash替换掉了

    return 0;
}

static char child_stack[1024*1024]; //设置子进程的栈空间为1M

int main(int argc, char *argv[])
{
    pid_t child_pid;

    if (argc < 2) {
        printf("Usage: %s <child-hostname>\n", argv[0]);
        return -1;
    }

    //创建并启动子进程,调用该函数后,父进程将继续往后执行,也就是执行后面的waitpid
    child_pid = clone(child_func,  //子进程将执行child_func这个函数
                    //栈是从高位向低位增长,所以这里要指向高位地址
                    child_stack + sizeof(child_stack),
                    //CLONE_NEWUTS表示创建新的UTS namespace,
                    //这里SIGCHLD是子进程退出后返回给父进程的信号,跟namespace无关
                    CLONE_NEWUTS | SIGCHLD,
                    argv[1]);  //传给child_func的参数
    NOT_OK_EXIT(child_pid, "clone");

    waitpid(child_pid, NULL, 0); //等待子进程结束

    return 0;    //这行执行完之后,父进程结束
}

在上面的代码中:

  • 父进程创建新的子进程,并且设置CLONE_NEWUTS,这样就会创建新的UTS namespace并且让子进程属于这个新的namespace,然后父进程一直等待子进程退出

  • 子进程在设置好新的hostname后被bash替换掉

  • 当bash退出后,子进程退出,接着父进程也退出

下面运行上面的代码:

#------------------------第一个shell窗口------------------------
#将上面的代码保存为namespace_uts_demo.c, 
#然后用gcc将它编译成可执行文件namespace_uts_demo
[root@hb02-other-172e28e8e23 ~]# gcc namespace_uts_demo.c -o namespace_uts_demo   

#启动程序,传入参数container001
#创建新的UTS namespace需要root权限,所以用到sudo
[root@hb02-other-172e28e8e23 ~]# ./namespace_uts_demo container001

#新的bash被启动,从shell的提示符可以看出,hostname已经被改成了container001
#这里bash的提示符是‘#’,表示bash有root权限,
#这是因为我们是用sudo来运行的程序,于是我们程序创建的子进程有root权限
[root@container001 ~]#

#用hostname命令再确认一下
[root@container001 ~]# hostname
container001

#pstree是用来查看系统中进程之间父子关系的工具
#下面的输出过滤掉了跟namespace_uts_demo无关的内容
#本次操作是通过ssh客户端远程连接到Linux主机进行的,
#所以bash(110600)的父进程是一系列的sshd进程,
#我们在bash(110600)里面执行了 ./namespace_uts_demo container001
#所以有了我们程序namespace_uts_d(110918)对应的进程,
#我们的程序自己clone了一个新的子进程,由于clone的时候指定了参数CLONE_NEWUTS,
#所以新的子进程属于一个新的UTS namespace,然后这个新进程调用execlp后被bash替换掉了,
#于是有了bash(110919), 这个bash进程拥有所有当前子进程的属性, 
#由于我们的pstree命令是在bash(110919)里面运行的,
#所以这里pstree(111551)是bash(110919)的子进程
[root@container001 ~]# pstree -pl
systemd(1)─sshd(9429)───sshd(110559)───sshd(110564)───bash(110565)───sudo(110596)───su(110599)───bash(110600)───namespace_urs_d(110918)───bash(110919)───pstree(111551)

#验证一下我们运行的bash进程是不是bash(27334)
#下面这个命令可以输出当前bash的PID
[root@container001 ~]# echo $$
110919

#验证一下我们的父进程和子进程是否不在同一个UTS namespace
[root@container001 ~]# readlink /proc/110918/ns/uts
uts:[4026531838]
[root@container001 ~]# readlink /proc/110919/ns/uts
uts:[4026532539]
#果然不属于同一个UTS namespace,说明新的uts namespace创建成功

#默认情况下,子进程应该继承父进程的namespace
#systemd(1)是我们程序父进程namespace_uts_d(110918)的祖先进程,
#他们应该属于同一个namespace
[root@container001 ~]# readlink /proc/1/ns/uts
uts:[4026531838]

#所有bash(110919)里面执行的进程应该和bash(110919)属于同样的namespace
#self指向当前运行的进程,在这里即readlink进程
[root@container001 ~]# readlink /proc/self/ns/uts
uts:[4026532539]

#------------------------第二个shell窗口------------------------
#重新打开一个新的shell窗口,确认这个shell和上面的namespace_uts_d(27333)属于同一个namespace
[root@hb02-other-172e28e8e23 ~]# readlink /proc/$$/ns/uts
uts:[4026531838]

#老的namespace中的hostname还是原来的,不受新的namespace影响
[root@hb02-other-172e28e8e23 ~]# hostname
hb02-other-172e28e8e23
#有兴趣的同学可以在两个shell窗口里面分别用命令hostname设置hostname试试,
#会发现他们两个之间相互不受影响,这里就不演示了


#------------------------第一个shell窗口------------------------
#继续回到原来的shell,试试在container001里面再运行一下那个程序会怎样
[root@container001 ~]# ./namespace_uts_demo container002

#创建了一个新的UTS namespace,hostname被改成了container002
[root&#
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值