本文为容器化技术系列文章的第二篇。本文将把容器技术与文件系统,压缩文件相结合,并且实现commit命令,用于保存镜像文件。
 
前置知识 
容器镜像与持久化 容器镜像 overlay文件系统   从本文开始,我们将把参考资料《自己动手写docker》的原版中实现的镜像构造进行改造,其中最根本原因是原书采用了AUFS,而在笔者的系统上,没有AUFS,因而采用了docker也在使用的overlayfs进行替代。   overlayfs与AUFS类似,在linux 3.18内核便被支持。overlay允许一个读写目录树覆盖到另一个只读目录树上,所有对文件的修改都在上层的可写层进行。它与其他的UFS不同,在打开文件后的所有操作都转换到底层或者上层的文件系统。这使得文件系统的实现得到简化。   overlay简单的用法如下所示。  
1 mount -t overlay overlay -o lowerdir=/lower,upperdir=/upper,workdir=/work /merged 
 
  从上述的指令可以看出,指令指定了底层文件只读层,可以有多层;上层文件可写层;工作文件是提供给文件系统便于生成。merged则是最后合并的挂载目录。注意底层,顶层与工作的目录的文件系统,overlay在这方面有很多限制。   在执行完成上述挂载指令后,我们可以在文件夹中进行读写文件的尝试。具体不在本文中呈现。下面我们将其挂入容器系统之中。  
busybox基础镜像   这里我们采用busybox作为基础的镜像。busybox是一个精简的镜像,包含了很多UNIX环境的常用命令。busybox可以使用docker export得到tar,并且解压得到新的rootfs。   我们实现的基础思路是:使用pivot_root将整个root文件系统移动进busybox所提供的目录,使用overlay将其创建为只读层,然后创建只写层,提供给容器进行运行操作。在容器结束运行后,还原这一切。   首先,我们将busybox解压,得到其rootfs。    
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func  CreateReadOnlyLayer (rootURL string )  error   {   	busyboxURL := rootURL + "busybox/"  	busyboxTarURL := rootURL + "busybox.tar"  	exist, err := PathExists(busyboxURL) 	if  err != nil  { 		return  fmt.Errorf("path exist error:%v" , err) 	} 	if  exist == false  { 		if  err := os.Mkdir(busyboxURL, 0777 ); err != nil  { 			return  fmt.Errorf("mkdir root error: %v" , err)     }      		if  _, err := exec.Command("tar" , "-vxf" , busyboxTarURL, "-C" , busyboxURL).CombinedOutput(); err != nil  { 			return  fmt.Errorf("tar error: %v" , err) 		} 	} 	return  nil  } 
 
  然后,创建读写层与工作层,将其挂入目录。此处不必多说。   之后,在mount各种需要的东西之前,我们要使用pivot_root系统调用,将整个系统的根文件移动进入该文件夹。  
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 func  pivotRoot (root string )  error   {  fmt.Println("pivot rooting" )    	if  err := syscall.Mount(root, root, "bind" , syscall.MS_BIND|syscall.MS_REC, "" ); err != nil  { 		return  fmt.Errorf("mount bind error: %v" , err) 	} 	pivotDir := filepath.Join(root, ".pivot_root" ) 	if  err := os.Mkdir(pivotDir, 0777 ); err != nil  { 		return  fmt.Errorf("mkdir error: %v" , err) 	} 	if  err := syscall.Mount("" , "/" , "" , syscall.MS_PRIVATE|syscall.MS_REC, "" ); err != nil  { 		fmt.Printf("mount / error: %v\n" , err) 		return  err 	} 	if  err := syscall.PivotRoot(root, pivotDir); err != nil  { 		return  fmt.Errorf("pivotroot error: %v" , err) 	} 	if  err := syscall.Chdir("/" ); err != nil  { 		return  fmt.Errorf("chdir error: %v" , err) 	}    	pivotDir = filepath.Join("/" , ".pivot_root" ) 	if  err := syscall.Unmount(pivotDir, syscall.MNT_DETACH); err != nil  { 		return  fmt.Errorf("umount error: %v" , err) 	} 	return  os.Remove(pivotDir) } 
 
  在上述函数中,我们首先为了使得老root和新root不再一个文件系统下,将root重新mount了一次,使得新老root不在同一个文件系统下。然后进行系统调用,更改当前目录,删除临时文件。   在此之后,就像之前的文件系统一样,我们挂载必要的,如proc等。我们得到了以根目录为虚拟环境的容器进程。   至于卸载方面,我们只需要umount我们所挂载的目录,并且删除文件夹,即可。在此也不过多阐述。   但我们需要的不仅是虚拟的环境,我们还有持久化的需求。这里我们引入数据卷的概念,来存放我们持久化的数据。  
持久化 mount –bind   就像标题所写,这里我们采用mount –bind的方法来进行持久化。mount –bind相当于将两个目录连接起来。我们可以在得到持久化的目录名后,使用bind将容器外的目录与容器内的目录连接起来。同时在最后退出的时候,先卸载对应目录,此时文件便留在了容器外的目录。我们便实现了持久化的目标。   同时我们要注意,我们需要增加新的command选项,解析字符串,在这里便不过多展示。  
volume代码实现   这里展示的仅仅是开始mount和结束umount的过程,增加命令与解析由于篇幅暂不展示。原理如上述所说。由于go的异常处理,代码会显得颇为冗杂。但本意如上所示。  
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 func  MountVolume (rootURL string , mntURL string , volumeURLs []string )  error   {   	parentURL := volumeURLs[0 ] 	if  err := os.Mkdir(parentURL, 0777 ); err != nil  { 		if  exist, _ := PathExists(parentURL); exist == false  { 			return  fmt.Errorf("error in mkdir parentURL: %v" , err) 		} 	} 	containerURL := volumeURLs[1 ] 	containerVolumeURL := mntURL + containerURL 	fmt.Println(containerVolumeURL) 	if  err := os.Mkdir(containerVolumeURL, 0777 ); err != nil  { 		return  fmt.Errorf("error in mkdir containerVolumeURL: %v" , err)   }    	cmd := exec.Command("mount" , "--bind" , parentURL, containerVolumeURL) 	cmd.Stderr = os.Stderr 	cmd.Stdout = os.Stdout 	if  err := cmd.Run(); err != nil  { 		return  fmt.Errorf("Mount volume error :%v" , err) 	} 	return  nil  } func  DeleteVolumeMountPoint (mntURL string , volumnURL string )  error   {   	cmd := exec.Command("umount" , "-v" , mntURL+volumnURL) 	cmd.Stdout = os.Stdout 	cmd.Stderr = os.Stderr 	if  err := cmd.Run(); err != nil  { 		return  fmt.Errorf("delete mount volume error : %v" , err) 	} 	return  nil  } 
 
  在完成这些工作后,我们可以尝试数据卷是否工作正常。下面是本机的一个运行实例。可以看见,在完成数据卷工作后,数据确实得到了持久化。  
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 root@wqy:/media/wqy/新加卷/projekts/Go/src/mydocker# sudo ./mydockerdev run -ti -v /volume:/containerVolume sh start runCommand New parent mounting  /home/wqy/mnt//containerVolume [/volume /containerVolume] your command is sh start initCommand reading user command the command is sh mount start /home/wqy/mnt pivot rooting the current wd is / / # ls bin              etc              root             usr containerVolume  home             sys              var dev              proc             tmp / # cd containerVolume/ /containerVolume # ls a.txt /containerVolume # echo "sfsfsf" >b.txt /containerVolume # exit wqy@wqy:/volume$ cat b.txt sfsfsf 
 
镜像打包   镜像打包也是我们需要的一项功能。在这里我们提供一个简单的实现方法:在为退出前,将所有文件打包成tar保存到外面即可。   在具体实现上,我们还需要增加指令,解析参数。我们这里使用commit命令,在容器运行时候进行打包。  
1 2 3 4 5 6 7 8 9 10 func  commitContainer (imageName string )  error   {	mntURL := "/home/wqy/mnt/"  	rootURL := "/home/wqy/"  	imageTar := rootURL + imageName + ".tar"  	fmt.Println(imageTar) 	if  _, err := exec.Command("tar" , "-czf" , imageTar, "-C" , mntURL, "." ).CombinedOutput(); err != nil  { 		return  fmt.Errorf("tar error: %v " , err) 	} 	return  nil  } 
 
  这里提供的简单的打包解决方案。同样,解析命令的操作便不在此展示。   至此,我们实现了一些与容器文件相关的简单操作。  
下一步做什么    在下一步,我们将继续扩展容器的功能,实现一些常用的docker指令,使得我们的容器更加接近docker,可用性更强。在下一篇文章中,我们将探讨这些功能。  
总结与后记   这属于容器化技术第二篇文章。本文只介绍了一些简单的文件功能以及实现。文章较上一篇篇幅明显减少,主要原因还是实现的功能不太复杂,原理也不需要过多介绍。完成时间也较于上一篇文章所预期的时间晚了整整一个星期。这期间也有参加比赛,拖了一下的缘故。   写完本文主要是对文件的知识有所回顾。在本文中,我尝试了第一次自己设计解决方案,如使用bind,这是在原本书上没有写的。书上的解决方案在本机上无法实现,便自己想了一个。后来才发现,docker也使用了这样的技术。   下一篇技术含量会比本篇要更高。但预计时间也更长,可能是一个月左右,敬请期待。