康华 :主要从事 Linux 操作系统内核、虚拟机、 Linux 技术标准、计算机安全、软件测试等领域的研究与开发工作,曾就职 MII-HP 软件实验室 、瞬联软件公司 /MOTOROLA 、 LENOVO 研究院 。其所合写的 Linux 专栏见 http://www.csdn.net/subject/linux/ 。 如果需要可以联系通过 [email protected] ( MSN )联系他 .
摘要 : 本文给出了一种从 VMM(virtual machine monitor) 中根据截获的硬件访问信息和 GUEST OS 的进程管理信息,在系统运行时自动识别 GUEST OS 中运行进程的方法——该方法不需要 GUEST OS 做任何修改或者安装任何软件。其意义在于将 VMM 的监控粒度从系统级 , 提高到进程级别。我们可以很方便的在此基础上实现很多有趣的功能 ( 见下文 ) 。
我的动机来自于一个简单的问题 " 如何从 VMM 中获知 GUEST OS 的负载 ?" , 最直接的办法是在 GUEST OS 安装一个精灵进程 , 不断收集负载信息 , 然后通过共享内存告诉 VMM, 但是如果 GUEST OS 禁止安装任何软件 , 那该怎么办呢 ? 我当时想借助最原始的方法:计算 idle 进程在单位时间内的运行时间 , 从而获得 GUEST OS 的运行负载。所以我需要从 VMM 中识别 GUEST OS 的 idle 进程。 沿着该思路 , 我想也许由 VMM 识别 GUEST OS 的进程会对一些管理场景有所帮助 , 比如: 1 从 VMM 中监控 GUEST OS 的进程运行状况和资源利用情况; 2 从 VMM 中杀死 GUEST OS 中的运行进程 , 这也许被用于死锁解锁等目的; 3 加固 GUEST OS —— VMM 可限特定进程资源访问权限和范围等 ( 比如只让某个进程访问物理特定内存 ) ; 4 从 VMM 中为 GUEST OS 的进程动态打补丁( patch ) , 在不重启进程或系统的情况下修改 bug 。
注 : xen 等 VMM 会利用 GUEST OS 执行 idle 进程时调用的 hlt 指令 (hlt 指令可以配置成陷入条件 ) 时间来估计 GUEST OS 的负载 , 当然这么做的前提是 GUEST OS 中 idle 指令会循环调用 hlt 指令。
VMM 会利用 GUEST OS 在做进程切换时 ( 需要访问特权积存器 CR3, 以载入新进程的页表基地址 , 在 VT 环境下会造成一个陷入—— vmexit) 的陷入时机和所带硬件信息 , 建立并维护一个 GUEST OS 进程踪迹记录 (GPTR) 的多维向量 , 其中包含 1 GUEST OS 的进程页目录地址 (GPPDA), 其值从 CR3 中获取; 2 GUEST OS 进程的名字或 ID, 其值从 GUEST OS 的进程描述符中获得 , 或者人为指定一个唯一值 ( 如果无法从描述符获得的情况下 ) 。
有了 GPTR 向量我们就可以在运行时识别 GUEST OS 的运行进程了 , 具体做法是——用被 VMM 捕获的 GUEST OS 当前进程的 GPPDA 做键值 , 在 GPTR 的 GPPDA 记录向量里进行匹配 , 以识别 GUEST OS 中哪个进程在运行。
注: 寻找GUEST OS 进程描述符号的方法需要根据操作系统而定, 并无固定做法. 比如对于Linux 系统当前进程描述符号的索引和内核堆栈连续存放于2 页或1 页( 根据内核堆栈的编译选项) 内, 所以我们可通过分析内核堆栈指针(RSP 也将在访问CR3 陷入时被VMM 获得) 而定位进程描述符位置, 从而解析出其中的进程ID 等信息( 见下图); 对于windows 系统, 定位当前进程描述符号更为方便。因为当前进程描述符会被放在一个位置固定( 对每种处理器而言) 的PRCB(processor control block) 中.
定位 Linux 的进程描述符:
movl $0xffffe000,%ecx /* or 0xfffffe000 for 8KB kernel stacks */
andl %esp,%ecx
movl (%ecx),p /* p pointe to current process descriptor */
我们以Linux 做GUEST OS 为例讲述实现概要。
1. VMM 在GUEST OS 做进程切换时捕获到 vmexit 。
2. VMM 从CR3 中获得待运行进程的GPPDA 和栈指针RSP (指向内核堆栈,因为进程切换发生与内核态)。
3. VMM 通过RSP 找到当前进程的描述符。
4. VMM 解析当前进程描述符,进程ID (GPRID) 。
5. VMM 将上次获得的GPPDA 和本次获得的GPRID 作为键值对形式,存储到GPTR 向量中。注意上轮获得的GPPDA 对于上轮来说就是待运行进程,对于本次来说则是当前进程(见下图)。
6. VMM 执行正常流程。
进程切换示意图:
为了验证上述方法是否可行,我以 KVM 为 VMM 实现了一个原形加以验证(之所以选择 KVM 是因为 KVM 易于调试,结构清晰;当然你也可以使用 XEN 等 VMM 作平台),该原形中用户可以指定被跟踪的 VM ( virtual machine, 即 GUEST OS ) , 并且在运行期获取该 VM 的运行进程信息。当然 GUEST OS 不需要有任何改动,修改的仅仅是 KVM 。原型代码请见< http://sourceforge.net/project/showfiles.php?group_id=200727 >。
主要修改是增加了用户操作接口,以及一个进程跟踪模块(目前该模块仅仅面向 Linux 做 GUEST OS) .
1 . 设置了一个 Hook 函数( in handler_cr )—— ToTraceCr3 (vm process id) 来截获并解析 CR3 寄存器,并添加一个注册函数( in vmx.c) 以将 gptrace.ko 中的回调函数挂到 hook 上。
注:当 kvm 处理 GUEST OS 因为 EXIT_REASON_CR_ACCESS 原因而陷入时,回调用 "handler cr" 函数。
typedef int (Cr3TraceFunc)(struct kvm *kvm ,int reg);
Register_cr3_trace(Cr3TraceFunc *func)
{
Hook_ToTraceCr3 = func;
}
EXPORT_SYMBOL_GPL(Register_cr3_trace);
2 . 在 kvm 结构中增加了 vm_id 域 ( 即承载 GUEST OS 运行的 Qemu 进程的 pid) ,目的是为了能识别VM( virtual machine ,即 GUEST OS )。
3 . 在 kvm 结构中增加了 trace_enable 标识 (vm trace enable) ,目的是为了打开或关闭VM跟踪。
4 . 在 kvm 结构中增加一个 opaque pointer 域,目的是为了存储 gptrv 结构 ( 见下文 ) 。
5 . 增加一些 ioctl 处理项
KVM_ENABLE_VM_TRACE (ioctl for /dev/kvm)
KVM_GET_VM_GPTRV(ioctl for /dev/kvm)
KVM_SET_VMID(for vcpu fd)
最重要的数据结构是 "struct gptrv" ,每个 VM 都会维护一个该结构数据,用来存储进程跟踪记录 "guest process trace record vector"
struct gptrv //GUEST process trace record vector
Struct gptritem{
unsigned long gpptaddr; //GUEST process page table directory address
unsigned long gpdaddr; //GUEST process descriptor address
int gpid; //GUEST process id
char gpname[30]; //GUEST process's name
__64 begin_time; // first record time
__64 last_time; // last record time
}gptr[MAX_TRACE_NUMBER]; //how many process that can be record
int last; //last time running process
int curr; //current running process
}
另外一个需要解释的地方是 PID_OFFSE/COMM_OFFSET 这些宏 , 它们表示的是 pid/comm 域在进程描述符表中的偏移.我们解析 GUEST OS 的进程 id 和名称需要找到并读取这些值。不过要注意由于不同版本的 Linux 内核进程描述符表结构有变化 , 其中 pid/comm 的偏移不尽相同。 ( 今后我会寻求一个能自动探测并获取 pid/comm 等域的方法 )
1 .在VM 启动时设置VM process id 到KVM 结构中 (interface)
利用KVM_SET_VMID ioctl, 在VM 启动时将qemu 进程的id 写入对应的kvm 结构。具体实现在kvm_qemu_create_context 中:ioctl(kvm_context->vm_fd, KVM_SET_VMID ,getpid())
2 .打开/ 关闭跟踪VM 功能(interface)
利用 KVM_ENABLE_VM_TRACE ioctl, enable trace ==0 是关闭,1 则相反。
3 .获取VM 的运行进程信息(Interface)
利用KVM_GET_VM_GPTRV ioctl 获取给定VM 所维护的 进程跟踪记录.
4 .杀死VM 中的给定进程(interface) -- 下篇文章中介绍.
5 .跟踪进程切换——主要功能模块,其算法描述如下:
检查是否VM 的trace enable flag 被设置
IF No : return;
检查是否VM 的opaque 指针是NULL
IF No : 创建gptrv 结构.
检查是否CR3 值是否已经被记录在GPTR 向量中
IF Yes :
更新last index.
更新timestamp
记录gpdaddr 到last 进程跟踪记录中。
记录gpid/gpname 到last 进程跟踪记录中。
更新last index 。
IF No :
记录gpptaddr 到curr 进程跟踪记录中, 并更新其 timestamp
记录gpdaddr 到last 进程跟踪记录中
记录gpdaddr 到last 进程跟踪记录中
更新 last index 和 curr index.
注:
1 gpdaddr 获得通过两步 ( 方法同 Linux 获取当前进程描述符 current 的方法 ) :a rsp&0xffff000 ( fffff000 for 4k kernel stack;ffffe000 for 8k ) ;b kvm_read_GUEST (vcpu, rsp&0xffff000,4, gpdaddr) (read the GUEST virtual address of process descriptor)
2 gpid/gpname 获取通过如下语句 : kvm_read_GUEST(vcpu,gpdaddr+PID_OFFSET/COMM_OFFSET,length,gptr->ptrt[].gpid/gpname)
6 加载模块
Insmod gptrace.ko
1. 目前获取 pid/comm 都是通过硬编码完成 , 因此仅仅适合 FC6 作为 GUEST OS, 如果你需要运行其它 Linux 发布版或者其他内核 , 需要你修改 PID_OFFSE/COMM_OFFSET 这些宏。
2. 目前只能记录 270 个活跃进程踪迹 . 因为现在我用 ioctl 带回数据 , 而 ioctl 的最大传输限制是 4 页 ,16k. 。
3. 和 KVM 一样 , 该方法只能在支持 VT 的 CPU 上实现。
4. 目前仅仅是原型验证,因此程序中尚有很多bug 。
1 . 下载原代码
2 . 进入目录 kvm-24
3 . 执行编译过程 ( 同 KVM)
./make clean
./configure -prefix=/usr/local/kvm
./make
./make install
./modprobe kvm-intel ( 我使用 Intel VT CPU)
./modprobe gptrace
4 . 启动一个 VM
./use/local/kvm/bin/qemu <your vm image>
5 . 为了简单期间 , 我将用户工具实现在 <kvm>/user 目录下的 main.c 中 . 你可执行在 <kvm>/user 目录执行 make 生成 kvmctl 执行文件 .
1 .打开跟踪功能
./kvmctl -E vmid (vmid is qemu process id ,you can get it form ps -aux|grep -I qemu)
2 .关闭跟踪功能
./kvmctl -D vmid
3 .显示 VM 的运行进程信息
./kvmctl -S vmid
4 .显示用法
./kvmctl -h