Contents

从0到1写docker之三构建简易镜像

(三)构建简易镜像:

补充知识: rootfs(root filesystem):

  • rootfs是分层文件树的顶端,包含对系统运行至关重要的文件和目录,包括设备目录和用于启动系统的程序。系统启动时,初始化进程会将rootfs挂载到/目录,之后再挂载其他文件系统到其子目录。

mount namespace 工作原理:

  • 每个进程可以创建属于自己的 mount table,但前提是必须先复制父进程的 mount table,之后再调用 mount 发生的更改都只会影响当前进程的 mount table

pivot_root系统调用介绍:

  • pivot_root 是由 Linux 提供的一种系统调用,它能够将一个 mount namespace 中的所有进程的根目录和当前工作目录切换到一个新的目录。pivot_root 的主要用途是在系统启动时,先挂载一个临时的 rootfs 完成特定功能,然后再切换到真正的 rootfs。
  • 可以将当前root文件系统移动到put_old文件夹中,然后将new_root成为新的root文件系统(注:new_root和put_old不能同时存在当前root的同一个文件系统中)

pivot_root与chroot区别:

  • chroot只改变某个进程的根目录,系统的其他部分依旧运行于旧的root目录。 pivot_root把整个系统切换到一个新的root目录中,然后去掉对之前rootfs的依赖,以便于可以umount之前的文件系统。
1.使用busybox来构建极简镜像

项目中使用privot_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
31
32
// pivotRoot 进行pivot_root系统调用
func pivotRoot(root string )error{
	
	//为了使当前root文件系统的老root文件系统和新root文件系统不在同一个文件系统下,这里把root重新mount一次
	//bind mount 就是把相同的内容换一个挂载点的挂载方式
	err := syscall.Mount(root,root,"bind",syscall.MS_BIND |syscall.MS_REC,"")
	if err != nil{
		return errors.New("Mount rootfs to itself failed,error:"+err.Error())
	}
	//存储旧root文件系统
	pivotDir := filepath.Join(root,".pivot_root")
	err = os.Mkdir(pivotDir,0777)
	if err != nil{
		return err
	}
	//root 为新root文件系统,pivotDir代表put_old文件夹,将旧root文件系统放在pivotDir文件夹中
	err = syscall.PivotRoot(root,pivotDir)
	if err != nil {
		return errors.New("pivot_root,error :"+err.Error())
	}
	err = syscall.Chdir("/")
	if err != nil{
		return errors.New("chdir,error:"+err.Error())
	}
	//此时的pivotDir就是刚刚存放旧的root文件系统的文件夹
	pivotDir = filepath.Join("/",".pivot_root")
	err = syscall.Unmount(pivotDir,syscall.MNT_DETACH)
	if err != nil{
		return errors.New("umount pivot_roo5t directory failed ,error :"+err.Error())
	}
	return os.Remove(pivotDir)
}

再将原先在InitProcess函数中进行mount操作移到setUpMount函数中,同时进行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
func setUpMount(){
	pwd ,err := os.Getwd()
	if err != nil{
		log.Println("get current location error:"+err.Error())
		return 
	}
	log.Println("the current location is "+pwd)
	
	err = syscall.Mount("","/","",syscall.MS_REC | syscall.MS_PRIVATE,"")
	if err != nil{
		log.Println("the first mount failed,error:",err.Error())
	}
	err = pivotRoot(pwd)
	if err != nil{
		log.Println("pivot_root system call failed")
	}
	//mount proc
	defaultMountFlag := syscall.MS_NODEV | syscall.MS_NOSUID | syscall.MS_NOEXEC 
	// 这里的 MountFlag 的意思如下。
	// 1. MS_NOEXEC在本文件系统中不允许运行其他程序。
	// 2. MS_NOSUID在本系统中运行程序的时候, 不允许 set-user-ID或 set-group-ID。
	// 3. MS_NODEV这个参数是自从Linux2.4以来,所有mount的系统都会默认设定的参数。
	syscall.Mount("proc","/proc","proc",uintptr(defaultMountFlag),"")
	//mount tmpfs
	syscall.Mount("tmpfs","/dev","tmpfs",syscall.MS_NOSUID | syscall.MS_STRICTATIME,"mode=755")
}

这里挂载到/,可以使后面挂载的/proc在退出容器时自动umount /proc,因为这样可以声明这个新的mount namespace独立 (注!!!:这个/挂载,必须要在所有挂载之前)

现在InitProcess函数就是这个样子:

 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
func InitProcess() error {

	data := readUserCommand()
	if  len(data) ==0 {
		return errors.New("Run container get command failed")
	}
	setUpMount() //将mount封装
	
	log.Println("mount success")
	//通过exec.LookPath找到命令在环境变量中路径
	cmdpath, err := exec.LookPath(data[0])
	if err != nil {
		log.Println(data[0]," look path in PATH environment variable failed")
		return err
	}

	err = syscall.Exec(cmdpath, data[0:], os.Environ())
	//!!!最重要的操作
	//syscall.Exec这个方法,
	//其实最终调用了Kernel的intexecve(const char *filename,char *const argv[], char *const envp[]);
	//这个系统函数。它的作用是执行当前 filename对应的程序。
	//它会覆盖当前进程的镜像、数据和堆栈等信息,包括 PID, 这些都会被将要运行的进程覆盖掉。
	//保证了我们进入容器之后,我们看到的第一个进程是我们指定的进程,因为之前的信息都被覆盖掉了
	if err != nil {
		log.Fatal("error :", err.Error())
	}
	log.Println("exec  success")
	return nil
}

完成后的结果就是这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 sh
    7 root      0:00 ps
/ # mount
/dev/sdc on / type ext4 (rw,relatime,discard,errors=remount-ro,data=ordered)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
tmpfs on /dev type tmpfs (rw,nosuid,mode=755)
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var

此时子进程就看不到父进程的mount信息,且rootfs切换成了我们设置的busybox

2.使用union filesystem来包装镜像

这里选择使用overlayFS,作为unionfs

相较于aufs,overlayFS的优势:

  • 速度更快,aufs层数更多,性能损耗更大
  • 简单,overlay2只有两层,容器层upper和镜像层lower
  • overlay2加入了linux kernel

相较于overlay驱动,overlay2驱动的优势:

  • overlay驱动只在一个lower overlayFS层之上,所以为了实现多层镜像需要大量的硬链接
  • overlay2驱动原生支持多个lower overlayFS

组成:

  • lower:镜像层,存储镜像文件,且只能读不能写
  • upper:容器层,可读可写,写时复制时,就是将需要写入的文件复制到upper中,进行修改,后续就直接在复制的文件中修改
  • merged:挂载的文件,展示参与联合挂载的目录的文件
  • work:主要是用来保证操作的原子性

通过命令来展示overlayFS的挂载,挂载前只有lower和upper中有文件,merged为空,挂载后merged展示了参与联合挂载的目录文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
root@localhost:~/test# mount -t overlay overlay ./merged -o upperdir=./upper,lowerdir=./lower,workdir=./work
root@localhost:~/test# ls
lower  merged  upper  work
root@localhost:~/test# tree
.
├── lower
│   └── lll
├── merged
│   ├── lll
│   └── uuu
├── upper
│   └── uuu
└── work

(这里我们还是使用busybox来作为镜像) 创建upper,lower,work,merged目录,同时挂载overlayFS:

  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
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
func NewWorkSpace() {
	_, err := os.Stat("/root/overlay")
	if os.IsNotExist(err) {
		if err := os.Mkdir("/root/overlay", 0777); err != nil {
			log.Println("can't create a new work space,error:", err.Error())
			return
		}

	}
	NewUpper("/root/overlay")
	NewLower("/root/overlay")
	NewWork("/root/overlay")
	NewMerged("/root/overlay")
}

// 镜像层
func NewLower(rootURL string) {
	lowerPath := path.Join(rootURL, "lower")
	_, err := os.Stat(lowerPath)
	if err == nil {
		log.Println("lower is nolmal")

	}
	if os.IsNotExist(err) {
		err = os.Mkdir(lowerPath, 0777)
		if err != nil {
			log.Println("can't create the lower,error:", err.Error())
			return
		}
		if _, err := exec.Command("tar", "-xvf", path.Join("/root", "busybox.tar"), "-C", lowerPath).CombinedOutput(); err != nil {
			log.Println("can't tar the target file")
			return
		}
	}
	if err != nil && !os.IsNotExist(err) {
		log.Println("can't judge the lower directory'state")
	}

}

// 容器层
func NewUpper(rootURL string) {
	upperPath := path.Join(rootURL, "upper")
	_, err := os.Stat(upperPath)
	if err == nil {
		log.Println("upper already exists")

	}
	if os.IsNotExist(err) {
		err := os.Mkdir(upperPath, 0777)
		if err != nil {
			log.Println("can't create the upper,error:", err.Error())
			return
		}
	}
	if err != nil && !os.IsNotExist(err) {
		log.Println("can't judge the upper directory's state")

	}

}
func NewWork(rootURL string) {
	workPath := path.Join(rootURL, "work")
	_, err := os.Stat(workPath)
	if err == nil {
		log.Println("work already exists")

	}
	if os.IsNotExist(err) {
		err := os.Mkdir(workPath, 0777)
		if err != nil {
			log.Println("can't create the work,error:", err.Error())
			return
		}
	}
	if err != nil && !os.IsNotExist(err) {
		log.Println("can't judge the work file's state")
	}

}
func NewMerged(rootURL string) {
	mergedPath := path.Join(rootURL, "merged")
	_, err := os.Stat(mergedPath)
	if err != nil {
		if os.IsNotExist(err) {
			err := os.Mkdir(mergedPath, 0777)
			if err != nil {
				log.Println("can't create the merged ,error:", err.Error())
				return
			}
		} else {
			log.Println("can't judge the merged file's state")
			return
		}
	}

	dir := "upperdir=" + path.Join(rootURL, "upper") + ",lowerdir=" + path.Join(rootURL, "lower") + ",workdir=" + path.Join(rootURL, "work")
	log.Println("the dir is :--->", dir)
	cmd := exec.Command("mount", "-o", dir, "-t", "overlay", "overlay", path.Join(rootURL, "merged"))
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		log.Println("overlayFS mount failed ,error :", err)
	}
}

取消挂载,并删除其他目录,保留镜像目录:

 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
36
func DeleteWorkSpace(){
	DeleteMerged("/root/overlay")
	DeleteUpper("/root/overlay")
	DeleteWork("/root/overlay")
}

func DeleteMerged(rootURL string){
	mergedPath := path.Join(rootURL,"merged")
	cmd := exec.Command("umount",mergedPath)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run();err != nil{
		log.Println("umount overlay failed")
	}
	log.Println("umount overlay successful...")
	if err := os.RemoveAll(mergedPath);err != nil{
		log.Println("remove merged directory failed")
		return
	}
}
func DeleteUpper(rootURL string){
	upperPath := path.Join(rootURL,"upper")
	if err := os.RemoveAll(upperPath);err != nil{
		log.Println("remove upper diroctory failed")
		return
	}

}

func DeleteWork(rootURL string){
	workPath := path.Join(rootURL,"work")
	if err := os.RemoveAll(workPath);err != nil{
		log.Println("remove work diroctory failed")
		return
	}
}

通过cmd.Dir把overlay的挂载点作为容器的根目录

3.使用volume数据卷挂载数据持久化

docker中的volume挂载时,已经启动了容器进程,此时已经有了mount namespace,但是现在未进行pivot_root系统调用或者chroot,在容器中还是可以观察到宿主机的全部文件系统,我们只需要在进行pivot_root系统调用前,将宿主机上的目录挂载到指定容器目录

对于volume的参数校验就不展示了,这里仅展示挂载数据卷:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func VolumeMount(rootURL string, volume []string) {
	//创建宿主机目录
	if err := os.Mkdir(volume[0], 0755); err != nil {
		log.Println("the state of the ", volume[0], "in host is :", err.Error())
	}

	mergedPath := path.Join(rootURL, "merged")
	target := path.Join(mergedPath, volume[1])
	//创建容器目录
	if err := os.Mkdir(target, 0755); err != nil {
		log.Println("the state of the ", volume[1], "in container is :", err.Error())
	}
	cmd := exec.Command("mount", "-o", "bind", volume[0], target)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		log.Println("mount volume failed,error:", err.Error())
		return
	}
	log.Printf("mount %s to %s successful!\n", volume[0], target)
}

注意数据卷和文件系的顺序:

  • 挂载:先进行文件系统的挂载,最后进行数据卷的挂载。
  • 取消挂载:先进行数据卷的取消挂载,再进行文件系统的取消挂载
4.简单的容器打包

添加commit命令,将容器的所有文件打包成tar包:

1
2
3
4
5
func CommitContainer (imageName string){
	if _,err := exec.Command("tar","-czf","/root/"+imageName+".tar","-C","/root/overlay/merged",".").CombinedOutput();err != nil{
		log.Println("commit failed")
	}
}
参考文档

《自己动手写docker》

https://waynerv.com/posts/container-fundamentals-filesystem-isolation-and-sharing/

https://cloud.tencent.com/developer/article/1681523

https://www.cnblogs.com/FengZeng666/p/14173906.html

https://blog.csdn.net/luckyapple1028/article/details/78075358

https://zhuanlan.zhihu.com/p/374924046

https://www.cnblogs.com/istitches/p/18011539