龙进
Longjin@dragonos.org
本文基于DragonOS主线 dcf232f3 版本进行讲解。
1. 开发动机
当初在开发的时候,发现DragonOS存在一些内存泄漏的问题,但是不清楚到底哪里产生了泄漏,也不清楚内核的内存分配过程。为了定位内存泄漏的问题,以及观测一些可能存在的性能问题,就实现了这个MMLog的组件,把每一次内存分配和释放都打到日志里面去,同时希望能在Linux下面启动一个监视器,去监控DragonOS虚拟机内的内存分配情况。
1.1. 为什么不在DragonOS里面直接输出?
主要有两个方面的考量:
- 性能原因:直接输出到屏幕,非常的慢,严重影响性能。如果直接从串口输出,也是一样的,太慢了,还会和正常的日志混杂在一起。
- 可观测性:因为内存管理太基础了,如果内存管理模块本身出了问题,难以打印日志到屏幕上。
2. 原理
2.1. 整体架构
内存分配日志机制的整体架构如上图所示。内核里面有一个无锁的MPMC(多生产者多消费者)环形缓冲区,每次产生内存分配、释放的时候,就会按照固定的格式把二进制日志输出进去。
同时,QEMU启动的时候,设置其Memory backend为宿主机上的一个共享内存文件,使得能够在宿主机读取到DragonOS虚拟机的内存。
接着在Linux下运行一个日志监视器,这个监视器的worker线程会不断地扫描DragonOS内的那个环形缓冲区,不断地提取新的日志,加入日志集合。同时监视器的主线程负责把日志集合内的日志打印输出到文件。
2.2. 日志监视器如何找到这个环形缓冲区?
如上图所示,内核内存日志缓冲区的名称固定为”__MM_ALLOCATOR_LOG_CHANNEL”。接着如下图所示,在日志监视器启动的时候,会加载内核ELF文件,寻找这个symbol,接着计算偏移量,就能知道CHANNEL在内存文件中的哪个位置了。
2.3. 怎么收集日志?
由于监视器不需要与DragonOS内核进行直接交互,那么我们会面临以下问题:
- 需要规定统一的日志格式。
- 监视器需要确定日志的顺序。
- 不能确定环形缓冲区的头部和尾部。由于我们没法加锁,同时这里也不允许直接修改DragonOS内存中的缓冲区,否则会造成错误。
- 观察到错误的日志数据:观察到的日志,可能是未完全写入的,也可能是在队列槽位recycle的时候未完全清空的。
第一个问题,我们设计了一个日志结构体,格式固定,监视器只需要按照这个格式去解析数据即可。注意所有的数据都要是#[repr(C)]的,这样采用固定的大小。
第二个问题,解决方案就是在日志头部加上id的字段。
第三个问题,这里采用的是一种“冗余计算”的方法:两个工作线程不停的循环扫描整个队列,发现新的日志,就把他加入到LogSet中。这样只要宿主机的工作线程足够快,那么就不存在漏日志的情况。
第四个问题,我们引入checksum。在扫描的时候,对于每次扫描的记录,我们都为其计算crc64,同时与日志记录内的checksum字段作比较,二者一致才认为这条日志是可信的。
解决了上面的四个问题,就能正确获取到所有的内存分配、释放日志了。
3. 使用
流程非常简单,首先编译DragonOS,再启动日志监视器。然后再运行DragonOS即可。
3.1. 编译:
3.2. 启动日志监视器
启动后应当会输出以下信息,提示“无法加载内存文件”,这是正常的,因为DragonOS此时尚未启动,监视器正在等待DragonOS启动。
3.3. 启动DragonOS
输入make qemu或者make qemu-vnc来启动DragonOS
DragonOS启动后,我们将会看到以下的信息,显示每秒的日志产生速率。
3.4. 查看日志文件
在logs文件夹下,能够看到内存日志: