1818IP-服务器技术教程,云服务器评测推荐,服务器系统排错处理,环境搭建,攻击防护等

当前位置:首页 - 运维 - 正文

君子好学,自强不息!

Docker基础技术:Linux Namespace(上)

2022-11-10 | 运维 | gtxyzz | 559°c
A+ A-

时下最热的技术莫过于Docker了,很多人都觉得Docker是个新技术,其实不然,Docker除了其编程语言用go比较新外,其实它还真不是个新东西,也就是个新瓶装旧酒的东西,所谓的The New “Old Stuff”。Docker和Docker衍生的东西用到了很多很酷的技术,我会用几篇 文章来把这些技术给大家做个介绍,希望通过这些文章大家可以自己打造一个山寨版的docker。

当然,文章的风格一定会尊重时下的“流行”——我们再也没有整块整块的时间去看书去专研,而我们只有看微博微信那样的碎片时间(那怕我们有整块的时间,也被那些在手机上的APP碎片化了)。所以,这些文章的风格必然坚持“马桶风格”(希望简单到占用你拉一泡屎就时间,而且你还不用动脑子,并能学到些东西)

废话少说,我们开始。先从Linux Namespace开始。

简介

Linux Namespace是Linux提供的一种内核级别环境隔离的方法。不知道你是否还记得很早以前的Unix有一个叫chroot的系统调用(通过修改根目录把用户jail到一个特定目录下),chroot提供了一种简单的隔离模式:chroot内部的文件系统无法访问外部的内容。Linux Namespace在此基础上,提供了对UTS、IPC、mount、PID、network、User等的隔离机制。

举个例子,我们都知道,Linux下的超级父亲进程的PID是1,所以,同chroot一样,如果我们可以把用户的进程空间jail到某个进程分支下,并像chroot那样让其下面的进程 看到的那个超级父进程的PID为1,于是就可以达到资源隔离的效果了(不同的PID namespace中的进程无法看到彼此)

šLinux Namespace 有如下种类,官方文档在这里《Namespace in Operation》

主要是š三个系统调用

  • šclone() – 实现线程的系统调用,用来创建一个新的进程,并可以通过设计上述参数达到隔离。
  • šunshare() – 使某进程脱离某个namespace
  • šsetns() – 把某进程加入到某个namespace

unshare() 和 setns() 都比较简单,大家可以自己man,我这里不说了。

下面还是让我们来看一些示例(以下的测试程序最好在Linux 内核为3.8以上的版本中运行,我用的是ubuntu 14.04)。

clone()系统调用

首先,我们来看一下一个最简单的clone()系统调用的示例,(后面,我们的程序都会基于这个程序做修改):

#define_GNU_SOURCE
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<sched.h>
#include<signal.h>
#include<unistd.h>

/*定义一个给clone用的栈,栈大小1M*/
#defineSTACK_SIZE(1024*1024)
staticcharcontainer_stack[STACK_SIZE];

char*constcontainer_args[]={
"/bin/bash",
NULL
};

intcontainer_main(void*arg)
{
printf("Container-insidethecontainer!\n");
/*直接执行一个shell,以便我们观察这个进程空间里的资源是否被隔离了*/
execv(container_args[0],container_args);
printf("Something'swrong!\n");
return1;
}

intmain()
{
printf("Parent-startacontainer!\n");
/*调用clone函数,其中传出一个函数,还有一个栈空间的(为什么传尾指针,因为栈是反着的)*/
intcontainer_pid=clone(container_main,container_stack+STACK_SIZE,SIGCHLD,NULL);
/*等待子进程结束*/
waitpid(container_pid,NULL,0);
printf("Parent-containerstopped!\n");
return0;
}

从上面的程序,我们可以看到,这和pthread基本上是一样的玩法。但是,对于上面的程序,父子进程的进程空间是没有什么差别的,父进程能访问到的子进程也能。

下面, 让我们来看几个例子看看,Linux的Namespace是什么样的。

UTS Namespace

下面的代码,我略去了上面那些头文件和数据结构的定义,只有最重要的部分。

intcontainer_main(void*arg)
{
printf("Container-insidethecontainer!\n");
sethostname("container",10);/*设置hostname*/
execv(container_args[0],container_args);
printf("Something'swrong!\n");
return1;
}

intmain()
{
printf("Parent-startacontainer!\n");
intcontainer_pid=clone(container_main,container_stack+STACK_SIZE,
CLONE_NEWUTS|SIGCHLD,NULL);/*启用CLONE_NEWUTSNamespace隔离*/
waitpid(container_pid,NULL,0);
printf("Parent-containerstopped!\n");
return0;
}

运行上面的程序你会发现(需要root权限),子进程的hostname变成了 container。

hchen@ubuntu:~$sudo./uts
Parent-startacontainer!
Container-insidethecontainer!
root@container:~#hostname
container
root@container:~#uname-n
container

IPC Namespace

IPC全称 Inter-Process Communication,是Unix/Linux下进程间通信的一种方式,IPC有共享内存、信号量、消息队列等方法。所以,为了隔离,我们也需要把IPC给隔离开来,这样,只有在同一个Namespace下的进程才能相互通信。如果你熟悉IPC的原理的话,你会知道,IPC需要有一个全局的ID,即然是全局的,那么就意味着我们的Namespace需要对这个ID隔离,不能让别的Namespace的进程看到。

要启动IPC隔离,我们只需要在调用clone时加上CLONE_NEWIPC参数就可以了。

intcontainer_pid=clone(container_main,container_stack+STACK_SIZE,
CLONE_NEWUTS|CLONE_NEWIPC|SIGCHLD,NULL);

首先,我们先创建一个IPC的Queue(如下所示,全局的Queue ID是0)

hchen@ubuntu:~$ipcmk-Q
Messagequeueid:0

hchen@ubuntu:~$ipcs-q
------MessageQueues--------
keymsqidownerpermsused-bytesmessages
0xd0d56eb20hchen64400

如果我们运行没有CLONE_NEWIPC的程序,我们会看到,在子进程中还是能看到这个全启的IPC Queue。

hchen@ubuntu:~$sudo./uts
Parent-startacontainer!
Container-insidethecontainer!

root@container:~#ipcs-q

------MessageQueues--------
keymsqidownerpermsused-bytesmessages
0xd0d56eb20hchen64400

但是,如果我们运行加上了CLONE_NEWIPC的程序,我们就会下面的结果:

root@ubuntu:~$sudo./ipc
Parent-startacontainer!
Container-insidethecontainer!

root@container:~/linux_namespace#ipcs-q

------MessageQueues--------
keymsqidownerpermsused-bytesmessages

我们可以看到IPC已经被隔离了。

PID Namespace

我们继续修改上面的程序:

intcontainer_main(void*arg)
{
/*查看子进程的PID,我们可以看到其输出子进程的pid为1*/
printf("Container[%5d]-insidethecontainer!\n",getpid());
sethostname("container",10);
execv(container_args[0],container_args);
printf("Something'swrong!\n");
return1;
}

intmain()
{
printf("Parent[%5d]-startacontainer!\n",getpid());
/*启用PIDnamespace-CLONE_NEWPID*/
intcontainer_pid=clone(container_main,container_stack+STACK_SIZE,
CLONE_NEWUTS|CLONE_NEWPID|SIGCHLD,NULL);
waitpid(container_pid,NULL,0);
printf("Parent-containerstopped!\n");
return0;
}

运行结果如下(我们可以看到,子进程的pid是1了):

hchen@ubuntu:~$sudo./pid
Parent[3474]-startacontainer!
Container[1]-insidethecontainer!
root@container:~#echo$$
1

你可能会问,PID为1有个毛用啊?我们知道,在传统的UNIX系统中,PID为1的进程是init,地位非常特殊。他作为所有进程的父进程,有很多特权(比如:屏蔽信号等),另外,其还会为检查所有进程的状态,我们知道,如果某个子进程脱离了父进程(父进程没有wait它),那么init就会负责回收资源并结束这个子进程。所以,要做到进程空间的隔离,首先要创建出PID为1的进程,最好就像chroot那样,把子进程的PID在容器内变成1。

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

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

Mount Namespace

下面的例程中,我们在启用了mount namespace并在子进程中重新mount了/proc文件系统。

intcontainer_main(void*arg)
{
printf("Container[%5d]-insidethecontainer!\n",getpid());
sethostname("container",10);
/*重新mountproc文件系统到/proc下*/
system("mount-tprocproc/proc");
execv(container_args[0],container_args);
printf("Something'swrong!\n");
return1;
}

intmain()
{
printf("Parent[%5d]-startacontainer!\n",getpid());
/*启用MountNamespace-增加CLONE_NEWNS参数*/
intcontainer_pid=clone(container_main,container_stack+STACK_SIZE,
CLONE_NEWUTS|CLONE_NEWPID|CLONE_NEWNS|SIGCHLD,NULL);
waitpid(container_pid,NULL,0);
printf("Parent-containerstopped!\n");
return0;
}

运行结果如下:

hchen@ubuntu:~$sudo./pid.mnt
Parent[3502]-startacontainer!
Container[1]-insidethecontainer!
root@container:~#ps-elf
FSUIDPIDPPIDCPRINIADDRSZWCHANSTIMETTYTIMECMD
4Sroot100800-6917wait19:55pts/200:00:00/bin/bash
0Rroot1410800-5671-19:56pts/200:00:00ps-elf

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

root@container:~#ls/proc
1dmakey-usersnetsysvipc
16driverkmsgpagetypeinfotimer_list
acpiexecdomainskpagecountpartitionstimer_stats
asoundfbkpageflagssched_debugtty
buddyinfofilesystemsloadavgschedstatuptime
busfslocksscsiversion
cgroupsinterruptsmdstatselfversion_signature
cmdlineiomemmeminfoslabinfovmallocinfo
consolesioportsmiscsoftirqsvmstat
cpuinfoirqmodulesstatzoneinfo
cryptokallsymsmountsswaps
deviceskcoremptsys
diskstatskeysmtrrsysrq-trigger

下图,我们也可以看到在子进程中的top命令只看得到两个进程了。

这里,多说一下。在通过CLONE_NEWNS创建mount namespace后,父进程会把自己的文件结构复制给子进程中。而子进程中新的namespace中的所有mount操作都只影响自身的文件系统,而不对外界产生任何影响。这样可以做到比较严格地隔离。

你可能会问,我们是不是还有别的一些文件系统也需要这样mount? 是的。

Docker的 Mount Namespace

下面我将向演示一个“山寨镜像”,其模仿了Docker的Mount Namespace。

首先,我们需要一个rootfs,也就是我们需要把我们要做的镜像中的那些命令什么的copy到一个rootfs的目录下,我们模仿Linux构建如下的目录:

hchen@ubuntu:~/rootfs$ls
bindevetchomeliblib64mntoptprocrootrunsbinsystmpusrvar

然后,我们把一些我们需要的命令copy到 rootfs/bin目录中(sh命令必需要copy进去,不然我们无法 chroot )

hchen@ubuntu:~/rootfs$ls./bin./usr/bin

./bin:
bashchowngziplessmountnetstatrmtabsteetoptty
catcphostnamelnmountpointpingsedtactesttouchumount
chgrpechoiplsmvpsshtailtimeouttruname
chmodgrepkillmorencpwdsleeptartoetruncatewhich

./usr/bin:
awkenvgroupsheadidmesgsortstracetailtopuniqviwcxargs

注:你可以使用ldd命令把这些命令相关的那些so文件copy到对应的目录:

hchen@ubuntu:~/rootfs/bin$lddbash
linux-vdso.so.1=>(0x00007fffd33fc000)
libtinfo.so.5=>/lib/x86_64-linux-gnu/libtinfo.so.5(0x00007f4bd42c2000)
libdl.so.2=>/lib/x86_64-linux-gnu/libdl.so.2(0x00007f4bd40be000)
libc.so.6=>/lib/x86_64-linux-gnu/libc.so.6(0x00007f4bd3cf8000)
/lib64/ld-linux-x86-64.so.2(0x00007f4bd4504000)

下面是我的rootfs中的一些so文件:

hchen@ubuntu:~/rootfs$ls./lib64./lib/x86_64-linux-gnu/

./lib64:
ld-linux-x86-64.so.2

./lib/x86_64-linux-gnu/:
libacl.so.1libmemusage.solibnss_files-2.19.solibpython3.4m.so.1
libacl.so.1.1.0libmount.so.1libnss_files.so.2libpython3.4m.so.1.0
libattr.so.1libmount.so.1.1.0libnss_hesiod-2.19.solibresolv-2.19.so
libblkid.so.1libm.so.6libnss_hesiod.so.2libresolv.so.2
libc-2.19.solibncurses.so.5libnss_nis-2.19.solibselinux.so.1
libcap.alibncurses.so.5.9libnss_nisplus-2.19.solibtinfo.so.5
libcap.solibncursesw.so.5libnss_nisplus.so.2libtinfo.so.5.9
libcap.so.2libncursesw.so.5.9libnss_nis.so.2libutil-2.19.so
libcap.so.2.24libnsl-2.19.solibpcre.so.3libutil.so.1
libc.so.6libnsl.so.1libprocps.so.3libuuid.so.1
libdl-2.19.solibnss_compat-2.19.solibpthread-2.19.solibz.so.1
libdl.so.2libnss_compat.so.2libpthread.so.0
libgpm.so.2libnss_dns-2.19.solibpython2.7.so.1
libm-2.19.solibnss_dns.so.2libpython2.7.so.1.0

包括这些命令依赖的一些配置文件:

hchen@ubuntu:~/rootfs$ls./etc
bash.bashrcgrouphostnamehostsld.so.cachensswitch.confpasswdprofile
resolv.confshadow

你现在会说,我靠,有些配置我希望是在容器起动时给他设置的,而不是hard code在镜像中的。比如:/etc/hosts,/etc/hostname,还有DNS的/etc/resolv.conf文件。好的。那我们在rootfs外面,我们再创建一个conf目录,把这些文件放到这个目录中。

hchen@ubuntu:~$ls./conf
hostnamehostsresolv.conf

这样,我们的父进程就可以动态地设置容器需要的这些文件的配置, 然后再把他们mount进容器,这样,容器的镜像中的配置就比较灵活了。

好了,终于到了我们的程序。

#define_GNU_SOURCE
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/mount.h>
#include<stdio.h>
#include<sched.h>
#include<signal.h>
#include<unistd.h>

#defineSTACK_SIZE(1024*1024)

staticcharcontainer_stack[STACK_SIZE];
char*constcontainer_args[]={
"/bin/bash",
"-l",
NULL
};

intcontainer_main(void*arg)
{
printf("Container[%5d]-insidethecontainer!\n",getpid());

//sethostname
sethostname("container",10);

//remount"/proc"tomakesurethe"top"and"ps"showcontainer'sinformation
if(mount("proc","rootfs/proc","proc",0,NULL)!=0){
perror("proc");
}
if(mount("sysfs","rootfs/sys","sysfs",0,NULL)!=0){
perror("sys");
}
if(mount("none","rootfs/tmp","tmpfs",0,NULL)!=0){
perror("tmp");
}
if(mount("udev","rootfs/dev","devtmpfs",0,NULL)!=0){
perror("dev");
}
if(mount("devpts","rootfs/dev/pts","devpts",0,NULL)!=0){
perror("dev/pts");
}
if(mount("shm","rootfs/dev/shm","tmpfs",0,NULL)!=0){
perror("dev/shm");
}
if(mount("tmpfs","rootfs/run","tmpfs",0,NULL)!=0){
perror("run");
}
/*
*模仿Docker的从外向容器里mount相关的配置文件
*你可以查看:/var/lib/docker/containers/<container_id>/目录,
*你会看到docker的这些文件的。
*/
if(mount("conf/hosts","rootfs/etc/hosts","none",MS_BIND,NULL)!=0||
mount("conf/hostname","rootfs/etc/hostname","none",MS_BIND,NULL)!=0||
mount("conf/resolv.conf","rootfs/etc/resolv.conf","none",MS_BIND,NULL)!=0){
perror("conf");
}
/*模仿dockerrun命令中的-v,--volume=[]参数干的事*/
if(mount("/tmp/t1","rootfs/mnt","none",MS_BIND,NULL)!=0){
perror("mnt");
}

/*chroot隔离目录*/
if(chdir("./rootfs")!=0||chroot("./")!=0){
perror("chdir/chroot");
}

execv(container_args[0],container_args);
perror("exec");
printf("Something'swrong!\n");
return1;
}

intmain()
{
printf("Parent[%5d]-startacontainer!\n",getpid());
intcontainer_pid=clone(container_main,container_stack+STACK_SIZE,
CLONE_NEWUTS|CLONE_NEWIPC|CLONE_NEWPID|CLONE_NEWNS|SIGCHLD,NULL);
waitpid(container_pid,NULL,0);
printf("Parent-containerstopped!\n");
return0;
}

sudo运行上面的程序,你会看到下面的挂载信息以及一个所谓的“镜像”:

hchen@ubuntu:~$sudo./mount
Parent[4517]-startacontainer!
Container[1]-insidethecontainer!
root@container:/#mount
procon/proctypeproc(rw,relatime)
sysfson/systypesysfs(rw,relatime)
noneon/tmptypetmpfs(rw,relatime)
udevon/devtypedevtmpfs(rw,relatime,size=493976k,nr_inodes=123494,mode=755)
devptson/dev/ptstypedevpts(rw,relatime,mode=600,ptmxmode=000)
tmpfson/runtypetmpfs(rw,relatime)
/dev/disk/by-uuid/18086e3b-d805-4515-9e91-7efb2fe5c0e2on/etc/hoststypeext4(rw,relatime,errors=remount-ro,data=ordered)
/dev/disk/by-uuid/18086e3b-d805-4515-9e91-7efb2fe5c0e2on/etc/hostnametypeext4(rw,relatime,errors=remount-ro,data=ordered)
/dev/disk/by-uuid/18086e3b-d805-4515-9e91-7efb2fe5c0e2on/etc/resolv.conftypeext4(rw,relatime,errors=remount-ro,data=ordered)

root@container:/#ls/bin/usr/bin
/bin:
bashchmodechohostnamelessmoremvpingrmsleeptailtesttoptruncateuname
catchowngrepiplnmountncpssedtabstartimeouttouchttywhich
chgrpcpgzipkilllsmountpointnetstatpwdshtacteetoetrumount

/usr/bin:
awkenvgroupsheadidmesgsortstracetailtopuniqviwcxargs

关于如何做一个chroot的目录,这里有个工具叫DebootstrapChroot,你可以顺着链接去看看(英文的哦)

接下来的事情,你可以自己玩了,我相信你的想像力 。:)

今天的内容就介绍到这里,在Docker 基础技术:Linux Namespace(下篇)中,我将向你介绍User Namespace、Network Namespace以及Namespace的其它东西。

本文来源:1818IP

本文地址:https://www.1818ip.com/post/8284.html

免责声明:本文由用户上传,如有侵权请联系删除!

发表评论

必填

选填

选填

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。