如何实现自己的linux container?

dy223 9年前

最近docker比较火爆, 我也研究了一下。docker 只是一个工具,container 技术的核心还是linux 内核的cgroup + chroot + namespace 技术。
本文主要讲解利用这三个技术实现自己的container。
chroot  没什么好说的,就是将根目录设置成另外一个目录。
namespace  有以下几种:
如何实现自己的linux container? 
这几个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
我们来验证一下结果
如何实现自己的linux container?
可以看到宿主机用的是centos 7的操作系统, 进入到container里面,宿主机用的是centos6,但用的都是3.10 的内核,根目录下的文件也不同。 hostname 宿主机为
localhost,而container里面为mydocker , 说明UTS namespace 隔离成功。
再看看 container 里面:
如何实现自己的linux 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隔离成功。
如何实现自己的linux container? 
mount 自由一些基本的文件系统,和宿主机的不一样,说明mount namespace隔离成功。
我们在来看看cgroup的隔离情况,
从cgroup文件系统来看,memory限制的是我们设置的512M,cpu使用0-1号,
如何实现自己的linux container?我们来实际测试一下:  如何实现自己的linux container? 可以看出,只有0号和1号cpu idle为0 ,其他的都接近100%,说明cgroup隔离效果是很好的。 如何实现自己的linux container?
我们的容器基本上是完成了。不可否认docker 是很好的工具,但这一切还是要归功于linux强大的内核。本文若有错误之处,请指出。

</div>

完整代码:
#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