lxd与lxc挂卷过程对比分析
LXD挂卷流程
在lxd环境中执行如下命令时:
lxc config device add test-centos sdb unix-block path=/dev/sdb
lxd的整个处理流程如下:
步骤1:lxc client先向lxd daemon发送一个put请求,更新当前容器的config,调用链如下:
lxc/config.go: (c *configCmd) run(config *lxd.Config, args []string) lxc/config.go: (c *configCmd) deviceAdd(config *lxd.Config, which string, args []string) client.go: (c *Client) ContainerDeviceAdd(container, devname, devtype string, props []string) client.go: (c *Client) put(base string, args interface{}, rtype api.ResponseType)
步骤2:lxd daemon在收到请求后,通过与原有config进行对比,得到addDevices,然后开始挂卷,调用链如下:
lxd/container_put.go: containerPut(d *Daemon, r *http.Request) lxd/container_lxc.go: (c *containerLXC) Update(args containerArgs, userRequested bool) lxd/container_lxc.go: (c *containerLXC) insertUnixDevice(name string, m types.Device)
步骤3:在insertUnixDevice函数中,lxd会先调用createUnixDevice函数在”/var/lib/lxd/devices/<container_name>/”目录下创建一个与待挂载设备具有相同major和minor号的块设备。
步骤4:创建好块设备后,insertUnixDevice函数会调用insertMount函数,完成挂载操作。具体步骤如下:
4.1、通过调用mount这个linux系统调用,并将flag设置为MS_BIND,lxd会在/var/lib/lxd/shmounts/<container_name>目录下创建一个临时文件,该文件为在步骤3中创建的块设备的复制文件,具体关系参见linux对mount调用中关于MS_BIND这个flag的描述:http://man7.org/linux/man-pages/man2/mount.2.html。
需要注意的是,/var/lib/lxd/shmounts/<container_name>这个目录具有一定的特殊性,它被挂载到了容器内部的/dev/.lxd-mounts目录下(lxd/container_lxc.go: (c *containerLXC) initLXC()函数中的err = lxcSetConfigItem(cc, "lxc.mount.entry", fmt.Sprintf("%s dev/.lxd-mounts none bind,create=dir 0 0", shared.VarPath("shmounts", c.Name())))),因此lxd这里创建的临时文件在容器中也能看到。
4.2、调用forkmount函数,在容器内部进行mount。forkmount函数的实现在lxd/main_nsexec.go文件中,是通过C语言实现的。
forkmount函数中,首先将user namespace与mount namespace切换到该容器的namespace中,然后根据目标路径创建file或dir(根据4.1中创建的临时文件的类型来定),最后通过mount调用以及MS_MOVE flag将步骤1中复制的块设备文件move到目标路径(当前从用户参数中的path字段获取,目前没有弄清楚为何不从用户参数的target字段获取。)。
至此,宿主机上的块设备已经可以在容器中看到了。
LXC挂卷流程
不过上述操作是用户执行挂载命令时lxd的内部流程,当容器重启后,lxd会对这些挂载设备进行恢复,这里走的就是另一条流程了,具体流程如下:
步骤1:lxc client向lxd daemon发送一个start请求,调用链如下:
lxd/action.go: (c *actionCmd) run(config *lxd.Config, args []string) client.go: (c *Client) Action(name string, action shared.ContainerAction, timeout int, force bool, stateful bool)
步骤2:lxd daemon在收到请求后,根据数据库中记录生成配置文件,作为启动容器的参数,调用链如下:
lxd/container_state.go: containerStatePut(d *Daemon, r *http.Request) client.go: (c *Client) Action(name string, action shared.ContainerAction, timeout int, force bool, stateful bool) lxd/container_lxc.go: (c *containerLXC) Start(stateful bool) lxd/container_lxc.go: (c *containerLXC) startCommon() lxd/container_lxc.go: (c *containerLXC) initLXC()
在initLXC函数中,各个device会被转化为配置文件中的lxc.mount.entry参数。
在最初执行的那个“lxc config device add”命令挂载的磁盘会转化为如下的配置项:
lxc.mount.entry = /var/lib/lxd/devices/test-centos/unix.dev-sdb dev/sdb none bind,create=file
其中,unix.dev-sdb就是在上面的步骤3中createUnixDevice函数中创建的块设备,dev/sdb是用户命令中的path字段,none和bind,create=file的含义后面会提到。
步骤3:lxc在启动容器时,对lxc.mount.entry参数的处理在文件conf.c中,调用链如下:
conf.c: lxc_setup(struct lxc_handler *handler) conf.c: setup_mount_entries(const struct lxc_rootfs *rootfs, struct lxc_list *mount, const char *lxc_name, const char *lxc_path) conf.c: make_anonymous_mount_file(struct lxc_list *mount) conf.c: mount_file_entries(const struct lxc_rootfs *rootfs, FILE *file, const char *lxc_name, const char *lxc_path) lxcmntent.c: getmntent_r (FILE *stream, struct mntent *mp, char *buffer, int bufsiz) conf.c: mount_entry_on_relative_rootfs(struct mntent *mntent, const struct lxc_rootfs *rootfs, const char *lxc_name, const char *lxc_path) conf.c: mount_entry_on_generic(struct mntent *mntent, const char* path, const struct lxc_rootfs *rootfs, const char *lxc_name, const char *lxc_path) conf.c: mount_entry_create_dir_file(const struct mntent *mntent, const char* path, const struct lxc_rootfs *rootfs, const char *lxc_name, const char *lxc_path) conf.c: cull_mntent_opt(struct mntent *mntent) conf.c: parse_mntopts(const char *mntopts, unsigned long *mntflags, char **mntdata) conf.c: mount_entry(const char *fsname, const char *target, const char *fstype, unsigned long mountflags, const char *data, int optional, int dev, const char *rootfs) utils.c: safe_mount(const char *src, const char *dest, const char *fstype, unsigned long flags, const void *data, const char *rootfs) utils.c: open_without_symlink(const char *target, const char *prefix_skip)
其中需要注意的有:
3.1、lxc_setup在调用setup_mount_entries提供的mount_list中存储的就是lxc配置文件中所有lxc.mount.entry的值(在confile.c: config_mount(const char *key, const char *value, struct lxc_conf *lxc_conf){函数中构造)
3.2、make_anonymous_mount_file函数将所有的mount_entries的内容写到了一个内存文件中,供mount_file_entries函数使用。
3.3、getmntent_r函数用于将mount_entries的内容解析为如下结构体:
struct mntent { char* mnt_fsname; char* mnt_dir; char* mnt_type; char* mnt_opts; int mnt_freq; int mnt_passno; };
以步骤2中生成的配置项为例,该配置项会被解析为如下内容:
struct mntent mp; mp.mnt_fsname=/var/lib/lxd/devices/test-centos/unix.dev-sdb; mp. mnt_dir= dev/sdb; mp.mnt_type= none; mp.mnt_opts= bind,create=file; mp.mnt_freq=0; mp.mnt_passno=0;
3.4、mount_entry_on_relative_rootfs在调用mount_entry_on_generic时,会将rootfs与mnt_dir拼接起来,组成mnt_dir在宿主机上的绝对路径,作为path参数传给mount_entry_on_generic。
3.5、mount_entry_create_dir_file在path对应的路径上创建一个file或dir(根据mnt_opts中的create=file或create=dir来决定),这一步与上面的4.2步forkmount中的创建操作一致。
3.6、cull_mntent_opt函数与parse_mntopts函数共同将mnt_opts转化为mntflags。
3.7、safe_mount函数中调用的open_without_symlink函数的作用仅仅是打开待挂载路径,得到一个文件句柄(通过校验保证待挂载路径不是文件链接)。
3.8、safe_mount函数最后调用mount调用,完成第一次挂载。需要注意的是,调用mount调用时,并没有直接使用待挂载路径作为dest参数,而是将/proc/self/fd/与3.7中得到的文件句柄拼接起来,作为mount调用的dest参数。这块有点不明觉厉。
3.9、mount_entry在调用safe_mount函数完成第一次挂载后,再次直接调用mount调用进行第二次挂载,两次挂载的区别在于第一次去掉了MS_REMOUNT flag,第二次加上了MS_REMOUNT flag。这块也有点不明觉厉。
对比lxd的挂卷过程与lxc的挂卷过程,可以发现相比于lxd,lxc的挂卷过程少了一次namespace切换,也少了一次带MS_MOVE flag的mount操作(lxd的挂卷过程是先将devices目录下的unix.dev-sdb通过bind mount方式mount到shmounts目录下,然后通过move mount方式mount到dev目录下,lxc则直接将devices目录下的unix.dev-sdb通过bind mount方式mount到dev目录下)。
- 点赞
- 收藏
- 关注作者
评论(0)