HIT oslab之实验7 地址映射与共享

一、实验内容

  • 用 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.hkey_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
在这里插入图片描述
在这里插入图片描述

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值