1. 背景
容器虚拟化带来轻量高效,快速部署的同时,也因其隔离性不够彻底,给用户带来一定程度的使用不便。Docker容器由于Linux内核namespace本身还不够完善的现状(例如,没有cgroup namespace,user namespace也是在kernel高版本才开始支持, /dev设备不隔离等),因此docker容器在隔离性方面也存在一些缺陷。例如,在容器内部proc文件系统中可以看到Host宿主机上的proc信息(如:meminfo, cpuinfo,stat, uptime等)。本文基于开源项目LXCFS,尝试通过用户态文件系统实现docker容器内的虚拟proc文件系统,增强docker容器的隔离性,减少给用户带来的不便。
2. LXCFS简介及原理
2.1 LXCFS介绍
LXCFS是一个开源的FUSE用户态文件系统,起初是为了更好的在Ubuntu上使用LXC容器而设计开发。LXCFS基于C语言开发,代码托管在github上,目前较多使用在LXC容器中。主要由Ubuntu的工程师提供维护和开发。
LXCFS主要提供两个基本功能点:
- a cgroupfs compatible view for unprivileged containers
- a set of cgroup-aware files:
/proc/meminfo
/proc/cpuinfo
/proc/stat
/proc/uptime
2.2 LXCFS实现原理
LXCFS是基于FUSE实现而成的一套用户态文件系统,和其他文件系统最本质的区别在于,文件系统通过用户态程序和内核FUSE模块交互完成。Linux内核从2.6.14版本开始通过FUSE模块支持在用户空间实现文件系统。通过LXCFS的源码可以看到,LXCFS主要通过调用底层fuse的lib库libfuse和内核模块fuse交互实现成一个用户态的文件系统。此外,LXCFS涉及到对cgroup文件系统的管理则是通过cgmanager用户态程序实现(为了提升性能,从0.11版本开始,LXCFS自身实现了cgfs用以替换第三方的cgroup manager,目前已经合入upstream)。
2.2.1 LXCFS主进程
LXCFS实现的main函数非常简单。在其main函数中可以看到,运行lxcfs时,首先会通过cgfs_setup_controllers入口函数进行初始化,大致步骤:
-
创建运行时工作目录/run/lxcfs/controllers/
-
将tmpfs文件系统挂载在/run/lxcfs/controllers/
-
检查当前系统已挂载的所有cgroup子系统
-
将当前系统各个cgroup子系统重新挂载在/run/lxcfs/controllers/目录下然后调用libfuse库主函数fuse_main,指定一个用户态文件系统挂载的目标目录(例如:/var/lib/lxcfs/),并传递如下参数:
-s is required to turn off multi-threading as libnih-dbus isn't thread safe.
-f is to keep lxcfs running in the foreground
-o allow_other is required to have non-root user be able to access the filesystem
进入到libfuse之后,就是在fuse_loop()中接受内核态FUSE的请求。接下来就是频繁的完成用户态文件系统和内核FUSE交互,完成用户态文件系统操作。LXCFS文件系统具备编码实现可见struct fuse_operations定义的ops函数:
const struct fuse_operations lxcfs_ops = {
.getattr = lxcfs_getattr,
.readlink = NULL,
.getdir = NULL,
.mknod = NULL,
.mkdir = lxcfs_mkdir,
.unlink = NULL,
.rmdir = lxcfs_rmdir,
.symlink = NULL,
.rename = NULL,
.link = NULL,
.chmod = lxcfs_chmod,
.chown = lxcfs_chown,
.truncate = lxcfs_truncate,
.utime = NULL,
.open = lxcfs_open,
.read = lxcfs_read,
.release = lxcfs_release,
.write = lxcfs_write,
.statfs = NULL,
.flush = lxcfs_flush,
.fsync = lxcfs_fsync,
.setxattr = NULL,
.getxattr = NULL,
.listxattr = NULL,
.removexattr = NULL,
.opendir = lxcfs_opendir,
.readdir = lxcfs_readdir,
.releasedir = lxcfs_releasedir,
.fsyncdir = NULL,
.init = NULL,
.destroy = NULL,
.access = NULL,
.create = NULL,
.ftruncate = NULL,
.fgetattr = NULL,
};
其中lxcfs_opendir等就是用户态文件系统的具体实现。
2.2.2 Fuse介绍
Fuse是指在用户态实现的文件系统,是文件系统完全在用户态的一种实现方式。目前Linux通过内核模块FUSE进行支持。libfuse是用户空间的fuse库,可以被非特权用户访问。通常对文件系统内的文件进行操作,首先会通过内核VFS接口,转发至各个具体文件系统实现操作最终返回用户态。这里同样,如果是FUSE文件系统,VFS调用FUSE接口,FUSE最终将操作请求返回给用户态文件系统实现具体的操作。关于FUSE实现的原理可以通过下面这张图。
![](http://img2.tuicool.com/vqYvIja.png!web)
3. docker容器虚拟proc文件系统实现
Linux系统proc文件系统是一个特殊的文件系统,通常存储当前系统内核运行状态的一系列特殊文件,包括系统进程信息,系统资源消耗信息,系统中断信息等。这里以meminfo内存相关统计信息为例,讲诉如何通过LXCFS用户态文件系统实现docker容器内的虚拟proc文件系统。
3.1 挂载虚拟proc文件系统到docker容器
通过 docker --volumns /var/lib/lxcfs/proc/:/docker/proc/
,将宿主机上/var/lib/lxcfs/proc/挂载到docker容器内部的虚拟proc文件系统目录下/docker/proc/。此时在容器内部/docker/proc/目录下可以看到,一些列proc文件,其中包括meminfo。
3.2 cat /proc/meminfo
用户在容器内读取/proc/meminfo时,实际上是读取宿主机上的/var/lib/lxcfs/proc/meminfo挂载到容器内部的meminfo文件,fuse文件系统将读取meminfo的进程pid传给lxcfs, lxcfs通过get_pid_cgroup获取读取meminfo的进程所属的cgroup分组。在host的/cgroup目录下找到对应进程的cgroup子系统信息,并通过系统调用再次进入内核文件系统读取/cgroup/memory/PID/meminfo最终返回。
以上过程FUSE内核态最终通过用户态LXCFS实现。在LXCFS用户态文件系统中,执行读取meminfo的操纵在proc_meminfo_read实现。
从FUSE内核态返回到用户态libfuse并最终进去LXCFS用户态其调用栈如下:
Breakpoint 3, lxcfs_read (path=0x7ffff0000af0 “/proc/meminfo”, buf=0x7ffff0000c50 “\004”, size=65536, offset=0, fi=0x7ffff73bbcf0) at lxcfs.c:2834
2834 {
(gdb) bt
#0 lxcfs_read (path=0x7ffff0000af0 “/proc/meminfo”, buf=0x7ffff0000c50 “\004”, size=65536, offset=0, fi=0x7ffff73bbcf0) at lxcfs.c:2834
#1 0x00007ffff7bab597 in fuse_fs_read_buf () from /lib64/libfuse.so.2
#2 0x00007ffff7bab772 in fuse_lib_read () from /lib64/libfuse.so.2
#3 0x00007ffff7bb414e in do_read () from /lib64/libfuse.so.2
#4 0x00007ffff7bb4beb in fuse_ll_process_buf () from /lib64/libfuse.so.2
#5 0x00007ffff7bb1481 in fuse_do_work () from /lib64/libfuse.so.2
#6 0x00007ffff7989df5 in start_thread () from /lib64/libpthread.so.0
#7 0x00007ffff76b71ad in clone () from /lib64/libc.so.6
在LXCFS用户态文件系统中,最终会通过proc_meminfo_read读取/var/lib/lxcfs/proc/meminfo文件计算并返回,其调用栈如下:
在fuse文件系统上下文,找到当前进程所属哪个容器,最终在容器对应的cgroup分组中获取/cgroup/memmory/containerID/memory.limitinbytes, memory.usageinbytes和memory.stat,然后将/var/lib/lxcfs/proc/meminfo中的信息替换成容器对应cgroup分组中的资源信息,这样使容器看到的是自己的meminfo信息。这个文件的读取,写入就是通过fuse用户态文件系统实现。
4. 实现测试
centos7 + docker v1.7.1 + lxcfs 0.11
通过以下命令行启动lxcfs:
lxcfs -s -f -o allow_other /var/lib/lxcfs启动成功后,查看宿主机上mount信息:
fusectl on /sys/fs/fuse/connections type fusectl (rw,relatime)
gvfsd-fuse on /run/user/1000/gvfs type fuse.gvfsd-fuse (rw,nosuid,nodev,relatime,user_id=1000,group_id=1000)
/dev/sr0 on /run/media/meifeng/CentOS 7 x86_64 type iso9660 (ro,nosuid,nodev,relatime,uid=1000,gid=1000,iocharset=utf8,mode=0400,dmode=0500,uhelper=udisks2)
/dev/sdb1 on /home/sdb type ext4 (rw,relatime,seclabel,data=ordered)
binfmt_misc on /proc/sys/fs/binfmt_misc type binfmt_misc (rw,relatime)
tmpfs on /run/lxcfs/controllers type tmpfs (rw,relatime,seclabel,size=100k,mode=700)
name=systemd on /run/lxcfs/controllers/name=systemd type cgroup (rw,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cpuset on /run/lxcfs/controllers/cpuset type cgroup (rw,relatime,cpuset)
cpuacct,cpu on /run/lxcfs/controllers/cpuacct,cpu type cgroup (rw,relatime,cpuacct,cpu)
memory on /run/lxcfs/controllers/memory type cgroup (rw,relatime,memory)
devices on /run/lxcfs/controllers/devices type cgroup (rw,relatime,devices)
freezer on /run/lxcfs/controllers/freezer type cgroup (rw,relatime,freezer)
net_cls on /run/lxcfs/controllers/net_cls type cgroup (rw,relatime,net_cls)
blkio on /run/lxcfs/controllers/blkio type cgroup (rw,relatime,blkio)
perf_event on /run/lxcfs/controllers/perf_event type cgroup (rw,relatime,perf_event)
hugetlb on /run/lxcfs/controllers/hugetlb type cgroup (rw,relatime,hugetlb)
lxcfs on /var/lib/lxcfs type fuse.lxcfs (rw,nosuid,nodev,relatime,user_id=0,group_id=0,allow_other)
将这两个目录通过docker -v的形式挂载到容器内部:
docker run -ti -d —privileged -v /var/lib/lxcfs/cgroup/:/docker/cgroup/:rw -v /var/lib/lxcfs/proc/:/docker/proc/:rw ubuntu:14.04
注意: docker 1.7.1版本不支持将外部目录再挂载到容器内部的/proc目录下,因此这里将/proc挂载在/docker/proc/下,实现容器虚拟proc文件系统。
5. 总结
本文通过分析容器内部/docker/proc/memnifo的具体实现,讲述了如何借助FUSE用户态文件系统在容器内部实现虚拟proc文件系统,以弥补docker容器因隔离性不够完善造成的使用不便。尽管lxcfs用户态文件系统实现的功能比较有限,但是借助用户态文件系统从容器对应的cgroup分组获取容器本身的资源信息,最终反馈在虚拟proc文件系统或者反馈给容器内部的监控agent,也不失为一种不错的研究方向。
参考:
https://github.com/lxc/lxcfs
https://linuxcontainers.org/lxcfs/getting-started/ https://insights.ubuntu.com/2015/03/02/introducing-lxcfs/ http://manpag.es/ubuntu1410/8+cgmanager http://fuse.sourceforge.net/ http://www.cnblogs.com/wzh206/archive/2010/05/13/1734901.html
https://linuxcontainers.org/lxcfs/