本次实践项目有两个基本内容:
(1)用Bochs调试工具跟踪Linux-0.11的地址转换过程;
(2)实现基于共享物理页框的进程间内存共享。
知识点补充
GDT和GDTR
和一个段有关的信息需要 8 个字节来描述,所以称为段描述符(Segment Descriptor),每个段都需要一个描述符。为了存放这些描述符,需要在内存中开辟出一段空间。在这段空间里,所有的描述符都是挨在一起,集中存放的,这就构成一个描述符表。最主要的描述符表是全局描述符表(Global Descriptor Table, GDT)。为了跟踪全局描述符表,处理器内部有一个 48 位的寄存器,称为全局描述符表寄存器(GDTR),该寄存器分为两部分,分别是 32 位的线性地址和 16 位的边界。
存储器的段描述符格式
段选择子的组成
在保护模式下访问一个段时,传送到段选择器的是段选择子。它由三部分组成,第一部分是描述符的索引号,用来在描述符表中选择一个段描述符。 TI 是描述符表指示器(Table Indicator), TI=0 时,表示描述符在 GDT 中; TI=1 时,描述符在 LDT 中。 RPL 是请求特权级,表示给出当前选择子的那个程序的特权级别,正是该程序要求访问这个内存段。
地址转换过程跟踪
要跟踪地址转换过程,首先需要以汇编级调试的方式启动Bochs,即在编译好Linux-0.11后,通过运行命令./dbg-asm来启动调试器,此时Bochs模拟器会处于黑屏状态,执行命令的宿主主机窗口中的显示如下图所示:
"Next at t=0"表示下面执行的指令是Bochs启动后要执行的第一条指令,单步跟踪进去就能看到BIOS代码。现在直接输入命令“c”,即继续运行程序,Bochs和以前一样启动Linux-0.11。
现在需要在Linux-0.11上编写一个测试程序test.c,要跟踪的地址就是这个程序中的地址。test.c代码如下:
#include <stdio.h>
int i = 0x12345678;
int main(void)
{
printf("The logical address of i is 0x%08x",&i);
fflush(stdout);
while(i);
return 0;
}
将test.c拷贝到Linux-0.11上编译、运行,运行输出如下:
The logical address of i is 0x00003004
由于打印的是逻辑地址,即离开程序段首的偏移地址,所以只要程序test.c不发生变化,0x00003004这个值也是不会变化的,即在同一机器上多次运行test.c,这个逻辑地址也是一样的。
由于test.c中有一个死循环,所以这个程序不会主动退出,正是这样,其各种资源,如逻辑地址、LDT表、GDT表、页表等信息才能在调试器中用调试命令查看。
现在在Bochs命令行窗口按下Ctrl + c键,Bochs会暂停运行,进入调试状态。此时的Bochs会有很大的可能是在test.c中运行,因为此时Linux-0.11中进程很少。宿主机调试器窗口会显示类似如下信息:
若其中的"000f"显示为"0008",则说明按下Ctrl+c中断发生在内核中,这时需要输入c继续执行,然后再按下Ctrl+c直到变为"000f"为止。如果显示的指令不是cmp,就用"n"命令单步运行几步,直到停在cmp指令上,实际上就是停在while(i)语句处。然后用“u/8命令”,显示从当前位置开始的8条指令的反汇编代码,如下图所示:
这正是从while(i)开始到return语句的汇编代码。不难分析出,变量i就保存在地址DS:0x3004处。cmp指令要将DS:3004处存放的内容和0就行比较,只有等于0才跳出循环,即执行"jz .+0x00000004"。
现在要开始寻找逻辑地址DS:0x3004对应的物理地址,即开始跟踪地址转换过程。由于是段页式内存结构,所以要先用段表找到虚拟地址。DS:0x3004是逻辑地址,DS表明这个地址属于DS段,只有找到进程对应的段表以后,才能通过DS寄存器的值在段表中找到DS段的具体信息,得到虚拟地址。这个段表就是进程的LDT表,接下来就要找到这个LDT表,LDTR就是起点。LDTR寄存器中存放的是当前进程LDT表地址在GDT表中的偏移值。
用"sreg"命令可以看到各个寄存器的信息:
可以看到ldtr的值是0x0068 = 0000000001101000,根据段选择子的结构,当前进程的LDT存放在GDT表中的13(1101)号位置。
GDT表的位置由GDTR寄存器明确给出,即在物理地址0x00005cb8位置处,GDT表中每一项占8个字节,所以我们要查找的LDT表的物理地址是0x00005cb8 + 13 * 8。用命令“xp /2w 0x00005cb8+13*8”可以查看这个位置的内容:
这两步在不同的机器上执行时得到的数值可能不一样,这是正常的。如果向确认是否正确,就看执行sreg命令后的输出信息中,ldtr所在行里的dl和dh的值,它们是Bachs自动计算出来的,从GDT表中找到的LDT地址应该和Bochs计算出来的一致。
将得到的数字“0xa2d00068 0x000082f9”进行组合,组合方式如下,得到LDT表的物理地址0x00f9a2d0,这就是LDT表的物理地址,组合方式是由段描述符的格式决定的。
执行命令“xp /8w 0x00f9a2d0”可以得到:
这就是当前进程LDT表的前四项内容了。
现在可以根据DS寄存器来查找LDT表了,由上面"sreg"命令获得的寄存器信息“ds:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=3”,可以直到,DS寄存器的值是0x0017,按照段选择子的格式,0x0017 = 0x0000000000010111,去掉最低三位,剩余的位组合起来得到的数值是2,即偏移为,因此是第三项,即“0x00003fff 0x10c0f300”,这个就是DS段的信息。用同样的组合方式组合“0x00003fff 0x10c0f300”得到DS段的基址为0x10000000,这就是当前进程DS段在虚拟内存空间中的起始地址。因此DS:0x3004对应的虚拟地址为:
0x10000000+3004 = 0x10003004
现在已经得到了虚拟地址,接下来就要将其转换为物理地址,核心就是查找页表。首先要计算出虚拟地址中的页目录号、页表号和页内偏移,它们分别对应了虚拟地址的前10位、中间10位和末尾的12位。不难计算出,虚拟地址0x10003004对应的页目录号是64,页号是3,页内偏移是4。页目录表的位置由CR3寄存器给出,用“creg”命令可以看到:
说明页目录表的基址为0。页目录表和表中内容都很简单,就是1024个32位二进制树,这32位中的前20位表示物理页框号,后面是一些属性信息(其中最重要的是最后一位P,表示是否有效)。第65页目录项就是要找的内容,用命令“xp /w 0+644”查看:
其中的027是属性,显然P=1,因此这个页目录项是有效的。因此页表所在物理页框号位0x00fa9,即该页目录对应的1024个页的所有页表项信息存放在物理地址0x00fa9000处,从该位置开始查找第3个页表项,即“xp /w 0x00fa9000+34”:
其中的067是属性,显然P=1,说明页表项也是有效的。
现在已知虚拟地址0x10003004对应的物理页框号为0x00fa7000,将它和页内偏移0x0004连接到一起,得到物理地址为0x00fa7004,这个就是变量i的物理地址,用命令“xp /w 0x00fa7004”查看:
得到的数值就是变量i的值,说明这个过程是正确的。
现在直接修改内存来改变i的值,使用命令“setpmem 0x00fa7004 4 0”实现,表示从0x00fa7004地址开始的4个字节都设置为0,然后使用“c”命令继续Bochs的运行,可以看到test进程退出了,说明i变量修改成功了。
基于共享物理页框的进程间内存共享的实现
在Linux下,可以通过shmget()和shmat()两个系统调用来使用共享内存。因此本部分的具体实现内容就是在Linux-0.11下添加shmget()和shmat()两个系统调用(Linux-0.11上没有这两个系统调用)。添加系统调用的具体过程可以参照操作系统实验2:系统调用。
shmget()系统调用的函数原型为:
int shmget(key_t key, size_t size, int shmflg);
该系统调用会新建/打开一页物理内存作为共享内存,并返回该页共享内存的shmid,即该页共享内存在操作系统中的标识。如果多个进程使用相同的key调用shmget,则这些进程就会获得相同的shmid,即得到同一块内存的标识。在shmget实现时,如果key所对应的共享内存已经建立,则直接返回shmid,否则新建。如果size超过一页内存的大小,返回-1,并置errno为EINVAL。如果系统无空=空闲内存,返回-1,并置errno为ENOMEM。对于本实验,shmflg参数忽略。
shmat()系统调用的函数原型为:
void* shmat(int shmid,const void *shmaddr, int shmflg);
该系统调用会将shmid指定的共享页面映射到当前进程的虚拟地址空间中,并返回一个逻辑地址p,调用进程可以通过读写逻辑地址p来读写这一页共享内存。如果shmid非法,返回-1,并置errno为EINVAL。对于本实验,参数shmaddr和shmflg都忽略。
因此,两个进程都调用shmat可以关联到同一页内存上,此时两个进程读写p指针就是在读写同一页内存,从而实现了基于共享内存的进程间通信。
下面参照操作系统实验2:系统调用添加系统调用。
-
添加系统调用的编号
系统调用编号在 include/unistd.h中定义,打开该文中找到系统调用编号的定义: -
添加IDT(中断描述符表)
打开include/linux/sys.h文件,在文件中的sys_call_table[]的数组中添加sys_shmget和sys_shmat,注意这里的前后顺序要和之前的系统调用编号的前后关系对应起来。同时,将sys_shmget()和sys_shmat()声明全局函数。 -
修改系统调用数
修改kernel/system_call.s中的系统调用数,由原来的72改为74. -
实现sys_shmget()和sys_shmat()
在kernel/目录下新建一个shm.c文件,用于保存这个两个函数。
在include/目录下新建一个shm.h文件,用于相关数据类型声明和函数声明。sys_shmget()函数的主要作用是获得一个空闲的物理页面,可以通过调用已有的get_free_page()函数来实现。
sys_shmat()的主要作用是将这个页面和进程的虚拟地址以及逻辑地址关联起来,让进程对某个逻辑地址的读写就是在读写该内存页。该函数首先要完成虚拟地址和物理页面的映射,核心就是填写页表,在Linux-0.11中的函数:
unsigned long put_page(unsigned long page,unsigned long address)
该函数的作用就是完成这样的映射,直接调用即可。函数中的page就是内存页的物理地址,即shm_list[shmid].page,address是虚拟地址。Linux-0.11给每个进程分配了64M的虚拟内存,其分布如下图所示。
可以看出,brk和start_stack之间的虚拟内存并没有使用,因此可以在这里分割一个虚拟内存页和那个物理内存页建立映射。brk和start_stack都是存储在进程的PCB中,可以用current->brk找到当前进程的brk,当前进程开始的虚拟地址存放在current->ldt[1]中,可以用get_base(current->ldt[1])获得,因此该虚拟内存页的虚拟地址为get_base(current->ldt[1]) + current->brk.
这样调用put_page函数就可以建立映射关系了。最后需要更新brk指针的指向,并返回虚拟内存页的逻辑地址,即原来的brk。
shm.h文件的内容如下:
#include <stddef.h>
typedef unsigned int key_t;
struct struct_shmem
{
unsigned int size;
unsigned int key;
unsigned long page;
};
int shmget(key_t key, size_t size);
void* shmat(int shmid);
#define SHM_NUM 16
shm.c文件内容如下:
#include <shm.h>
#include <linux/mm.h>
#include <unistd.h>
#include <errno.h>
#include <linux/kernel.h>
#include <linux/sched.h>
struct struct_shmem shm_list[SHM_NUM] = {{0,0,0}};
int sys_shmget(key_t key, size_t size)
{
int i;
unsigned long page;
if(size > PAGE_SIZE){
errno = EINVAL;
printk("shmget:The size connot be greater than the PAGE_SIZE!\r\n");
return -1;
}
if(key == 0){
printk("shmget:key connot be 0!\r\n");
return -1;
}
//判斷是否已经创建
for(i = 0; i < SHM_NUM; i++){
if(shm_list[i].key == key)
return i;
}
page = get_free_page(); //申请内存页
if(!page){
errno = ENOMEM;
printk("shmget:connot get free page!\r\n");
return -1;
}
for(i = 0; i < SHM_NUM; i++){
if(shm_list[i].key == 0){
shm_list[i].size = size;
shm_list[i].key = key;
shm_list[i].page = page;
break;
}
}
return i;
}
void* sys_shmat(int shmid)
{
unsigned long tmp; //虚拟地址
unsigned long logicalAddr;
if(shmid < 0 || shmid >= SHM_NUM || shm_list[shmid].page == 0 || shm_list[shmid].key <= 0){
errno = EINVAL;
printk("shmat:The shmid id invalid!\r\n");
return NULL;
}
tmp = get_base(current->ldt[1]) + current->brk; //计算虚拟地址
put_page(shm_list[shmid].page,tmp);
logicalAddr = current->brk; //记录逻辑地址
current->brk += PAGE_SIZE; //更新brk指针
return (void *)logicalAddr;
}
-
修改Makefile文件
文件位置:kernel/Makefile
修改完成之后重新编译整个工程。 -
编写测试代码
这里编写两个测试进程,一个进程向共享内存页中写入数据,另一进程从中读取数据,比对写入和读出的数据就能验证实验过程是否正确。
test1.c每间隔5秒向共享内存页中写入递增的数据,代码如下:
#define __LIBRARY__
#include <shm.h>
#include <unistd.h>
static inline _syscall1(void*,shmat,int,shmid);
static inline _syscall2(int,shmget,key_t,key,size_t,size);
int main()
{
key_t key = 666;
size_t size = sizeof(int);
int shmid = shmget(key,size);
int* p = (int*)shmat(shmid);
*p = 0;
while(1){
(*p)++;
printf("process1:write %d\r\n",*p);
sleep(5);
}
return 0;
}
test2.c每间隔5妙从共享内存页中读取数据,代码如下:
#define __LIBRARY__
#include <shm.h>
#include <unistd.h>
static inline _syscall1(void*,shmat,int,shmid);
static inline _syscall2(int,shmget,key_t,key,size_t,size);
int main()
{
key_t key = 666;
size_t size = sizeof(int);
int shmid = shmget(key,size);
int* p = (int*)shmat(shmid);
while(1){
printf("process2:read %d\r\n",*p);
sleep(5);
}
return 0;
}
- 文件拷贝
先在lab6目录下,执行下列命令就行挂载:
sudo ./mount-hdc
然后将test1.c和test2.c拷贝到linux-0.11中,命令如下:
cp test1.c test2.c ./hdc/usr/root/
另外,在test1.c和test2.c中用到shm.h和修改过的unistd.h,因此也需要将这两个文件拷贝到Linux-0.11中,命令如下(在lab6目录下):
cp ./linux-0.11/include/unistd.h ./hdc/usr/include/
cp ./linux-0.11/include/shm.h ./hdc/usr/include/
- 编译并运行测试代码
运行Linux-0.11,然后编译test1.c和test2.c,命令如下:
gcc -o test1 test1.c
gcc -o test2 test2.c
sync
注意,Linux-0.11只有一个终端,而现在需要在一个终端上同时运行两个程序,方法是在命令末尾输入&,命令就会进入后台运行。命令如下:
./test1 &
./test2 &
然后就可看到打印出来的信息了,如下图所示:
可以看出,写入和读出的数据是一样的。
注意,在实验过程发现,如果一个进程结束会将共享的物理内存页释放,此时如果另一个进程再去读写该物理内存页就会导致错误!文章来源:https://www.toymoban.com/news/detail-414579.html
至此,整个实验结束!文章来源地址https://www.toymoban.com/news/detail-414579.html
到了这里,关于操作系统实验6:地址映射与共享的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!