Docker 学习笔记8 容器技术原理 UTS Namespace
- 原型说明:
- 参数说明:
- Flag标志含义
一、 容器技术说明
容器的核心技术是Cgroup与Namespace,在此基础上还有一些其它工具共同构成容器技术。
容器是宿主机上的进程
- 容器技术通过Namespace实现资源隔离
- 通过Cgroup实现资源控制
- 通过rootfs实现文件系统隔离
- 容器引擎本身的特性来管理容器的生命周期。
Docker类似早期的LXC管理引擎。LXC是Cgrup的管理工具,Cgroup是Namespace的用户空间管理接口。
Namespace是Linux内核在task_struct中对进程组管理的基础机制。
二、Namespace
Linux Namespace是Linux提供的一种内核级别环境隔离的方法。chroot提供了一种简单的隔离模式:chroot内部的文件系统无法访问外部的内容。Linux Namespace在此基础上,提供了对UTS、IPC、mount、PID、network、User等的隔离机制。
一个容器基本需要做到6项基本隔离,也就是Linux内核中提供的6种Namespace隔离:
Namespace | Constant | 隔离内容 | 内核版本 |
IPC | CLONE_NEWIPC | 信号量,消息队列和共享内存 | Linux 2.6.19 |
Network | CLONE_NEWNET | 网络资源 | 始于Linux 2.6.24 完成于 Linux 2.6.29 |
Mount | CLONE_NEWNS | 文件系统挂载点 | Linux 2.4.19 |
PID | CLONE_NEWPID | 进程ID | Linux 2.6.24 |
UTS | CLONE_NEWUTS | 主机名和域名 | Linux 2.6.19 |
User | CLONE_NEWUSER | 用户ID和组ID | 始于 Linux 2.6.23 完成于 Linux 3.8) |
这些Namespace分别对进程的Cgroup root、进程间通信、网络、文件系统挂载点、进程ID、主机名域名进行隔离。
对Namespace的操作,主要是通过Clone,setns,unshare这三个系统调用来完成的。
- clone() – 实现线程的系统调用,用来创建一个新的进程,并可以通过上述参数达到隔离
- unshare() – 使某进程脱离某个namespace
- setns() – 把某进程加入到某个namespace
clone()系统调用
Clone可以用来创建新的Namespace。clone有一个flags参数,这些flags参数以CLONE_NEW*为格式,传入这些参数后,由clone创建出来的新进程就位于新的Namespace之中了。
原型说明:
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
参数说明:
- fn是函数指针,我们知道进程的4要素,这个就是指向程序的指针,就是所谓的“剧本"
- child_stack是为子进程分配系统堆栈空间(在Linux下系统堆栈空间是2页面,就是8K的内存,其中在这块内存中,低地址上放入了值,这个值就是进程控制块task_struct的值)
- flags就是标志用来描述你需要从父进程继承哪些资源
- arg就是传给子进程的参数。下面是flags可以取的值
Flag标志含义
- CLONE_PARENT 创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子”
- CLONE_FS 子进程与父进程共享相同的文件系统,包括root、当前目录、umask
- CLONE_FILES 子进程与父进程共享相同的文件描述符(file descriptor)表
- CLONE_NEWNS 在新的namespace启动子进程,namespace描述了进程的文件hierarchy
- CLONE_SIGHAND 子进程与父进程共享相同的信号处理(signal handler)表
- CLONE_PTRACE 若父进程被trace,子进程也被trace
- CLONE_VFORK 父进程被挂起,直至子进程释放虚拟内存资源
- CLONE_VM 子进程与父进程运行于相同的内存空间
- CLONE_PID 子进程在创建时PID与父进程一致
- CLONE_THREAD Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群
一个最简单的clone()调用示例(Linux内核3.8以上):
#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 */
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];
char* const container_args[] = {
"/bin/bash",
NULL
};
int container_main(void* arg)
{
printf("Container - inside the container!\n");
/* 直接执行一个shell,以便我们观察这个进程空间里的资源是否被隔离了 */
execv(container_args[0], container_args);
printf("Something's wrong!\n");
return 1;
}
int main()
{
printf("Parent - start a container!\n");
/* 调用clone函数,其中传出一个函数,还有一个栈空间的(为什么传尾指针,因为栈是反着的) */
int container_pid = clone(container_main, container_stack+STACK_SIZE, SIGCHLD, NULL);
/* 等待子进程结束 */
waitpid(container_pid, NULL, 0);
printf("Parent - container stopped!\n");
return 0;
}
使用gcc编译运行:
这里clone类似pthread。 父子进程在进程空间没什么差别,子进程可以访问父进程的资源。
UTS Namespace
下面对代码简单修改:
int container_main(void* arg)
{
printf("Container - inside the container!\n");
sethostname("container",10); /* 设置hostname */
execv(container_args[0], container_args);
printf("Something's wrong!\n");
return 1;
}
int main()
{
printf("Parent - start a container!\n");
int container_pid = clone(container_main, container_stack+STACK_SIZE,
CLONE_NEWUTS | SIGCHLD, NULL); /*启用CLONE_NEWUTS Namespace隔离 */
waitpid(container_pid, NULL, 0);
printf("Parent - container stopped!\n");
return 0;
}
运行效果:(需要root权限)
当运行程序后,子进程的hostname变成了container,exit退出后hostname显示的是主机的名称。
本文参考:
- https://lwn.net/Articles/531114/
- https://coolshell.cn/articles/17010.html
- 《容器云运维实战》 电子工业出版社