操作系统实验6:地址映射与共享

本次实践项目有两个基本内容:
(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+3
4”:
在这里插入图片描述
其中的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:系统调用添加系统调用。

  1. 添加系统调用的编号
    系统调用编号在 include/unistd.h中定义,打开该文中找到系统调用编号的定义:
    在这里插入图片描述

  2. 添加IDT(中断描述符表)
    打开include/linux/sys.h文件,在文件中的sys_call_table[]的数组中添加sys_shmget和sys_shmat,注意这里的前后顺序要和之前的系统调用编号的前后关系对应起来。同时,将sys_shmget()和sys_shmat()声明全局函数。
    在这里插入图片描述

  3. 修改系统调用数
    修改kernel/system_call.s中的系统调用数,由原来的72改为74.在这里插入图片描述

  4. 实现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;
}
  1. 修改Makefile文件
    文件位置:kernel/Makefile
    在这里插入图片描述
    在这里插入图片描述
    修改完成之后重新编译整个工程。

  2. 编写测试代码
    这里编写两个测试进程,一个进程向共享内存页中写入数据,另一进程从中读取数据,比对写入和读出的数据就能验证实验过程是否正确。
    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;
}

  1. 文件拷贝
    先在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/
  1. 编译并运行测试代码
    运行Linux-0.11,然后编译test1.c和test2.c,命令如下:
gcc -o test1 test1.c 
gcc -o test2 test2.c
sync 

注意,Linux-0.11只有一个终端,而现在需要在一个终端上同时运行两个程序,方法是在命令末尾输入&,命令就会进入后台运行。命令如下:

./test1 &
./test2 &

然后就可看到打印出来的信息了,如下图所示:
在这里插入图片描述
可以看出,写入和读出的数据是一样的。
注意,在实验过程发现,如果一个进程结束会将共享的物理内存页释放,此时如果另一个进程再去读写该物理内存页就会导致错误!

至此,整个实验结束!

  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

忆昔z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值