一、实验内容
- 用 Bochs 调试工具跟踪 Linux 0.11 的地址翻译(地址映射)过程,了解 IA-32 和 Linux 0.11 的内存管理机制;
- 在 Ubuntu 上编写多进程的生产者—消费者程序,用共享内存做缓冲区;
- 在信号量实验的基础上,为 Linux 0.11 增加共享内存功能,并将生产者—消费者程序移植到 Linux 0.11。
二、跟踪地址翻译过程
1.将test.c
传入linux-0.11,按照实验指导书进行实验。
当 test 运行的时候,在命令行窗口按 Ctrl+c,Bochs 会暂停运行,进入调试状态。
命令行窗口指的是
./dbg-asm
这个窗口。
只要 test 不变,0x00003004 这个值在任何人的机器上都是一样的。即使在同一个机器上多次运行 test,也是一样的。
疑惑点1:这是为啥呢?
回答
:test.c被编译后,按照程序分段的逻辑,每个段(代码段、数据段、栈段)的逻辑地址都是从0开始的,编译器为i分配的地址是逻辑地址。源码没变,这个逻辑地址就不会变。要载入内存时,该程序的每段会映射到虚拟内存的空闲区域(每个段在不同的区域)。段基址 + 逻辑地址(本质上是偏移地址)= 虚拟地址(虚拟地址对应的存储单元并不需要真实存在,所以叫虚拟内存)。
2.地址翻译过程
(1)变量 i 保存在ds:0x3004
这个地址
(2)ds 表明这个地址属于 ds 段–>找段表,即找LDT。
①找段表–>找LDT基址–>看ldtr的值(如下图所示)
解读:
0x0068 = 0000000001101000B,根据下图段选择子的结构:
得到段描述符索引即0000000001101B =13D
表示 LDT 的段描述符存放在 GDT 表的13号位置。
补充:RPL = 0。之后要访问GDT表的13号位置,能有没有权限去访问呢?RPL <= DPL就可以访问。
GDT表的13号位置的段描述符,就可以看到DPL = 0,所以可以访问。
②找GDT基址–>看gdtr的值(如下图所示)
这是GDT的基址。
③综合①和②得到LDT基址
- GDT表中的每一项占8个字节(2个字)
如上所述,LDT 的段描述符存放在 GDT 表的13号位置。
更好的方式:
实际上,找到ldtr时,就得到了LDT 的段描述符:
- 根据段描述符的结构(如下图所示),得到LDT的物理地址:
0x00fa72d0
第1行指dh,第2行指dl。
LDT的物理地址(即由加粗部分拼成):0x000082fa 0x72d00068
DPL = 0
(3)查ds段属于LDT的哪个表项
0x0017 = 0000000000010111B, 根据上文给的段选择子的结构,
①要想访问该段,则有如下要求:
疑惑点2:但之前将系统调用的时候,是这么说的:DPL(0表示内核态,3表示用户态)>=RPL(请求特权级);
回答
:上文,ldtr的RPL = 0,GDT表的13号表项的DPL = 0,RPL <= DPL,所以,可以访问。这样就得到了LDT的基址(物理地址)。接着,ds段,要去访问LDT表的第2个表项,其DPL = 3,ds段的RPL也是3,RPL <= DPL,所以,可以访问。
②TI = 1,所以去ds段的段描述符在LDT表的2号表项中。
(4)查看LDT表的2号表项(ds段所在的表项)
验证:
(5)ds段的段基址
根据上文给的段描述符的结构,可得ds段的段基址:0x10000000
(6)ds:0x3004
的线性地址
0x10000000 + 0x3004 = 0x10003004
验证:
0x10003004 = 268447748D(十进制)
(7)线性地址变成物理地址的过程
0x10003004 = 0001 0000 0000 0000 0011 0000 0000 0100B
页目录号(10位):0001000000B = 2 6 D = 64 D 2^6D = 64D 26D=64D
页表号(10位):0000000011B = 3D
页内偏移(12位):000000000100B = 4D
1)查页目录表的基址
IA-32 下,页目录表的位置由 CR3 寄存器指引。
页目录表的基址为 0
2)查64号页表的基址
页目录表的表项是1个字(4B),所以64号表项内容如下:
32 位中前 20 位是物理页框号,后面是一些属性信息(其中最重要的是最后一位P,P指Present,P=1表示存在)
所以页表的基址0x00faa000
3)查页表3号页表项的内容
4)变量i的物理地址
0x00fa9000 + 0x004 = 0x00fa9004
验证:
3.地址翻译过程的总结
三、基于共享内存的生产者—消费者程序
1.producer.c的源码及注释
#define __LIBRARY__ //使用了_syscalln
#include <shm.h> //使用了shm_t
#include <stdio.h> //使用了printf()
#include <sem.h> //信号量的头文件
#include <unistd.h> //用到了 __NR_xxx
#include <stdlib.h> //使用了fflush
/* 信号量系统调用 */
_syscall2(sem_t*,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,key_t,key,size_t,size);
_syscall1(void*,shmat,int,shmid);
#define BUFSIZE 10 //缓冲区大小
#define MAX_NUM 500 //为了测试方便,item_num最大为500
#define KEY 1024 //用于为共享内存段命名
sem_t *empty, *full, *mutex;
void producer(int *p) {
//生产一个产品 item;
int item_num;
printf("I am a producer, here are contents of producing:\n");
fflush(stdout);
for(item_num = 0; item_num <= MAX_NUM; item_num++) {
//test是否有空闲的缓存资源
sem_wait(empty);
//通过互斥信号量,实现互斥访问
sem_wait(mutex);
/* 将item放到空闲缓存中 */
*(p + item_num % BUFSIZE) = item_num;
printf("%d\n", item_num);
fflush(stdout);
sem_post(mutex);
//增加产品资源
sem_post(full);
}
printf("Producer, over.\n");
}
int main() {
long key = KEY;
int shmid;
int *p;
int i;
/* 为了防止已经open了如下信号量,先释放*/
sem_unlink("empty");
sem_unlink("full");
sem_unlink("mutex");
/* 创建信号量 */
empty = sem_open("empty", BUFSIZE);
full = sem_open("full", 0);
mutex = sem_open("mutex", 1);
/* 使用共享内存作为缓冲区 */
shmid = shmget(key, (MAX_NUM + 1) * sizeof(int));
p = (int *)shmat(shmid);
/* 生产者 */
producer(p);
return 0;
}
2.consumer.c的源码及注释
#define __LIBRARY__
#include <shm.h>
#include <stdio.h>
#include <sem.h>
#include <unistd.h>
#include <stdlib.h>
_syscall2(sem_t*,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,key_t,key,size_t,size);
_syscall1(void*,shmat,int,shmid);
#define BUFSIZE 10
#define MAX_NUM 500
#define KEY 1024
sem_t *empty, *full, *mutex;
void consumer(int *p) {
int item_num, i;
printf("I am a consumer, here are contents of consuming:\n");
fflush(stdout);
for(i = 0; i <= MAX_NUM; i++) {
sem_wait(full);
sem_wait(mutex);
item_num = *(p + i % BUFSIZE);
printf("item_num = %d\n", item_num);
fflush(stdout);
sem_post(mutex);
sem_post(empty);
}
printf("Consumer, over.\n");
fflush(stdout);
}
int main() {
long key = KEY;
int shmid;
int *p;
empty = sem_open("empty", BUFSIZE);
full = sem_open("full", 0);
mutex = sem_open("mutex", 1);
shmid = shmget(key, (MAX_NUM + 1) * sizeof(int));
p = (int *)shmat(shmid);
consumer(p);
/* 此时生产者和消费者都不再使用信号量了,进行释放 */
sem_unlink(empty);
sem_unlink(full);
sem_unlink(mutex);
return 0;
}
删除注释,拷贝到linux-0.11的root目录下,并可进行验证,结果见“五、”。
四、共享内存的实现
1.准备工作
(1)在include/sys/types.h
将key_t定义为long:
注意
:还要修改linux-0.11中types.h
(2)在include\shm.h
中定义结构体shm_t;
#ifndef _SHM_H_
#define _SHM_H_
//共享内存的个数
#define SHM_NUM 36
typedef struct {
unsigned int key; //共享内存的键
unsigned int size; //共享内存的大小
unsigned long address; //共享内存的首地址
}shm_t;
#endif
共享内存也是一种资源,所以也要向管理信号量一样,定义结构体去管理共享内存的使用情况。
注意
:还要把该文件拷贝到linux-0.11的include目录下
2.shm.c
(在linux-0.11/mm
目录下)
(0)流程见“二、2.”
(1)实现2个系统调用函数:
int sys_shmget(key_t key, size_t size);
void *sys_shmat(int shmid);
(2)完整源码及注释
#include <shm.h> //使用了shm_t这个结构体
#include <sys/types.h> //使用了key_t的定义
#include <linux/mm.h> //使用了PAGE_SIZE, get_free_page(), put_page()
#include <linux/sched.h> //使用了get_base宏函数
#include <errno.h>
static shm_t shm_list[SHM_NUM]; //静态变量,下一次调用的时候保持原来的赋值;
/* 新建/打开一页内存 */
int sys_shmget(key_t key, size_t size) {
void *address; //指向空闲页面的首地址;
int i;
/* 如果曾经分配过,直接返回下标 */
for(i = 0; i < SHM_NUM; i++) {
if(shm_list[i].key == key)
return i;
}
if(size > PAGE_SIZE)
return -EINVAL;
address = get_free_page();
if(!address)
return -ENOMEM;
/* 找到1个可用的共享内存 */
for(i = 0; i < SHM_NUM; i++) {
if(shm_list[i].key == 0) {
shm_list[i].key = key;
shm_list[i].size = size;
shm_list[i].address = address;
return i;
}
}
return -ENOMEM;
}
/* 将 shmid 指定的共享页面映射到当前进程的虚拟地址空间中,并将其首地址返回 */
void *sys_shmat(int shmid) {
void *linear_address;
if(shmid < 0)
return -EINVAL;
/* 寻找空闲的虚拟地址空间 */
linear_address = get_base(current->ldt[2]) + current->brk;
current->brk += PAGE_SIZE;
if(shm_list[shmid].key != 0) {
put_page(shm_list[shmid].address, linear_address); //建立线性地址和物理地址的映射
incr_mem_map(shm_list[shmid].address);
return current->brk - PAGE_SIZE;
}
return -EINVAL;
}
(3)对sys_shmget函数和sys_shmat函数的进一步解读
1)sys_shmget函数
通过address = get_free_page();
就得到了指向4KB内存大小的物理地址。
2)sys_shmat函数(难点也是重点!)
①生产者进程需要先有一块逻辑空间,然后往里面写数据,以便消费者进程去取。
②根据上图可知,current->brk即可作为这块逻辑空间的基址。
③由于current->brk只是偏移地址,所以需要通过current的LDT查到基址。
④逻辑空间(用作生产者-消费者的缓冲区)的虚拟地址[ds:current->brk],和地址翻译中的变量i的虚拟地址[ds:0x3004]是一个道理。
综合①~④,可得如下代码:
linear_address = get_base(current->ldt[2]) + current->brk;
current->brk += PAGE_SIZE;
current->ldt[2]是数据段的段描述符, get_base便可以得到数据段的段基址,加上current->brk这个偏移量,便得到了逻辑空间的线性地址。并更新current->brk。
⑤线性地址映射成物理地址,代码如下:
put_page(shm_list[shmid].address, linear_address); //建立线性地址和物理地址的映射
回顾地址翻译过程,如何将线性地址映射为物理地址。
⑥mem_map找到该共享内存,并+1,表示有1个进程在使用。
incr_mem_map(shm_list[shmid].address);
由于共享内存被n个进程使用,所以在mem_map中标记为n。当一个进程退出后,OS回收其占用的内存,
n--
,直到n = 0
,才真正回收共享内存。
虽然这样解决了
trying to free free page
,但也引入了如下问题(还未解决):
⑦返回逻辑空间的逻辑地址
return current->brk - PAGE_SIZE;
因为程序中的地址是逻辑地址,要理解上述内容,要认真研读
《Linux-0.11内核完全剖析》的5.3.1--5.3.4
。
(4)在linux-0.11/mm/memory.c
中增加incr_mem_map函数
void incr_mem_map(unsigned long addr)
{
mem_map[MAP_NR(addr)]++;
}
3.修改mm/Makefile
文件
五、验证
1.producer.c的输出结果重定向到producer.out
1.consumer.c的输出结果重定向到consumer.out