共计 2788 个字符,预计需要花费 7 分钟才能阅读完成。
- 任务描述
当使用qemu模拟了虚拟机运行(仅仅启动内核);此时虚拟机作为一个qemu进程,查看其在宿主机上所占用的内存页面与页面的属性
2. 实验记录
2.1. qmap
内存的分配方式主要有两种:
1. 直接分配。这是由进程直接分配内存的方式。在创建进程时,会为它分配一个或多个内存区域,这些区域的大小和位置由操作系统决定。这种方式的优点是简单直接,可以满足进程对内存的基本需求。但缺点是效率较低,因为每次分配都需要在操作系统的管理下进行,而且分配的内存不一定能够得到充分的利用。
2. 通过文件映射。这是另一种内存分配方式,主要应用在虚拟内存中。虚拟内存是一种将硬盘空间作为内存使用的技术,通过这种方式,进程可以使用比实际物理内存更多的内存。当进程需要使用内存时,操作系统会将一个文件映射到进程的地址空间中,这样,进程就可以像访问物理内存一样访问这个文件。这种方式的优点是可以提高效率,因为操作系统可以统一管理内存的分配和释放,而且可以将暂时不用的内存数据换出到硬盘中,从而节省物理内存的空间。但缺点是操作相对复杂,需要一定的系统资源
pmap -x 52369 //该命令可以查看某进程的内存映像信息;选项 -d 可以获取更详细信息
//各列信息如下:
- Address: 进程的内存地址。
- Kbytes: 进程在该地址上占用的内存大小(以KB为单位)。
- RSS: 进程在该地址上的驻留集大小(Resident Set Size),也就是进程当前在物理内存中占用的大小。
- Dirty: 进程在该地址上的"Dirty"大小。"Dirty"指的是内存中已经被进程修改,但还没有被写回到磁盘的部分。
- Mode: 进程在该地址上的内存访问模式,例如'r'代表可读,'w'代表可写,'x'代表可执行。
- Mapping: 进程在该地址上的内存映射详情。在这里,所有的映射都是匿名映射(anon),这表示这些内存是由进程直接分配的,而不是通过文件映射的。*/

2.2. GDB
gdb -p 52369
<gdb> info proc mappings //获取进程内存映射信息;经验证其与pmap获取一致
2.3. 小结
以上两种方法其实都是通过读取所属进程的/proc/pid/maps文件来获取进程的内存映射信息,进行了一些格式处理
下一步应当弄清楚/proc/pid/maps文件是由谁什么时候如何组织的
3. 内核模块编写获取进程内存信息
3.1. 前置知识
1. 默认读者已经掌握内存页面转换知识;
2. 默认读者掌握PCB的结构体知识——task_struc;
3. 默认读者掌握页表项与页目录项的结构以及内存在内核中的结构体表示mm_struct
3.2. 实验思路
1.通过pid号确定对应的task_struct;
2.通过task_struct中的内存字段进行遍历的操作完成该进程内存页面的地址的读取与标志获取
3.3. 代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/mm.h>
#include <linux/slab.h>
MODULE_LICENSE("GPL");
MODULE_VERSION("1.0.1");
MODULE_AUTHOR("CHH");
static void get_process_mappings(struct task_struct *task) {
//内核中用于内存管理的数据结构
struct mm_struct *mm;
//mm_struct中指向内存映射链表的头指针
struct vm_area_struct *vma;
unsigned long start, end;
unsigned long mf_flags;
//mm字段是在进程控制块task_struct中的内存管理的数据结构
mm = task->mm;
if (!mm) {
printk(KERN_INFO "No memory mapping available.\n");
return;
}
printk(KERN_INFO "Process mappings for PID %d:\n", task->pid);
//以下字段均可以通过查看linux内核源码获得
//简单的链表的指针实现遍历
for (vma = mm->mmap; vma; vma = vma->vm_next) {
/*
*获得页面起始地址与页表项的标志
*/
start = vma->vm_start;
end = vma->vm_end;
mf_flags = vma->vm_flags;//页面标志
printk(KERN_INFO "Start: 0x%lx, End: 0x%lx, Flags: 0x%lx\n", start, end, mf_flags);
}
}
static int __init mm_module_init(void) {
// 在这里编写获取内存映射页面的代码
struct task_struct *task;
pid_t pid = 197496; // 替换为你要获取内存映射页面的进程ID,目前的pid为一个qemu进程
//pid_task系统调用,通过pid返回一个task_struct结构体的指针
task = pid_task(find_vpid(pid), PIDTYPE_PID);
if (!task) {
printk(KERN_INFO "Invalid process ID.\n");
return -1;
}
get_process_mappings(task);
return 0;
}
static void __exit mm_module_exit(void) {
printk(KERN_INFO "Module unload\n");
}
//内核模块地注册
module_init(mm_module_init);
module_exit(mm_module_exit);
3.4. 内核模块编译与添加
3.4.1. Makefile文件编写
obj-m += mm_tracing.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
全网最牛Linux内核Makefile文件详解 – 知乎 (zhihu.com)
3.4.2. 编译
//在源码目录下
make
3.4.3. 查看编译结果

3.4.4. 插入内核模块并且查看模块插入信息


3.5. 实验结果分析
3.5.1. 起始地址


通过对比可以发现其地址获取成功
3.5.2. 标志位分析
以下是页表项与页目录项
通过对比/proc/\<pid>/maps中的模式字段与将Flags值解析为二进制再按照上面的页表结构进行对比可以得出:页面控制信息也成功获取
正文完