本节将更新哈工大《操作系统》课程第七个 Lab 实验 地址映射与共享。按照实验书要求,介绍了非常详细的实验操作流程,并提供了超级无敌详细的代码注释。
实验目的:
- 深入理解操作系统的段、页式内存管理,深入理解段表、页表、逻辑地址、线性地址、物理地址等概念;
- 实践段、页式内存管理的地址映射过程;
- 编程实现段、页式内存管理上的内存共享,从而深入理解操作系统的内存管理。
实验任务:
1、用 Bochs 调试工具跟踪 Linux 0.11 的地址翻译(地址映射)过程,了解 IA-32 和 Linux 0.11 的内存管理机制; 2、在 Ubuntu 上编写多进程的生产者—消费者程序,用共享内存做缓冲区; 3、在信号量实验的基础上,为 Linux 0.11 增加共享内存功能,并将生产者—消费者程序移植到 Linux 0.11。
文件名 | 介绍 |
---|---|
hit-操作系统实验指导书.pdf | 哈工大OS实验指导书 |
Linux内核完全注释(修正版v3.0).pdf | 赵博士对Linux v0.11 OS进行了详细全面的注释和说明 |
file1615.pdf | BIOS 涉及的中断数据手册 |
hit-oslab-linux-20110823.tar.gz | hit-oslab 实验环境 |
gcc-3.4-ubuntu.tar.gz | Linux v0.11 所使用的编译器 |
Bochs 汇编级调试指令 | bochs 基本调试指令大全 |
最全ASCII码对照表0-255 | 屏幕输出字符对照的 ASCII 码 |
x86_64 常用寄存器大全 | x86_64 常用寄存器大全 |
一、IA-32 的地址翻译过程
1、启动调试器
- 将
test.c
测试程序拷贝到 linux0.11 系统下
cd oslab_Lab6
sudo ./mount-hdc
cp test.c hdc/usr/root/
sudo umount hdc
// 启动 linux-0.11
./dbg-asm
c // 运行到断点位置
Next at t=0 表示下面的指令是 Bochs 下一条要执行的软件指令
2、编译并运行 test.c
- linux-0.11 系统下编译运行
test.c
gcc -o test test.c
./test
// 输出:The logical/virtual address of i is 0x00003004
test 是一个死循环,只会不停占用 CPU,不会退出。
- 暂停
- 在命令行窗口按
Ctrl+c
,Bochs 会暂停运行态。绝大多数情况下都会停在 test 内。 - 其中的 000f 如果是 0008,则说明中断在了内核里。那么就要
c
,然后再Ctrl+c
,直到变为 000f 为止。 - 如果显示的下一条指令不是 cmp ...(这里指语句以 cmp 开头),就用
n
命令单步运行几步,直到停在 cmp ...。
- 使用命令
u /8
,显示从当前位置开始 8 条指令的反汇编代码。 - 这就是 test.c 中从 while 开始一直到 return 的汇编代码。变量 i 保存在 ds:0x3004 这个地址,并不停地和 0 进行比较,直到它为 0,才会跳出循环。
3、查段表,计算 DS 线性地址
- LDT 表位置
- ds:0x3004 是虚拟地址,应用程序都有一个段表,叫 LDT。
ldtr
寄存器表示 LDT表 存放在 GDT 表的位置。sreg
指令显示
<bochs:2> sreg
cs:s=0x000f, dl=0x00000002, dh=0x10c0fa00, valid=1
ds:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=3
ss:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=1
es:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=1
fs:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=1
gs:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=1
ldtr:s=0x0068, dl=0xa2d00068, dh=0x000082fa, valid=1
tr:s=0x0060, dl=0xa2e80068, dh=0x00008bfa, valid=1
gdtr:base=0x00005cb8, limit=0x7ff
idtr:base=0x000054b8, limit=0x7ff
- 0x0068=0000000001101000,则存放在 GDT 表的 1101(二进制)=13(十进制)号位置
- 在 GDT 表中查找 LDT 表
- GDT 表中的每一项占 64 位(8 个字节),所以我们要查找的项的地址是
0x00005cb8+13*8
。xp
是用于查看内存内容的调试指令,w表示显示两个字(b-BYTE, h-WORD, w-DWORD, g-DWORD64)
<bochs:5> xp /2w 0x00005cb8+13*8
[bochs]:
0x00005d20 <bogus+ 0>: 0xa2d00068 0x000082fa
此时可以发现,和前面
sreg
指令中ldtr
后面的dl
和dh
值一样(这是Bochs 调试器自动计算出的)
- 利用 GDT 表中描述符计算 LDT 表线性地址
- 描述符为“0xa2d00068 0x000082fa”,->
0x 00 fa a2d0
(前面32位对应下面行,后面32位对应上面行)
- 根据该地址,找到了 LDT 表位置,前4项内容为(每一项也是64位):
<bochs:7> xp /8w 0x00faa2d0
[bochs]:
0x00faa2d0 <bogus+ 0>: 0x00000000 0x00000000
0x00000002 0x10c0fa00
0x00faa2e0 <bogus+ 16>: 0x00003fff 0x10c0f300
0x00000000 0x00fab000
- 段寄存器(段选择子) DS 从 LDT 表获取段描述符
sreg
指令获取 DS 寄存器内容
<bochs:8> sreg
ds:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=3
- RPL:即如果 RPL 的数值大于 CPL(数值越大,权限越小),则用 RPL 的值覆盖 CPL 的值,即 CPR, RPL ≤ DPL
- TI:TI=0,在 GDT 表中查; TI = 1,在 LDT 表中查。
- ds = 0x0017 = 0001 0111,即RPL = 11, TI = 1,索引 = 10,即在 LDT 表中索引为 2 的描述符(即第三个),即
0x00003fff 0x10c0f300
为搜寻很久的 DS 段描述符了。
此时可以发现,和前面
sreg
指令中ds
后面的dl
和dh
值一样(这是Bochs 调试器自动计算出的)
总结:
- 根据段选择子
ldtr
在GDT表中找到了LDT表中的段描述符,计算线性地址,该线性地址其实bochs调试器在ldtr
寄存器后面的dh、dl中已经计算好了,可以供我们验证; - 根据段选择子
DS
在LDT表中找到DS的段描述符,计算线性地址,该线性地址其实bochs调试器在DS
寄存器后面的dh、dl中已经计算好了,可以供我们验证。
- 利用 LDT 表中段描述符计算 DS 线性地址(段基址)
- 由段描述符
0x00003fff 0x10c0f300
可以计算得到段基址为0x 10 00 0000
,所以ds:0x3004
的线性地址为0x100003004
。 - 使用
cals ds:0x3004
可以验证该结果
4、查页表,由线性地址计算物理地址
即计算 页目录号(10位)、页表号(10位)和页内偏移(12位)
- 线性地址
0x100003004
:页目录号 = 64, 页号 = 3, 页内偏移 = 4
- 在页目录表中查找
- 页目录表的位置由 CR3 寄存器指引,
creg
命令可以查看:
- 说明页目录表的基址为 0
- 页目录表和页表中的内容很简单,是 1024 (2^10)个 32 位数,表大小刚好为4K。用以下指令可以在页目录表中查找到页目录项为
xp /w 0+64*4
// 0x00000100 : 0x00fa5027
- 在页表中查找
- 上述可得页表所在物理内存位置为
0x00faa
位置(027是属性),得3号页表项为:
xp /w 0x00faa000+3*4
// 0x00fa500c : 0x00f9b067
- 计算物理地址
物理页框号为 0x00f9b(067是属性),和页内偏移 0x004,得到 0x00f9b004
,这就是变量 i 的物理地址。
可以通过两种方式验证:
- 基于线性地址
0x10003004
,使用page
指令:
page 0x10003004
linear page 0x10003000 maps to physical page 0x00f9b000
- 基于物理地址
0x00fa7004
,使用xp
指令查看地址中存放的一个字节:
xp /w 0x00f9b004
0x00fa7004 : 0x12345678
5、修改内存来改变 i 的值,使程序退出循环
- 将从 0x00f9b004 地址开始的 4 个字节都设为 0
setpmem 0x00f9b004 4 0
- 然后再用
c
命令继续 Bochs 的运行,可以看到 test 退出了,说明 i 的修改成功了,此项实验结束。
二、在 Linux 0.11 中实现共享内存
本次实验将在 Lab6 信号量的实现和应用
实验的基础上,使用共享内存替换文件缓冲区,来完成生产者和消费者两个程序。
Linux 中,将不同进程的虚拟地址空间通过页表映射到物理内存的同一区域,实现共享内存。
1、新建文件kernel/shm.c
,实现共享内存
#include <asm/segment.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/mm.h>
#include <errno.h>
#define _SHM_NUM 20
/* 共享内存结构体 */
struct shm_tables
{
int key; /* 共享内存标识 */
int size; /* 共享内存大小 */
unsigned long page; /* 共享内存地址 */
} shm_tables[_SHM_NUM];
/* 获取一块空闲的物理页面来创建共享内存 */
int sys_shmget(int key, int size)
{
int i;
unsigned long page; /* 存放共享内存地址 */
/* 查看 key 对应的共享内存是否已存在 */
for (i = 0; i < _SHM_NUM; i++)
if(shm_tables[i].key == key)
return i;
/* 内存大小超过一页 */
if (size > PAGE_SIZE)
return -EINVAL;
/* 获取一块空闲物理内存页面,返回起始物理地址 */
page = get_free_page();
if(!page)
return -ENOMEM;
/* 记录到共享内存表中 */
for (i = 0; i < _SHM_NUM; i++) {
if(shm_tables[i].key == 0) {
shm_tables[i].key = key;
shm_tables[i].size = size;
shm_tables[i].page = page;
return i;
}
}
return -1; /* 共享内存数量已满 */
}
/* 将指定物理页面映射到当前进程的虚拟内存空间 */
void * sys_shmat(int shmid)
{
int i;
unsigned long data_base;
/* 判断共享内存 shmid 是否越界 及 共享内存是否存在 */
if (shmid < 0 || shmid >= _SHM_NUM || shm_tables[shmid].key == 0)
return -EINVAL;
/* 把物理页面映射到进程的虚拟内存空间,映射到代码段+数据段后,堆栈段前 */
put_page(shm_tables[shmid].page, current->brk + current->start_code);
/* 修改总长度,brk为代码段和数据段的总长度 */
current->brk += PAGE_SIZE;
return (void*)(current->brk - PAGE_SIZE);
}
对程序中
sys_shmat
中的虚拟内存地址进行解释:
brk
为代码段和数据段的总长度,brk + start_code
即为代码段+数据段结束的位置;start_stack
为栈的起始地址。- 因此,将物理内存映射到虚拟内存处,
brk
和start_stack
之间的空间为栈准备,栈底是闲置的(栈是往低地址方向扩展的),可将共享内存映射到这块空间。
2、修改文件include/unistd.h
,新增全局函数
/* 实验6 */
#define __NR_sem_open 72
#define __NR_sem_wait 73
#define __NR_sem_post 74
#define __NR_sem_unlink 75
/* 实验7 */
#define __NR_shmget 76
#define __NR_shmat 77
3、修改/kernel/system_call.s
,需要修改总的系统调用数
nr_system_calls = 78
4、修改/include/linux/sys.h
,声明全局新增函数
/* 实验6 */
extern int sys_sem_open();
extern int sys_sem_wait();
extern int sys_sem_post();
extern int sys_sem_unlink();
/* 实验7 */
extern int sys_shmget();
extern int sys_shmat();
fn_ptr sys_call_table[] = {
//...sys_setreuid,sys_setregid,sys_whoami,sys_iam,
sys_sem_open,sys_sem_wait,sys_sem_post,sys_sem_unlink, /* 实验6 */
sys_shmget, sys_shmat /* 实验7 */
};
5、修改 linux-0.11/kernel/Makefile
,添加sem.c
编译规则
OBJS = sched.o system_call.o traps.o asm.o fork.o \
panic.o printk.o vsprintf.o sys.o exit.o \
signal.o mktime.o who.o sem.o
// ...
### Dependencies:
sem.s sem.o: sem.c ../include/linux/sem.h ../include/linux/kernel.h \
../include/unistd.h
6、实现生产者程序 producer.c
和 消费者程序 consumer.c
为了确保对共享内存操作的互斥,仍需要使用一个信号量在每次读写的时候进行限制;同理,还需要两个信号量保来保证共享内存中缓冲区大小为 10
producer.c
#define __LIBRARY__ /* 在第一行添加 */
#include <stdio.h>
#include <stdlib.h>
#include <linux/sem.h>
#include <unistd.h>
#include <fcntl.h>
#define SIZE 10
#define M 510
/* add */
_syscall2(int,sem_open,const char*,name,unsigned int,value)
_syscall1(int,sem_wait,sem_t *,sem)
_syscall1(int,sem_post,sem_t *,sem)
_syscall1(int,sem_unlink,const char *,name)
_syscall2(int,shmget,int,key,int,size)
_syscall1(int, shmat, int, shmid)
int main()
{
int shm_id;
int count = 0;
int *p;
int curr;
sem_t *sem_empty, *sem_full, *sem_shm; /*3个信号量*/
sem_empty = sem_open("empty", SIZE);
sem_full = sem_open("full", 0);
sem_shm = sem_open("shm", 1);
shm_id = shmget(2521, SIZE); /* 获取一块空闲的物理页面来创建共享内存 */
p = (int *)shmat(shm_id); /* 将指定物理页面映射到当前进程的虚拟内存空间 */
/*生产多少个产品就循环几次*/
while (count <= M) {
/*empty大于0,才能生产*/
sem_wait(sem_empty); /* empty-- */
sem_wait(sem_shm); /* mutex-- */
/*从上次位置继续向文件缓冲区写入一个字符*/
curr = count % SIZE; /*更新写入缓冲区位置,保证在0-9之间,缓冲区最大为10*/
*(p + curr) = count;
printf("Producer: %d\n", *(p + curr));
fflush(stdout);
sem_post(sem_shm); /* mutex++ */
sem_post(sem_full); /* full++,唤醒消费者线程 */
count++;
}
printf("producer end.\n");
fflush(stdout);
return 0;
}
consumer.c
#define __LIBRARY__ /* 在第一行添加 */
#include <stdio.h>
#include <stdlib.h>
#include <linux/sem.h>
#include <unistd.h>
#include <fcntl.h>
#define SIZE 10
#define M 510
/* add */
_syscall2(int,sem_open,const char*,name,unsigned int,value)
_syscall1(int,sem_wait,sem_t *,sem)
_syscall1(int,sem_post,sem_t *,sem)
_syscall1(int,sem_unlink,const char *,name)
_syscall2(int,shmget,int,key,int,size)
_syscall1(int, shmat, int, shmid)
int main()
{
int shm_id;
int count = 0;
int *p;
int curr;
sem_t *sem_empty, *sem_full, *sem_shm; /*3个信号量*/
sem_empty = sem_open("empty", SIZE);
sem_full = sem_open("full", 0);
sem_shm = sem_open("shm", 1);
shm_id = shmget(2521, SIZE); /* 获取一块空闲的物理页面来创建共享内存 */
p = (int *)shmat(shm_id); /* 将指定物理页面映射到当前进程的虚拟内存空间 */
/*生产多少个产品就循环几次*/
while(count <= M) {
/* full大于0,才能消费 */
sem_wait(sem_full); /* full-- */
sem_wait(sem_shm); /* mutex-- */
/*从上次位置继续向文件缓冲区写入一个字符*/
curr = count % SIZE; /*更新写入缓冲区位置,保证在0-9之间,缓冲区最大为10*/
printf("%d:%d\n", getpid(), *(p + curr));
fflush(stdout);
sem_post(sem_shm); /* mutex++ */
sem_post(sem_empty); /* empty++,唤醒生产者进程 */
count++;
}
printf("consumer end.\n");
fflush(stdout);
/*释放信号量*/
sem_unlink("empty");
sem_unlink("full");
sem_unlink("shm");
return 0;
}
三、在linux0.11环境下编译运行
- 将已经修改的
consumer.c
、producer.c
和unistd.h
、sem.h
(详见Lab6)文件拷贝到linux-0.11系统中
cd oslab_Lab6
sudo ./mount-hdc
cp ./consumer.c ./hdc/usr/root/
cp ./producer.c ./hdc/usr/root/
cp ./linux-0.11/include/unistd.h ./hdc/usr/include/
cp ./linux-0.11/include/linux/sem.h ./hdc/usr/include/linux/
sudo umount hdc
- 编译及运行Bochs
cd oslab_Lab6/linux-0.11
make all
../run
- 在linux0.11的Bochs中编译生产者-消费者程序
gcc -o producer producer.c
gcc -o consumer consumer.c
sync
./producer > p.txt &
./consumer > c.txt
&
表示使用终端的后台运行功能。
- 在Ubuntu中挂载
hdc
,将linux0.11输出的1.txt
文件移动到Ubuntu中
sudo ./mount-hdc
cd /hdc/usr/root/
sudo cp ./p.txt ../../..
sudo cp ./c.txt ../../..
打开p.txt
和 c.txt
文件即可见到输出如下: