如何实现自己的linux container?
dy223
10年前
最近docker比较火爆, 我也研究了一下。docker 只是一个工具,container 技术的核心还是linux 内核的cgroup + chroot + namespace 技术。
本文主要讲解利用这三个技术实现自己的container。
chroot 没什么好说的,就是将根目录设置成另外一个目录。
namespace 有以下几种:
这几个flag 可以在调用clone时候作为参数传入,从而实现namespace的隔离,
从这个角度来说,container跟主要是进程角度的隔离,而不是传统的虚拟机,
因为它底层用用的同一个内核来调度。
cgroup 是linux 内核的另外一个控制和隔离进程的特性,他分为cpu ,memory,net,io等几个子系统,从而实现对进程cpu,内存,磁盘,网络等资源使用的控制。
制作自己容器,需要一个image ,可以从网上下一个,也可以自己制作,制作很简单,新装一个操作系统,安装一些需要用到的软件包,然后用tar 制作 / 目录下的压缩包,去掉一些虚拟文件系统的文件,本文用的是自己制作的centos 6.5 的image。
容器实现过程可以归纳为
1, 用clone系统调用 创建子进程,传入namespace的那几个参数,实现namespace的隔离
2, 父进程中创建veth pair ,一个veth在自己的namespace,将另一个设置为子进程的namespace,实现container和宿主机的网络通信
3, 父进程创建cgroup memory和cpuset子系统,将子进程attach到cgroup子系统上,实现container 的资源限制和隔离
4, 子进程在自己的namespace里,设置主机名,mount proc虚拟文件系统,设置veth ip,chroot到centos 6镜像的位置, 最终将进程镜像替换成/bin/bash
5, 父进程调用waitpid 等待子进程退出
最终代码见最后
编译代码 gcc -lcgroup mydocker.c -o mydocker
我们来验证一下结果
可以看到宿主机用的是centos 7的操作系统, 进入到container里面,宿主机用的是centos6,但用的都是3.10 的内核,根目录下的文件也不同。 hostname 宿主机为
localhost,而container里面为mydocker , 说明UTS namespace 隔离成功。
再看看 container 里面:
网络方面有回环网络卡lo和veth1,veth1 169.254.1.2 能ping 通veth0的地址(宿主机上的veth)169.254.2.1,如果在外面加iptables 做nat 转换的话,container里面还可以和外面通信。我们看不到外面宿主机的eth0 和 eth1,说明container 的network namespace 隔离成功。
目前container 里面只有/bin/bash , 且进程号为 1,不是我们常见的init进程,或者systemd 。因为/bin/bash 为该namespace 下的第一个进程,说明我们的pid namespace隔离成功。
mount 自由一些基本的文件系统,和宿主机的不一样,说明mount namespace隔离成功。
我们在来看看cgroup的隔离情况,
从cgroup文件系统来看,memory限制的是我们设置的512M,cpu使用0-1号,
我们来实际测试一下: 可以看出,只有0号和1号cpu idle为0 ,其他的都接近100%,说明cgroup隔离效果是很好的。
我们的容器基本上是完成了。不可否认docker 是很好的工具,但这一切还是要归功于linux强大的内核。本文若有错误之处,请指出。
完整代码:
#define _GNU_SOURCE #include <sys/types.h> #include <sys/wait.h> #include <stdio.h> #include <sched.h> #include <signal.h> #include <unistd.h> #include <errno.h> #include <stdlib.h> #include <sys/mount.h> #include <libcgroup.h> #include <time.h> #include <signal.h> #define STACK_SIZE (1024 * 1024) #define MEMORY_LIMIT (512*1024*1024) const char* rootfs = "/data1/centos6/rootfs/"; //centos6 镜像位置 const char* hostname = "mydocker"; //container 主机名 static char child_stack[STACK_SIZE]; char* const child_args[] = { "/bin/bash", NULL }; int pipe_fd【2】; //父子进程同步 int child_main(void* args) { char c; printf("In child process(container)\n"); chroot(rootfs); //用chroot 切换根目录 if(errno != 0){ perror("chroot()"); exit(1); } //clone 调用中的 CLONE_NEWUTS起隔离主机名和域名的作用 sethostname(hostname, sizeof(hostname)); if( errno != 0 ){ perror("sethostname()!"); exit(1); } //挂载proc子系统,CLONE_NEWNS 起隔离文件系统作用 mount("proc", "/proc", "proc", 0, NULL); if (errno != 0){ perror("Mount(proc)"); exit(1); } //切换的根目录 chdir("/"); close(pipe_fd【1】); read(pipe_fd【0】, &c, 1); //设置veth1 网络 system("ip link set lo up"); system("ip link set veth1 up"); system("ip addr add 169.254.1.2/30 dev veth1"); //将子进程的镜像替换成bash execv(child_args[0], child_args); return 1; } struct cgroup* cgroup_control(pid_t pid){ struct cgroup *cgroup = NULL; int ret; ret = cgroup_init(); char* cgname = malloc(19*sizeof(char)); if (ret) { printf("error occurs while init cgroup.\n"); return NULL; } time_t now_time = time(NULL); sprintf(cgname, "mydocker_%d", (int)now_time); printf("%s\n", cgname); cgroup = cgroup_new_cgroup(cgname); if( !cgroup ){ ret = ECGFAIL; printf("Error new cgroup%s\n", cgroup_strerror(ret)); goto out; } //添加cgroup memory 和 cpuset子系统 struct cgroup_controller *cgc = cgroup_add_controller(cgroup, "memory"); struct cgroup_controller *cgc_cpuset = cgroup_add_controller(cgroup, "cpuset"); if ( !cgc || !cgc_cpuset ){ ret = ECGINVAL; printf("Error add controller %s\n", cgroup_strerror(ret)); goto out; } // 内存限制 512M if( cgroup_add_value_uint64(cgc, "memory.limit_in_bytes", MEMORY_LIMIT) ){ printf("Error limit memory.\n"); goto out; } //限制只能使用0和1号cpu if ( cgroup_add_value_string(cgc_cpuset, "cpuset.cpus", "0-1") ){ printf("Error limit cpuset cpus.\n"); goto out; } //限制只能使用0和1块内存 if ( cgroup_add_value_string(cgc_cpuset, "cpuset.mems", "0-1") ){ printf("Error limit cpuset mems.\n"); goto out; } ret = cgroup_create_cgroup(cgroup, 0); if (ret){ printf("Error create cgroup%s\n", cgroup_strerror(ret)); goto out; } ret = cgroup_attach_task_pid(cgroup, pid); if (ret){ printf("Error attach_task_pid %s\n", cgroup_strerror(ret)); goto out; } return cgroup; out: if (cgroup){ cgroup_delete_cgroup(cgroup, 0); cgroup_free(&cgroup); } return NULL; } int main() { char* cmd; printf("main process: \n"); pipe(pipe_fd); if( errno != 0){ perror("pipe()"); exit(1); } int child_pid = clone(child_main, child_stack + STACK_SIZE, \ CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD, NULL); struct cgroup* cg = cgroup_control(child_pid); //添加veth pair ,设置veth1 namespace 为子进程的,veth0 在父进程的namespace //linl3 实现起来太繁琐,借用命令行工具ip 实现 system("ip link add veth0 type veth peer name veth1"); asprintf(&cmd, "ip link set veth1 netns %d", child_pid); system(cmd); system("ip link set veth0 up"); system("ip addr add 169.254.1.1/30 dev veth0"); free(cmd); //等执行以上命令,通知子进程,子进程设置自己的网络 close(pipe_fd【1】); waitpid(child_pid, NULL, 0); if (cg) { cgroup_delete_cgroup(cg, 0); //删除cgroup 子系统 } printf("child process exited.\n"); return 0; }来自:http://weibo.com/p/1001603824282965777334