HIT-OSLab6
实验目的:
- 深入理解操作系统的段、页式内存管理,深入理解段表、页表、逻辑地址、线性地址、物理地址等概念
- 实现段、页式内存管理的地址映射过程
- 编程实现段、页式内存管理上的内存共享,从而深入理解操作系统的内存管理
实验内容:
- 用 Bochs 调试工具跟踪 Linux-0.11 的地址翻译(地址映射)过程,了解 IA-32(Intel Architecture 32-bit) 的CPU架构下的地址翻译
- 在 Linux-0.11 中实现 Linux-0.11 的内存管理机制,具体来说就是实现如下3个系统调用:
- sys_shmget:获取一个共享内存块
- sys_shmat:映射一个共享内存块
- 在 Ubuntu 上编写多进程的生产者-消费者程序,用共享内存做缓冲区(上一个实验是用文件做缓冲区)
- 在上一个实验(信号量的实现和在 pc.c 程序上的应用)的基础上,为 Linux-0.11 增加共享内存功能,并将生产者-消费者程序移植到 Linux-0.11
实验过程
本实验基于上个实验,因此需要保存上个实验的代码
逻辑地址,虚拟地址,线性地址,物理地址是计算机内存管理中的重要概念:
- 逻辑地址:
- 逻辑地址是程序使用的地址
- 编译器编译程序时,会为程序生成代码段和数据段,然后将所有代码放到代码段中,将所有数据放到数据段中,最后程序中的每句代码和每条数据都会有自己的逻辑地址
- 虚拟地址:
- 虚拟地址是操作系统为每个进程分配的地址空间,保证了进程之间的隔离和安全性
- 线性地址:
- 由虚拟地址通过 “分段” / “分页” 机制转换而来的地址
- 物理地址:
- 物理地址是指内存中的实际地址,它是硬件访问的地址
- 如果 CPU 没有分页机制,那么线性地址等于物理地址
- 如果 CPU 有分页机制,那么线性地址必须通过转换才能变成物理地址
逻辑地址,虚拟地址,线性地址,物理地址之间的关系是:
- 在程序编写时,汇编指令使用的地址为逻辑地址(编译器为各个符号分配的地址)
- 当程序被加载到内存中时,操作系统会将逻辑地址转化为虚拟地址
- 在 x86 架构中:
- 虚拟地址通过段选择器和段描述符转化为线性地址
- 线性地址通过页表转化为物理地址
实验要求跟踪调试 Linux-0.11 的地址映射过程,测试代码如下:
1 |
|
- 运行后程序死循环
我们的目标就是调试分析变量 i 的物理地址,修改该地址使程序停止死循环:
- 程序运行之后输出的 0x3004 就是变量 i 的虚拟地址
线性地址由段选择器和段描述符计算而来,因此我们需要先查找到段描述符
进程的段描述符记录在该进程的 LDT 表中,而 LDT 表则记录在 GDT 表中(LDTR 寄存器记录 LDT 表存放在 GDT 表的位置,GDTR 寄存器则记录有 GDT 表的物理地址)
用 sreg
命令可以显示所有寄存器的信息:
1 | <bochs:3> sreg |
- GDTR 寄存器中的值为 0x5cb8
- LDTR 寄存器中的值为 0x0068
- DS 寄存器中的值为 0x0017(
0b000000000010111
,索引值为0b10
)
用 xp
命令可以打印指定内存的数据:
- 查看 GDT 表信息,LDT 表的物理地址为
0xfe92d0
1 | <bochs:5> xp /8w 0x00005cb8+0x68 |
- 查看对应 LDT 表信息,DS 段的索引为
0x2
,对应0x00003fff 0x10c0f300
1 | <bochs:6> xp /8w 0xfe92d0 |
段描述符的结构如下:
- 段基址由3部分组合而成:
[0,8),[24,32),[48,64)
1 | bin(0x00003fff10c0f300) => |
- 计算得基地址为
0x10000000
- 因此线性地址为
0x10003004
1 | In [1]: hex(0b00010000000000000000000000000000) |
用 calc
命令验证线性地址是否正确:
1 | <bochs:7> calc ds:0x3004 |
线性地址的结构如下:
1 | bin(0x10003004) => |
- 0-11:页内偏移 - 4
- 12-21:页表索引 - 3
- 22-31:页目录索引 - 64(二级页表索引)
在 IA-32(英特尔的32位CPU架构) 下,页目录表的位置由 CR3 寄存器指引,用 creg
命令可以看到:
1 | <bochs:2> creg |
- 页目录表所在物理地址为
0x0
- 页目录表和页表中的内容很简单,是1024个32位数,这32位中前20位是物理页框号,后面是一些属性信息(最重要的是最后一位P)
1 | <bochs:4> xp /8w 0+64*4 |
- 页表所在的物理页框号为
0x00fa6
,即页表在物理内存为0x00fa6000
处
1 | <bochs:5> xp /8w 0x00fa6000 + 3*4 |
- 物理页所在的物理页框号为
0x00f99
,即物理页在物理内存为0x00f99000
处 - 因此变量 i 的物理地址为
0x00f99004
用 page
命令验证物理地址是否正确:
1 | <bochs:6> page 0x10003004 |
打印该地址的数据即可发现 0x12345678
:
1 | <bochs:8> xp /w 0x00f99004 |
最后使用 setpmem
命令修改内存即可:
1 | <bochs:10> setpmem 0x00f99004 4 0 |
- 程序成功退出
Linux 和 Unix 系统中,共享内存(Shared Memory)是一种在多进程环境下实现进程间通信的技术,它允许多个进程同时访问同一块内存区域,从而实现数据的共享和通信
在为 linux-0.11 编写共享内存代码前,我们需要先分析一下 linux-0.11 对页面的管理机制
1 |
|
mem_map
是一个全局数组,在 Linux 0.11 中用于存储物理内存的映射- 其索引代表线性地址,每个元素代表一个物理页框
在 Linux-0.11 中,物理内存由一系列页框组成,每个页框的大小为 4KB,mem_map
数组存储了所有已经分配的物理页框的地址,当进程需要分配内存时,会从 mem_map
中查找一个空闲的页框,将其分配给进程,并将该页框的地址返回给进程
初始化 mem_map
数组的函数为 mem_init
,在 swapper 进程中会调用一次:
1 |
|
空闲页面分配的函数 get_free_page
代码如下:
1 |
|
repne scasb
用于将寄存器的内容与内存中的数据进行比较(一直重复直到edi
末尾为 “\0”)- 核心操作就是遍历一遍
mem_map
,返回合适的空闲页面
释放已分配页面的函数 get_free_page
代码如下:
1 | void free_page(unsigned long addr) |
- 将对应的
mem_map[addr]
末尾置为 “\x00”
函数 put_page
用于将线性地址与物理页进行映射,其实现如下:
1 | unsigned long put_page(unsigned long page,unsigned long address) |
本实验要求我们实现共享内存,首先我们需要添加共享内存的系统调用号:(在 /include/unistd.h
中)
1 |
- 需要注意的是:这里同时需要修改
hdc-0.11.img
中的hdc/usr/include/unistd.h
文件,如果想在虚拟机中使用 gcc 编译的话,会导入虚拟机hdc/usr/include/
中的文件为头文件
接着修改系统调用号的总数:(在 /kernel/system_call.s
中)
1 | nr_system_calls = 78 |
最后添加新的系统调用定义:(在 /include/linux/sys.h
中)
1 | extern int sys_shmget(); |
头文件以及宏定义:(在 /kernel
中新建文件 shm.c
)
1 |
核心结构体 struct_shm_tables
:
1 | struct struct_shm_tables |
基础字符串函数:
1 | int strcmp_shm(char* name,char* tmp){ |
系统调用 sys_shmget:获取一片共享内存
1 | int sys_shmget(char *name) |
系统调用 sys_shmat:映射一片共享内存
1 | void *sys_shmat(int shmid) |
最后修改 makefile:
1 | OBJS = sched.o system_call.o traps.o asm.o fork.o \ |
接下来我们需要修改上一个实验的生产者-消费者程序,并用共享内存做缓冲区:
1 |
|
这里有一个点需要注意,由于共享页面同时存在于两个进程中,因此会在 do_exit
中被释放两次
为了不触发内核报错,这里需要修改 free_page
函数:
1 | void free_page(unsigned long addr) |
最终的效果如下: