《操作系统》实验手记 | 从逻辑地址到共享内存:一次完整的内存管理实践 1. 实验目标与核心概念第一次打开Bochs调试器时看到黑屏上闪烁的光标和满屏的十六进制数字我意识到这将是一次深入操作系统内核的探险。这次实验要完成两个关键任务一是手动追踪变量i0x12345678从逻辑地址到物理地址的完整翻译过程二是在Linux 0.11上实现共享内存机制。这两个目标看似独立实则共同构成了理解现代操作系统内存管理的完整拼图。地址映射就像城市快递系统当你在电商平台下单逻辑地址快递员需要根据配送区域分段机制找到对应的物流中心线性地址再根据具体楼栋导航分页机制最终将包裹送到你家门口物理地址。IA-32架构采用的就是这种分段分页的二级寻址机制而Linux 0.11在x86平台上完美展现了这一设计。共享内存则像是公司里的公共白板不同部门的同事进程无需传递纸条进程间通信开销可以直接在白板共享内存区域上读写信息。实现这个机制需要解决三个关键问题如何找到空闲物理页面、如何建立地址映射、如何管理虚拟地址空间。在Linux 0.11中这些功能分别由get_free_page()、put_page()和change_ldt()等函数协作完成。2. 地址映射实战解析2.1 实验环境搭建在Ubuntu下准备好test.c后我通过以下命令将其移植到Linux 0.11环境sudo ./mount-hdc cp ./exp_07/test.c ./hdc/usr/root/ sudo umount hdc这个简单的C程序会在无限循环中打印变量i的逻辑地址#include stdio.h int i 0x12345678; int main(void) { printf(The logical/virtual address of i is 0x%08x, i); while(i); return 0; }启动Bochs调试器时./dbg-asm命令会进入汇编级调试模式。当看到终端显示Next at t0时我直接输入c命令让系统继续运行直到Linux 0.11完成启动。这个过程中有个关键细节如果CtrlC暂停时显示的是0008而非000f说明中断发生在内核态需要继续执行直到进入用户态。2.2 段表寻址过程当程序暂停在cmp指令时使用sreg命令查看段寄存器状态ds:s0x0017, dl0x00003fff, dh0x10c0f300, valid3 ldtr:s0x0068, dl0xc2d00068, dh0x000082f9, valid1 gdtr:base0x00005cc8, limit0x7ff这里的ds值0x0017是段选择子拆解其二进制0000000000010111TI1表示查找LDT表索引值2对应LDT表中第3个描述符从0开始计数通过gdtr找到GDT基地址0x00005cc8LDT在GDT的第13项0x0068313使用xp /2w 0x00005cc813*8查看得到LDT描述符0xc2d00068 0x000082f9组合后得到LDT物理地址0x00f9c2d0。2.3 线性地址计算在LDT中找到第3个描述符0x00003fff 0x10c0f300提取段基址0x10000000。加上段内偏移0x3004得到线性地址0x10000000段基址 0x3004偏移 0x10003004用calc ds:0x3004验证结果一致。2.4 页表查询与物理地址通过creg查看CR30x00000000即页目录表基址为0。线性地址0x10003004二进制拆分页目录号64高10位1000000页表号3中间10位0000000011页内偏移4低12位000000000100查询页目录项xp /w 064*4 # 显示0x00faa027页表基址为0x00faa000查询第3个页表项xp /w 0x00faa0003*4 # 显示0x00fa7067最终物理地址为页框号0x00fa7 偏移0x004 0x00fa7004。使用xp /w 0x00fa7004确实看到了初始值0x12345678。3. 共享内存实现3.1 系统调用添加首先需要在三个关键文件中添加系统调用声明include/unistd.h 添加调用编号#define __NR_shmget 87 #define __NR_shmat 88kernel/system_call.s 修改系统调用总数nr_system_calls 89include/linux/sys.h 添加函数声明int sys_shmget(); void * sys_shmat();3.2 关键数据结构在include/shm.h中定义共享内存控制块#define SHM_NUM 16 struct struct_shmem { unsigned int size; unsigned int key; unsigned long page; };3.3 shmget实现kernel/shm.c中的核心逻辑int sys_shmget(key_t key, size_t size) { if(size PAGE_SIZE) { errno EINVAL; return -1; } // 检查是否已存在相同key的共享内存 for(int i0; iSHM_NUM; i) { if(shm_list[i].key key) return i; } // 申请新页面 unsigned long page get_free_page(); if(!page) { errno ENOMEM; return -1; } // 填充控制块 for(int i0; iSHM_NUM; i) { if(!shm_list[i].key) { shm_list[i].key key; shm_list[i].page page; shm_list[i].size size; return i; } } return -1; }3.4 shmat实现映射过程的关键步骤void* sys_shmat(int shmid) { if(shmid 0 || shmid SHM_NUM || !shm_list[shmid].page) return NULL; unsigned long tmp get_base(current-ldt[1]) current-brk; put_page(shm_list[shmid].page, tmp); unsigned long ret current-brk; current-brk PAGE_SIZE; return (void*)ret; }这里get_base获取数据段基址current-brk是进程堆顶指针通过put_page建立页表映射。4. 生产者-消费者验证4.1 程序改造要点原始pc.c需要做三处修改将文件缓冲区改为共享内存拆分为producer.c和consumer.c两个独立程序使用相同的SHM_KEY确保访问同一块内存producer.c关键部分#define SHM_KEY 2018 int main() { shm_id shmget(SHM_KEY, BUFFER_SIZE*sizeof(int)); p (int*)shmat(shm_id); while(i PRODUCE_NUM) { sem_wait(Empty); sem_wait(Mutex); p[location] i; sem_post(Mutex); sem_post(Full); } }4.2 运行与调试编译运行命令gcc -o pro producer.c gcc -o con consumer.c ./pro pro.txt ./con con.txt当遇到共享内存无法访问时可以通过dmesg查看内核打印信息。我在调试时就发现因为没有正确设置key导致两个进程访问了不同的内存区域。5. 深度思考与优化在实现过程中我发现原始设计有几个可以改进的点内存泄漏风险当所有进程都退出时共享内存不会自动释放。可以在shm.c中添加引用计数当最后一个进程调用shmdt时自动释放页面。安全性问题当前实现没有权限检查任何进程只要知道key就能访问共享内存。可以借鉴现代Linux的权限位设计在struct_shmem中添加uid/gid字段。性能优化put_page每次都会修改页表对于频繁的共享内存操作可以考虑使用更大的页面尺寸如4MB大页减少TLB失效。这个实验最让我惊喜的是通过亲手实现这些底层机制那些课本上抽象的概念突然变得具象起来。记得在调试页表映射时当第一次看到xp /w成功显示出预期的数值那种窥见计算机灵魂的震撼感至今难忘。