操作系统原理与实践8-地址映射与共享

地址映射与共享

难度系数:★★★★☆

实验目的

  • 深入理解操作系统的段、页式内存管理,深入理解段表、页表、逻辑地址、线性地址、物理地址等概念;
  • 实践段、页式内存管理的地址映射过程;
  • 编程实现段、页式内存管理上的内存共享,从而深入理解操作系统的内存管理。

实验内容

本次实验的基本内容是:

  • 用Bochs调试工具跟踪Linux 0.11的地址翻译(地址映射)过程,了解IA-32和Linux 0.11的内存管理机制;
  • 在Ubuntu上编写多进程的生产者—消费者程序,用共享内存做缓冲区;
  • 在信号量实验的基础上,为Linux 0.11增加共享内存功能,并将生产者—消费者程序移植到Linux 0.11。

跟踪地址翻译过程

首先以汇编级调试的方式启动bochs,引导Linux 0.11,在0.11下编译和运行test.c。它是一个无限循环的程序,永远不会主动退出。然后在调试器中通过查看各项系统参数,从逻辑地址、LDT表、GDT表、线性地址到页表,计算出变量i的物理地址。最后通过直接修改物理内存的方式让test.c退出运行。test.c的代码如下:

#include 

int i = 0x12345678;

int main(void)
{
    printf("The logical/virtual address of i is 0x%08x", &i);
    fflush(stdout);

    while (i)
        ;

    return 0;
}

基于共享内存的生产者—消费者程序

本项实验在Ubuntu下完成,与信号量实验中的pc.c的功能要求基本一致,仅有两点不同:

  • 不用文件做缓冲区,而是使用共享内存;
  • 生产者和消费者分别是不同的程序。生产者是producer.c,消费者是consumer.c。两个程序都是单进程的,通过信号量和缓冲区进行通信。

Linux下,可以通过shmget()和shmat()两个系统调用使用共享内存。

共享内存的实现

进程之间可以通过页共享进行通信,被共享的页叫做共享内存,结构如下图所示:

图1 进程间共享内存的结构

本部分实验内容是在Linux 0.11上实现上述页面共享,并将上一部分实现的producer.c和consumer.c移植过来,验证页面共享的有效性。

具体要求在mm/shm.c中实现shmget()和shmat()两个系统调用。它们能支持producer.c和consumer.c的运行即可,不需要完整地实现POSIX所规定的功能。

shmget()

int shmget(key_t key, size_t size, int shmflg);

shmget()会新建/打开一页内存,并返回该页共享内存的shmid(该块共享内存在操作系统内部的id)。所有使用同一块共享内存的进程都要使用相同的key参数。如果key所对应的共享内存已经建立,则直接返回shmid。如果size超过一页内存的大小,返回-1,并置errno为EINVAL。如果系统无空闲内存,返回-1,并置errno为ENOMEM。shmflg参数可忽略。

shmat()

void shmat(int shmid, const void shmaddr, int shmflg);

shmat()会将shmid指定的共享页面映射到当前进程的虚拟地址空间中,并将其首地址返回。如果shmid非法,返回-1,并置errno为EINVAL。shmaddr和shmflg参数可忽略。

实验报告

完成实验后,在实验报告中回答如下问题:

  • 对于地址映射实验部分,列出你认为最重要的那几步(不超过4步),并给出你获得的实验数据。
  • test.c退出后,如果马上再运行一次,并再进行地址跟踪,你发现有哪些异同?为什么?

评分标准

  • 跟踪地址映射的过程,20%
  • shmget(),10%
  • shmat(),10%
  • producer.c,15%
  • consumer.c,15%
  • 实验报告,30%

实验提示

  • IA-32的地址翻译过程

Linux 0.11完全遵循IA-32(Intel Architecture 32-bit)架构进行地址翻译,Windows、后续版本的Linux以及一切在IA-32保护模式下运行的操作系统都遵循此架构。因为只有这样才能充分发挥CPU的MMU的功能。关于此地址翻译过程的细节,请参考《注释》一书中的5.3.1-5.3.4节。

  • 用Bochs汇编级调试功能进行人工地址翻译

此过程比较机械,基本不消耗脑细胞,做一下有很多好处。

  • 准备

编译好Linux 0.11后,首先通过运行./dbg-asm启动调试器,此时Bochs的窗口处于黑屏状态,而命令行窗口显示:

========================================================================
                       Bochs x86 Emulator 2.3.7
               Build from CVS snapshot, on June 3, 2008
========================================================================
00000000000i[     ] reading configuration from ./bochs/bochsrc.bxrc
00000000000i[     ] installing x module as the Bochs GUI
00000000000i[     ] using log file ./bochsout.txt
Next at t=0
(0) [0xfffffff0] f000:fff0 (unk. ctxt): jmp far f000:e05b         ; ea5be000f0
1>_

“Next at t=0”表示下面的指令是Bochs启动后要执行的第一条软件指令。单步跟踪进去就能看到bios的代码。不过这不是本实验需要的。直接输入命令“c”,continue程序的运行,Bochs一如既往地启动了Linux 0.11。

在Linux 0.11下输入(或拷入)test.c,编译为test,运行之,打印如下信息:

The logical/virtual address of i is 0x00003004

只要test不变,0x00003004这个值在任何人的机器上都是一样的。即使在同一个机器上多次运行test,也是一样的。

test是一个死循环,只会不停占用CPU,不会退出。

  • 暂停

当test运行的时候,在命令行窗口按“ctrl+c”,Bochs会暂停运行,进入调试状态。绝大多数情况下都会停在test内,显示类似如下信息:

(0) [0x00fc8031] 000f:00000031 (unk. ctxt): cmp dword ptr ds:0x3004, 0x00000000 ; 833d0430000000

其中加粗的“000f”如果是“0008”,则说明中断在了内核里。那么就要c,然后再ctrl+c,直到变为“000f”为止。如果显示的下一条指令不是“cmp ...”,就用“n”命令单步运行几步,直到停在“cmp ...”。

使用命令“u /7”,显示从当前位置开始7条指令的反汇编代码,如下:

10000031: (                    ): cmp dword ptr ds:0x3004, 0x00000000 ; 833d0430000000
10000038: (                    ): jz .+0x00000002           ; 7402
1000003a: (                    ): jmp .+0xfffffff5          ; ebf5
1000003c: (                    ): xor eax, eax              ; 31c0
1000003e: (                    ): jmp .+0x00000000          ; eb00
10000040: (                    ): leave                     ; c9
10000041: (                    ): ret                       ; c3

这就是test.c中从while开始一直到return的汇编代码。变量i保存在ds:0x3004这个地址,并不停地和0进行比较,直到它为0,才会跳出循环。

现在,开始寻找ds:0x3004对应的物理地址。

  • 段表

ds:0x3004是虚拟地址,ds表明这个地址属于ds段。首先要找到段表,然后通过ds的值在段表中找到ds段的具体信息,才能继续进行地址翻译。每个在IA-32上运行的应用程序都有一个段表,叫LDT,段的信息叫段描述符。

LDT在哪里呢?ldtr寄存器是线索的起点,通过它可以在GDT(全局描述符表)中找到LDT的物理地址。

用“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=0xc2d00068, dh=0x000082f9, valid=1
tr:s=0x0060, dl=0x52e80068, dh=0x00008bfd, valid=1
gdtr:base=0x00005cc8, limit=0x7ff
idtr:base=0x000054c8, limit=0x7ff

可以看到ldtr的值是0x0068=0000000001101000(二进制),表示LDT表存放在GDT表的1101(二进制)=13(十进制)号位置(每位数据的意义参考后文叙述的段选择子)。而GDT的位置已经由gdtr明确给出,在物理地址的0x00005cc8。用“xp /32w 0x00005cc8”查看从该地址开始,32个字的内容,及GDT表的前16项,如下:

0x00005cc8 :    0x00000000    0x00000000    0x00000fff    0x00c09a00
0x00005cd8 :    0x00000fff    0x00c09300    0x00000000    0x00000000
0x00005ce8 :    0xa4280068    0x00008901    0xa4100068    0x00008201
0x00005cf8 :    0xf2e80068    0x000089ff    0xf2d00068    0x000082ff
0x00005d08 :    0xd2e80068    0x000089ff    0xd2d00068    0x000082ff
0x00005d18 :    0x12e80068    0x000089fc    0x12d00068    0x000082fc
0x00005d28 :    0xc2e80068    0x00008bf9    0xc2d00068    0x000082f9
0x00005d38 :    0x00000000    0x00000000    0x00000000    0x00000000

GDT表中的每一项占64位(8个字节),所以我们要查找的项的地址是“0x00005cc8 + 13 8”。“xp /2w 0x00005cc8 + 13 8”,得到:

0x00005d30 :    0xc2d00068    0x000082f9

上两步看到的数值可能和这里给出的示例不一致,这是很正常的。如果想确认是否准确,就看sreg输出中,ldtr所在行里,dl和dh的值,它们是Bochs的调试器自动计算出的,你寻找到的必须和它们一致。

“0xc2d00068 0x000082f9”将其中的加粗数字组合为“0x00f9c2d0”,这就是LDT表的物理地址(为什么这么组合,参考后文介绍的段描述符)。“xp /8w 0x00f9c2d0”,得到:

0x00f9c2d0 :    0x00000000    0x00000000    0x00000002    0x10c0fa00
0x00f9c2e0 :    0x00003fff    0x10c0f300    0x00000000    0x00f9d000

这就是LDT表的前4项内容了。

  • 段描述符

在保护模式下,段寄存器有另一个名字,叫段选择子,因为它保存的信息主要是该段在段表里索引值,用这个索引值可以从段表中“选择”出相应的段描述符。

先看看ds选择子的内容,还是用“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=0x52d00068, dh=0x000082fd, valid=1
tr:s=0x0060, dl=0x52e80068, dh=0x00008bfd, valid=1
gdtr:base=0x00005cc8, limit=0x7ff
idtr:base=0x000054c8, limit=0x7ff

可以看到,ds的值是0x0017。段选择子是一个16位寄存器,它各位的含义如下图:

图2 段选择子的结构

其中RPL是请求特权级,当访问一个段时,处理器要检查RPL和CPL(放在cs的位0和位1中,用来表示当前代码的特权级),即使程序有足够的特权级(CPL)来访问一个段,但如果RPL(如放在ds中,表示请求数据段)的特权级不足,则仍然不能访问,即如果RPL的数值大于CPL(数值越大,权限越小),则用RPL的值覆盖CPL的值。而段选择子中的TI是表指示标记,如果TI=0,则表示段描述符(段的详细信息)在GDT(全局描述符表)中,即去GDT中去查;而TI=1,则去LDT(局部描述符表)中去查。

看看上面的ds,0x0017=0000000000010111(二进制),所以RPL=11,可见是在最低的特权级(因为在应用程序中执行),TI=1,表示查找LDT表,索引值为10(二进制)= 2(十进制),表示找LDT表中的第3个段描述符(从0开始编号)。

LDT和GDT的结构一样,每项占8个字节。所以第3项“0x00003fff 0x10c0f300”就是搜寻好久的ds的段描述符了。用“sreg”输出中ds所在行的dl和dh值可以验证找到的描述符是否正确。

接下来看看段描述符里面放置的是什么内容:

图3 段描述符的结构

可以看到,段描述符是一个64位二进制的数,存放了段基址和段限长等重要的数据。其中位P(Present)是段是否存在的标记;位S用来表示是系统段描述符(S=0)还是代码或数据段描述符(S=1);四位TYPE用来表示段的类型,如数据段、代码段、可读、可写等;DPL是段的权限,和CPL、RPL对应使用;位G是粒度,G=0表示段限长以位为单位,G=1表示段限长以4KB为单位;其他内容就不详细解释了。

  • 段基址和线性地址

费了很大的劲,实际上我们需要的只有段基址一项数据,即段描述符“0x00003fff 0x10c0f300”中加粗部分组合成的“0x10000000”。这就是ds段在线性地址空间中的起始地址。用同样的方法也可以算算其它段的基址,都是这个数。

段基址+段内偏移,就是线性地址了。所以ds:0x3004的线性地址就是:

0x10000000 + 0x3004 = 0x10003004

用“calc ds:0x3004”命令可以验证这个结果。

  • 页表
  • 从线性地址计算物理地址,需要查找页表。线性地址变成物理地址的过程如下:

图4 页表工作原理

线性地址变成物理地址

首先需要算出线性地址中的页目录号、页表号和页内偏移,它们分别对应了32位线性地址的10位+10位+12位,所以0x10003004的页目录号是64,页号3,页内偏移是4。

IA-32下,页目录表的位置由CR3寄存器指引。“creg”命令可以看到:

CR0=0x8000001b: PG cd nw ac wp ne ET TS em MP PE
CR2=page fault laddr=0x10002f68
CR3=0x00000000
    PCD=page-level cache disable=0
    PWT=page-level writes transparent=0
CR4=0x00000000: osxmmexcpt osfxsr pce pge mce pae pse de tsd pvi vme

说明页目录表的基址为0。看看其内容,“xp /68w 0”:

0x00000000 :    0x00001027    0x00002007    0x00003007    0x00004027
0x00000010 :    0x00000000    0x00024764    0x00000000    0x00000000
0x00000020 :    0x00000000    0x00000000    0x00000000    0x00000000
0x00000030 :    0x00000000    0x00000000    0x00000000    0x00000000
0x00000040 :    0x00ffe027    0x00000000    0x00000000    0x00000000
0x00000050 :    0x00000000    0x00000000    0x00000000    0x00000000
0x00000060 :    0x00000000    0x00000000    0x00000000    0x00000000
0x00000070 :    0x00000000    0x00000000    0x00000000    0x00000000
0x00000080 :    0x00ff3027    0x00000000    0x00000000    0x00000000
0x00000090 :    0x00000000    0x00000000    0x00000000    0x00000000
0x000000a0 :    0x00000000    0x00000000    0x00000000    0x00000000
0x000000b0 :    0x00000000    0x00000000    0x00000000    0x00ffb027
0x000000c0 :    0x00ff6027    0x00000000    0x00000000    0x00000000
0x000000d0 :    0x00000000    0x00000000    0x00000000    0x00000000
0x000000e0 :    0x00000000    0x00000000    0x00000000    0x00000000
0x000000f0 :    0x00000000    0x00000000    0x00000000    0x00ffa027
0x00000100 :    0x00faa027    0x00000000    0x00000000    0x00000000

页目录表和页表中的内容很简单,是1024个32位(正好是4K)数。这32位中前20位是物理页框号,后面是一些属性信息(其中最重要的是最后一位P)。其中第65个页目录项就是我们要找的内容,用“xp /w 0+64*4”查看:

0x00000100 :    0x00faa027

其中的027是属性,显然P=1,其他属性实验者自己分析吧。页表所在物理页框号为0x00faa,即页表在物理内存的0x00faa000位置。从该位置开始查找3号页表项,得到(xp /w 0x00faa000+3*4):

0x00faa00c :    0x00fa7067

其中067是属性,显然P=1,应该是这样。

  • 物理地址

最终结果马上就要出现了!

线性地址0x10003004对应的物理页框号为0x00fa7,和页内偏移0x004接到一起,得到0x00fa7004,这就是变量i的物理地址。可以通过两种方法验证。

第一种方法是用命令“page 0x10003004”,可以得到信息:“linear page 0x10003000 maps to physical page 0x00fa7000”。

第二种方法是用命令“xp /w 0x00fa7004”,可以看到:

0x00fa7004 :    0x12345678

这个数值确实是test.c中i的初值。

现在,通过直接修改内存来改变i的值为0,命令是: setpmem 0x00fa7004 4 0,表示从0x00fa7004地址开始的4个字节都设为0。然后再用“c”命令继续Bochs的运行,可以看到test退出了,说明i的修改成功了,此项实验结束。

  • Linux中的共享内存

Linux支持两种方式的共享内存。一种方式是shm_open()、mmap()和shm_unlink()的组合;另一种方式是shmget()、shmat()和shmdt()的组合。本实验建议使用后一种方式。

这些系统调用的详情,请查阅man及相关资料。

特别提醒:没有父子关系的进程之间进行共享内存,shmget()的第一个参数key不要用IPC_PRIVATE,否则无法共享。用什么数字可视心情而定。

  • 在Linux 0.11中实现共享内存

  • 获得空闲物理页面

实验者需要考虑如何实现页面共享。首先看一下Linux 0.11如何操作页面,如何管理进程地址空间。

在kernel/fork.c文件中有:

int copy_process(…)
{
    struct task_struct *p;
    p = (struct task_struct *) get_free_page();
    if (!p) return -EAGAIN;
    ……
}

函数get_free_page()用来获得一个空闲物理页面,在mm/memory.c文件中:

unsigned long get_free_page(void)
{
    register unsigned long __res asm("ax");
    __asm__("std ; repne ; scasb\n\t"
            "jne 1f\n\t"
            "movb $1,1(%%edi)\n\t"
            "sall $12,%%ecx\n\t"  //页面数*4KB=相对页面起始地址
            "addl %2,%%ecx\n\t"  //在加上低端的内存地址,得到的是物理起始地址
            "movl %%ecx,%%edx\n\t"
            "movl $1024,%%ecx\n\t"
            "leal 4092(%%edx),%%edi\n\t"
            "rep ; stosl\n\t"
            "movl %%edx,%%eax\n"  //edx赋给eax,eax返回了物理起始地址
            "1:" :"=a" (__res) :"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
            "D" (mem_map+PAGING_PAGES-1):"di","cx","dx"); return __res;
}

static unsigned char mem_map [ PAGING_PAGES ] = {0,};

显然get_free_page函数就是在mem_map位图中寻找值为0的项(空闲页面),该函数返回的是该页面的起始物理地址。

  • 地址映射

有了空闲的物理页面,接下来需要完成线性地址和物理页面的映射,Linux 0.11中也有这样的代码,看看mm/memory.c中的do_no_page(unsigned long address),该函数用来处理线性地址address对应的物理页面无效的情况(即缺页中断),do_no_page函数中调用一个重要的函数get_empty_page(address),其中有:

unsigned long tmp=get_free_page();
put_page(tmp, address); //建立线性地址和物理地址的映射

显然这两条语句就用来获得空闲物理页面,然后填写线性地址address对应的页目录和页表。

  • 寻找空闲的虚拟地址空间

有了空闲物理页面,也有了建立线性地址和物理页面的映射,但要完成本实验还需要能获得一段空闲的虚拟地址空闲。要从数据段中划出一段空间,首先需要了解进程数据段空间的分布,而这个分布显然是由exec系统调用决定的,所以要详细看一看exec的核心代码,do_execve(在文件fs/exec.c中)。在函数do_execve()中,修改数据段(当然是修改LDT)的地方是change_ldt,函数change_ldt实现如下:

static unsigned long change_ldt(unsigned long text_size,unsigned long * page)
{ 
    /*其中text_size是代码段长度,从可执行文件的头部取出,page为参数和环境页*/
    unsigned long code_limit,data_limit,code_base,data_base;
    int i;

    code_limit = text_size+PAGE_SIZE -1; code_limit &= 0xFFFFF000;  
    //code_limit为代码段限长=text_size对应的页数(向上取整)
    data_limit = 0x4000000; //数据段限长64MB
    code_base = get_base(current->ldt[1]); data_base = code_base;

    //数据段基址=代码段基址
    set_base(current->ldt[1],code_base); set_limit(current->ldt[1],code_limit); 
    set_base(current->ldt[2],data_base); set_limit(current->ldt[2],data_limit);
    __asm__("pushl $0x17\n\tpop %%fs":: );
    data_base += data_limit; //从数据段的末尾开始

    for (i=MAX_ARG_PAGES-1 ; i>=0 ; i--) {  //向前处理
        data_base -= PAGE_SIZE;  //一次处理一页
        if (page[i]) put_page(page[i],data_base); //建立线性地址到物理页的映射
    }
    return data_limit;  //返回段界限
}

仔细分析过函数change_ldt,想必实验者已经知道该如何从数据段中找到一页空闲的线性地址。《注释》中的图13-6也能给你很大帮助。

  • 在同一终端中同时运行两个程序

Linux的shell有后台运行程序的功能。只要在命令的最后输入一个&,命令就会进入后台运行,前台马上回到提示符,进而能运行下一个命令,例如:

# ./producer &
# ./consumer

当运行“./consumer”的时候,producer正在后台运行。


实验报告

一、线性地址到物理地址

1、从线性地址到物理地址

线性地址也叫逻辑地址,是进程中使用的相对地址。一个线性地址由32位共8个字来表示,前10位表示页目录位置,第2个10位表示页面位置,低位的12位表示页面内的偏移,在32系统中每页大小均为4K,这个可以在段描述符中的G位找到对应的表示,当G位为1时表示页大小为4K,若为0则表示页大小用字节表示。

那进程中的段偏移地址则是数据或代码在进程中的位置,如实验中用到的&x的值为0x00003004,即表示该值存储位置为从数据段的开始处算共有3004个字节,那么这3004个字节对应线性地址中的哪一部分呢?起初我一直以为是最后的偏移位置,但经过实验发现不正确,应该是低22位,即包括页位置及页内偏移两部分。如果查看线性地址的设计以及偏移值的二进制代码就明白了。32位系统下,每页大小为4K,即每页共有1024个项目,每个项目为4字节。即每个项目的索引为0-1023,展开为二进制代码,如0x00003004,二进制代码为:

0B 0011 0000 0000 0100,可以看到从第13位开始计算则刚好位于第3页,而页内偏移则为4。比对一下1023的二进制码:0B 0011 1111 1111 ,4095的二进制码:0B 1111 1111 1111,即第0页的最后一个偏移位置的后12位均为1,即一个地址在数据段内的偏移表示成22位后刚好从12位处分开,高位处为页码,低12位为偏移值。所以计算一个地址的线性地址完全不需要进行额外的运算,只需要将当前的段内偏移值加上段基址就可以了,所以实验中的&x=0x0000 3004 ,转换成线性地址就是 code_base+offset=0x1000 0000 + 0x0000 3004 = 0x1000 3004。

而一个段的基址则可以从段描述符中找到,而段描述符则保存在LDT表中,而LDT表则可以查看ldtr寄存器找到其在GDT表中的位置。当然这些值是完全可以在进程中获取的。因为task struct数据结构中保存有一个进程的这些值。

2、从线性地址寻找对应的物理地址

得到了一个变量的线性地址,要寻找其对应的物理地址,则要查找页目录表以及页表。在linux0.11中,页目录表只有一个其保存在物理内存0处,即在系统初始化时被初始化的pgr变量地址。而当前运行的进程的LDT表、GDT表以及全局页目录表的位置均保存在相应的寄存器中,其中全局页目录表的位置保存在cr3寄存器中,所在在本次实验中,通过调试的方法来寻找,可以就可以通过查看这些寄存器值得到。通过creg命令得到。这样就可以得到数据段、LDT表的描述符,以及GDT表的基址和全局页目录表的地址。进一步就可以得到相应线性地址的物理地址。

实验步骤

实验用代码


#include 
int i=0x12345678;
int main(void)
{
    printf("The logical/Virtual address of i is 0x%08x\n",&i);
    fflush(stdout);
    while(i)
    ;

    return 0;
}
  1. 通过sreg寄存器找到数据段的选择符为0x0017,LDT的选择符为0x0068,而gdt的基址为0x0000 5cb8。
  2. 0x0017的二进制码0B 0000 0000 0001 0111 可知,该段选择符保存在LDT表中,其为用户态权限,保存位置为LDT表中的第2项。 所以要查看LDT表在全局表中的位置索引, 由0x0068的二进制码0B 0000 0000 0110 1000可知,该LDT的描述符保存在GDT表中,其位置为第13项。所以要查看GDT表中第13项内容,而DGT表保存的物理地址为0x0000 5cb8,通过命令:xp /12w 0x0000 5cb8 + 13 8(每个描述符占8字节),可以得到LDT的描述符为:0x52d8 0068 0x0000 82fd,即LDT的基址为:0x00fd 52d8。 通过命令: xp /12w 0x00fd 52d8+2 8,可以得到当前数据段的描述符为:0x00003fff 0x10c0 f300,其基址为:0x1000 0000。故变量i的线性地址为:&i=0x1000 0000 + 0x0000 3004 = 0x1000 3004。展开其二进制码为:0B 0001 0000 00 00 0000 0011 0000 0000 0100,得到变量i的线性地址为第64个目录项所指的页表中第3项,其页内偏移为4。
  3. 通过creg寄存器找到全局页目录物理地址为:0x0000 0000(在linux 0.11 中全局页目录总是保存在内存0处)。故通过命令:xp /12w 0x0000 0000 +64*4(页表中每项占4字节),可以得知其值为:0x00fa7027,而低3位表示项的属性,因为所有内存页框地址均4K对齐,所以低12位均为0。故得知其页面所在物理地址为:0xfa7000。
  4. 通过命令:xp /12w 0x00fa 7000 +2*4,得到0x00fa 6027,故数据段对应页框地址为:0x00fa 6000。通过命令:xp /4w 0x00fa 6000+4,看到其内容正是0x12345678,即变量i的值。到此,找到了变量i对应的物理地址为:0x00fa6004。
  5. 通过命令:setpmem 0x00fa 6004 4 0,将其值修改为0。通过命令c让系统继续,看到进程已经正常退出。

实验过程截图

完成实验后,在实验报告中回答如下问题:

对于地址映射实验部分,列出你认为最重要的那几步(不超过4步),并给出你获得的实验数据。

答:第一步为找到需要的各个选择符,DS:S=0X0017,LDT:s=0x0068,GDT:BASE=0X0000 5CB8。第二步为找到数据段的基址。实验中进程的NR为4,即第5个进程,所以按照LINUX 0.11的设计,其基址应为 4*64M,转换为16进制即为0x1000 0000。实验结果也验证了这一点。第三步根据找到的线性地址,通过查看页目录表就可以找到对应页表的物理地地址。实验中页表的物理地址为0x00fa 70000。 第四步根据找到的页表所对就应的物理地址就可以找到变量所在页的物理地址。实验中变量所在页的物理地址为0x00fa6000。这里有个很有趣的现象,页表的物理地址比具体一页的物理地址高。而页表一定是先分配的内存,这也说明linux 0.11中物理内存的分配是从高到低进行的。这一点通过查看get_free_page函数得到印证。

test.c退出后,如果马上再运行一次,并再进行地址跟踪,你发现有哪些异同?为什么?

答:若马上再运行一次,则可以看到得到的线性地址为0x1400 3004,即进程的数据段基地多了0x0400 0000。就是多了一个64M。这也正是linux 0.11中对进程线性地址分配的方法即:nr*64M。因为马上运行此时进程4并没有被删除,而只是状态个性为3,所以再运行的进程正好是5。所以其分配的线性地址基址为0x1400 0000。

共享内存实现消费者程序

在UBUNTU下运行共享内存的实验代码


//producer.c

#include 
#include 
#include 
#include 
#include    
#include    
#include    
#include    
#include    
#include     

#define BUFFERSIZE    10
struct int_buffer
{
    int int_count;
    int head;
    int tail;
    int data[BUFFERSIZE];
};

sem_t *mutex; // = sem_open("mutex",O_CREAT | O_EXCL,0644,BUFFERSIZE);

int main(void)
{
    char err_desc[255];
    char mutex_sem[]= "mutex";
    int itemValue = -1;

    key_t key;
    struct int_buffer * logbuf;
    int shmid_main;

    key = ftok("./tmp.txt",0x03);
    shmid_main=shmget(key,sizeof(struct int_buffer),IPC_CREAT|0666);
    if(shmid_main == -1)
    {
        printf("shmget(1234,..) error!\n");
        perror(err_desc);
        return -1;
    }

    mutex = sem_open(mutex_sem,O_CREAT,0644,1);
    if(mutex == SEM_FAILED)
    {
        printf("create semaphore mutex error!\n");
        return 1;
    }



    logbuf=(struct int_buffer *)shmat(shmid_main,NULL,0);
    if((long)logbuf==-1)
    {
        printf("in producer shmat(shmid_main,NULL,0) error!\n");
        perror(err_desc);
        exit(-1);
    }

    while(itemValue<499)
    {
        itemValue++;

        //printf("producer(%u) int_count=>%d  itemValue=> %d\n",getpid(),logbuf->int_count,itemValue);

        //
        while(logbuf->int_count==10)
            ;
        if(sem_wait(mutex)!=0)
        {
            printf("in producer sem_wait(mutex) error!\n");
            perror(err_desc);
            break;
        }
        //
        logbuf->int_count+=1;
        logbuf->data[logbuf->head]=itemValue;
        (logbuf->head)++;
        if(logbuf->head>=BUFFERSIZE)
        {
            logbuf->head=0;
        }


        if(sem_post(mutex)!=0)
        {
            printf("in producer sem_post(mutex) error!\n");
            perror(err_desc);
            break;
        }


    }

    //
    if(shmdt(logbuf)!=0)
    {
        printf("in producer shmdt(logbuf) error!\n");
        perror(err_desc);
    }

    return 0;
}


//consumer.c

#include 
#include 
#include 
#include 
#include    
#include    
#include    
#include    
#include    

#define BUFFERSIZE    10
struct int_buffer
{
    int int_count;
    int head;
    int tail;
    int data[BUFFERSIZE];
};

sem_t *mutex; // = sem_open("mutex",O_CREAT | O_EXCL,0644,BUFFERSIZE);

int main(void)
{
    int get_count=0;

    char err_desc[255];
    char mutex_sem[]= "mutex";
    int itemValue = -1;

    int shmid_main;
    key_t key;    
    struct int_buffer * logbuf;
    key = ftok("./tmp.txt",0x03);

    shmid_main=shmget(key,sizeof(struct int_buffer),IPC_CREAT|0666);
    if(shmid_main == -1)
    {
        printf("shmget(1234,..) error!\n");
        perror(err_desc);
        return -1;
    }


    mutex = sem_open(mutex_sem,O_CREAT,0644,1);
    if(mutex == SEM_FAILED)
    {
        printf("create semaphore mutex error!\n");
        return 1;
    }


    //
    logbuf=(struct int_buffer *)shmat(shmid_main,NULL,0);
    if((long)logbuf==-1)
    {
        printf("in producer shmat(shmid_main,NULL,0) error!\n");
        perror(err_desc);
        exit(-1);
    }

    while(get_count<500)
    {
        //
        while(logbuf->int_count<=0)
            ;


        if(sem_wait(mutex)!=0)
        {
            printf("in customer %u,sem_post(empty) error!",getpid());
            perror(err_desc);
            break;
        }


        itemValue=logbuf->data[logbuf->tail];
        logbuf->int_count--;
        (logbuf->tail)++;
        if(logbuf->tail>=BUFFERSIZE)
        {
            logbuf->tail=0;
        }
        printf("%u:%d\n",getpid(),itemValue);
        get_count++;


        if(sem_post(mutex)!=0)
        {
            printf("in customer %u,sem_post(empty) error!\n",getpid());
            perror(err_desc);
            break;
        }

    }

    //detach the shared memory
    if(shmdt(logbuf)!=0)
    {
        printf("in customer shmdt(logbuf) error!\n");
        perror(err_desc);
    }
    //delete the shared memory
    if(shmctl(shmid_main,IPC_RMID,0)==-1)
    {
        printf("in customer shmctl(shmid,IPC_RMID,0) error!\n");
        perror(err_desc);
    }

    sem_unlink("mutex");

    return 0;
}

运行结果截图

实验的补充说明

由于此次实验的进程仅有两个,所以在实现中不使用信号量进行同步也完全没有问题。去掉进程同步代码结果也完全相同。

在linux 0.11下实现共享内存

此次在LINUX 0.11下实现共享内存并没有太多的困难。当然如果要实现符合POSXI标准的代码就要困难得多。查看现在的LINUX 2.6 内核中相关代码时发现其使用的是文件与内存相关联的方法来实现的。此次仅是使用最简单的办法来实现的。本次实验共实现了4个函数,shmget(),shmat(),shmdt()以及shmctl()其中shmctl函数仅实现了删除共享内存的代码。具体代码见下:


//unistd.h 中修改的部分

/*melon*/
#define    NR_SHM    64

struct shmid_ds
{
    int key;
    int size;
    unsigned long page;
    int attached;
};
/*melon 2015-7-16*/


//shm.c

#define __LIBRARY__
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define LOW_MEM 0x100000

static struct shmid_ds shm_list[NR_SHM]={{0,0,0,0}};
extern void add_mem_count(long addr);
extern void remove_page(long addr);

int sys_shmget(int key,int size,int shmflag)
{

    int i;
    int return_key=-1;
    unsigned long page;


    if(size>PAGE_SIZE)
    {
        return -EINVAL;
    }


    for(i=0;iif(shm_list[i].key==key)
        {
            return i;
        }
    }


    for(i=0;iif(shm_list[i].key==0)
        {

            return_key=i;
            break;
        }
    }

//_get_free_page() 是页面分配器提供给调用者的最底层的内存分配函数。它分配连续的物理内存。返回的是物理内存地址。
    page=get_free_page();
    printk("shmget get memory's address is 0x%08x\n",page);
    if(page==0)
    {
        return -ENOMEM;
    }


    shm_list[return_key].key=key;
    shm_list[return_key].size=size;
    shm_list[return_key].page=page;
    shm_list[return_key].attached=0;


    return return_key;
}


void * sys_shmat(int shmid,const void *shmaddr, int shmflag)
{
    unsigned long data_base;
    unsigned long brk;
    unsigned long page;


    if(shm_list[shmid].key<=0 || shm_list[shmid].page==0)
    {
        return (void*)-EINVAL;
    }
//get_base函数的作用就是取出段选择符指向的段描述符中的段基址。
    data_base=get_base(current->ldt[2]);
    printk("current's data_base = 0x%08x,new page = 0x%08x\n",data_base,shm_list[shmid].page);

    brk=current->brk+data_base;
    current->brk +=PAGE_SIZE;

/ put_page用来完成物理页面与一个线性地址页面的挂接,从而将一个
// 线性地址空间内的页面落实到物理地址空间内,copy_page_tables函数
// 只是为一个进程提供了在线性地址空间的一个页表及1024页内存,而当时
// 并未将其对应的物理内存上。put_page函数则负责为copy_page_tables开
// 的“空头支票”买单。
// page为物理地址,address为线性地址
//unsigned long put_page(unsigned long page,unsigned long address)
page=put_page(shm_list[shmid].page,brk); if(page==0) { return (void*)-ENOMEM; } //add_mem_count(page); shm_list[shmid].attached++; //printk("current->brk=0x%08x,shmat return 0x%08x,put_page return 0x%08x\n",current-brk,brk,page); return (void *)(brk - data_base); } int sys_shmdt(int shmid) { unsigned long data_base; if(shm_list[shmid].key<=0 && shm_list[shmid].page==0) { return -EINVAL; } shm_list[shmid].attached--; data_base=get_base(current->ldt[2]); //取数据段基址 current->brk-=PAGE_SIZE; remove_page(data_base+current->brk); //将线性地址从页面中去除 return 0; } int sys_shmctl(int shmid,int shmcmd, struct shmid_ds * buf) { int ret=0; switch(shmcmd) { case 0: if(shm_list[shmid].attached>=0 && shm_list[shmid].page!=0) { free_page(shm_list[shmid].page); } else { ret=-EINVAL; } break; } return ret; }

在LINUX 0.11下使用的生产者及消费者程序代码


//producer11.c

#define __LIBRARY__
#include    
#include    
#include    
#include    
#include    
#include    
#define BUFFERSIZE    10

static _syscall2(sem_t *,sem_open,const char *,name,int,value);
static _syscall1(int,sem_post,sem_t *,sem);
static _syscall1(int,sem_wait,sem_t *,sem);
static _syscall1(int,sem_getvalue,sem_t *,sem);
static _syscall1(int,sem_unlink,const char*,name);

static _syscall3(int,shmget,int,key,int,size,int,shmflag);
static _syscall3(void *,shmat,int,shmid,const void *,shmaddr,int,shmflag);
static _syscall1(int,shmdt,int,shmid);
static _syscall3(int,shmctl,int,shmid,int,shmcmd,struct shmid_ds *,buf);

struct int_buffer
{
    int int_count;
    int head;
    int tail;
    int data[BUFFERSIZE];
};

sem_t *mutex;

int main(void)
{
    char err_desc[255];
    int itemValue = -1;
    char mutex_sem[]= "mutex";
    struct int_buffer * logbuf;
    int shmid_main;
    int mutex_value;

    mutex = sem_open(mutex_sem,1);
    if(mutex == NULL)
    {
        printf("create semaphore mutex error!\n");
        return 1;
    }

    mutex_value=sem_getvalue(mutex);
    if(mutex_value==-1)
    {
        printf("sem_getvalue(mutex) error\n");
        perror(err_desc);
        return 1;
    }
    else
    {
        printf("producer:mutext's value=%d\n",mutex_value);
    }

    shmid_main=shmget(1234,sizeof(struct int_buffer),0);
    if(shmid_main == -1)
    {
        printf("shmget(1234,..) error!\n");
        perror(err_desc);
        return -1;
    }

    logbuf=(struct int_buffer *)shmat(shmid_main,NULL,0);

    if((long)logbuf==-1)
    {
        printf("in producer shmat(shmid_main,NULL,0) error!\n");
        perror(err_desc);
        exit(-1);
    }

    while(itemValue<499)
    {
        itemValue++;


        while(logbuf->int_count==10)
            ;

        if(sem_wait(mutex)!=0)
        {
            printf("in customer %u,sem_post(empty) error!",getpid());
            perror(err_desc);
            break;
        }

        logbuf->int_count++;

        if(sem_post(mutex)!=0)
        {
            printf("in customer %u,sem_post(empty) error!\n",getpid());
            perror(err_desc);
            break;
        }

        logbuf->data[logbuf->head]=itemValue;
        (logbuf->head)++;
        if(logbuf->head>=BUFFERSIZE)
        {
            logbuf->head=0;
        }

    }


    if(shmdt(shmid_main)!=0)
    {
        printf("in producer shmdt(logbuf) error!\n");
        perror(err_desc);
    }

    return 0;
}


//consumer11.c

#define __LIBRARY__
#include    
#include    
#include    
#include    
#include    
#include    
#define BUFFERSIZE    10

static _syscall2(sem_t *,sem_open,const char *,name,int,value);
static _syscall1(int,sem_post,sem_t *,sem);
static _syscall1(int,sem_wait,sem_t *,sem);
static _syscall1(int,sem_getvalue,sem_t *,sem);
static _syscall1(int,sem_unlink,const char*,name);

static _syscall3(int,shmget,int,key,int,size,int,shmflag);
static _syscall3(void *,shmat,int,shmid,const void *,shmaddr,int,shmflag);
static _syscall1(int,shmdt,int,shmid);
static _syscall3(int,shmctl,int,shmid,int,shmcmd,struct shmid_ds *,buf);

struct int_buffer
{
    int int_count;
    int head;
    int tail;
    int data[BUFFERSIZE];
};

sem_t *mutex;

int main(void)
{
    int get_count=0;

    char err_desc[255];
    char mutex_sem[]= "mutex";
    int itemValue = -1;
    int shmid_main;
    struct int_buffer * logbuf;
    int log = open("pclog.log", O_CREAT|O_TRUNC|O_RDWR, 0666);
    char buflog[255];

    int mutex_value;

    shmid_main=shmget(1234,sizeof(struct int_buffer),0);
    if(shmid_main == -1)
    {
        printf("shmget(1234,..) error!\n");
        perror(err_desc);
        return -1;
    }


    mutex = sem_open(mutex_sem,1);
    if(mutex == NULL)
    {
        printf("create semaphore mutex error!\n");
        return 1;
    }

    mutex_value=sem_getvalue(mutex);
    if(mutex_value==-1)
    {
        printf("sem_getvalue(mutex) error\n");
        perror(err_desc);
        return 1;
    }
    else
    {
        printf("producer:mutext's value=%d\n",mutex_value);
    }


    logbuf=(struct int_buffer *)shmat(shmid_main,NULL,0);
    if((long)logbuf==-1)
    {
        printf("in producer shmat(shmid_main,NULL,0) error!\n");
        perror(err_desc);
        exit(-1);
    }

    while(get_count<500)
    {


        while(logbuf->int_count<=0)
            ;


        itemValue=logbuf->data[logbuf->tail];

        if(sem_wait(mutex)!=0)
        {
            printf("in customer %u,sem_post(empty) error!",getpid());
            perror(err_desc);
            break;
        }

        logbuf->int_count--;

        if(sem_post(mutex)!=0)
        {
            printf("in customer %u,sem_post(empty) error!\n",getpid());
            perror(err_desc);
            break;
        }

        (logbuf->tail)++;
        if(logbuf->tail>=BUFFERSIZE)
        {
            logbuf->tail=0;
        }

        lseek(log,0,SEEK_END);
        sprintf(buflog,"%u:%d\n",getpid(),itemValue);
        write(log,&buflog,sizeof(char)*strlen(buflog));

        get_count++;



    }

    close(log);

    if(shmdt(shmid_main)!=0)
    {
        printf("in customer shmdt(logbuf) error!\n");
        perror(err_desc);
    }

    if(shmctl(shmid_main,0,0)==-1)
    {
        printf("in customer shmctl(shmid,IPC_RMID,0) error!\n");
        perror(err_desc);
    }

    sem_unlink("mutex");

    return 0;
}

运行结果截图

关于shmdt及shmctl函数的补充

由于实验指导中没有要求实现shmdt以入shmctl函数,所以关于这两个函数问题没有打算写入报告中,但考虑到代码中有关于这两个函数的内容,而且本人以为这两个函数的实现在难度上要稍高于另外两个,所以还是补充在这里。

开始时shmdt函数的实现只是简单地从共享内存的结构中将计数器减1,以及从将进程的数据段长度修改回最初,但在运行中却发现,两个进程使用同一段共享内存时其中数据却是随机的。而且有时还会提示释放一段已释放的内存的信息(说明:在最初测试时使用两进程都没有对共享内存进行删除)。可以想象当时是如何意外,也很蒙的。随后仔细想了想,主要是考虑到系统提示说释放一段已释放的内存。那一定是对共享内存进行了删除操作的。而测试进程是一定没有释放的,那也就是说系统自动在释放这段内存。会在哪里呢?后来想到第二个进程运行时第一个进程已经结束,那一定是系统在删除结束进程时对进程所持有的物理内存进行了释放。那就得想办法让系统在结束一个僵尸进程时不释放指定内存,但看了看内存管理的全局结构,似乎不可行。后来就决定在shmat同时将mem_map对应数据增1,因为记得内存的低1M内存在mem_map中其值都是USED(=100),所以在其中加入了一个增加mem_map[]计数的函数,只是简单在将其增1。以为这样在进程结束被系统回收时其值只会减1,而不会真正被释放掉。但一运行才发现根本不可行,因为在用户态的put_page中要求mem_map只能为1,不能为其他值。也就是说LINUX 0.11不允许用户态内存共享。当然这里也可以简单地将其限制修改掉,但不能保证其他部分会不会有涉及此值的验证或相关代码,所以不能在这里修改。也就是说共享的这一页内存在mem_map中其值只能为1。那就只能在其他部分想办法。最的决定在shmdt时按照put_page的反操作将对应页表中的值去除。本以为有现在函数可用,找了好久也没找到,最后只能在memory.c中手动增加一个remove_page的函数来实现这个目的。具体代码见下面。


void remove_page(long addr)
{
    unsigned long * page_table;

    page_table=(unsigned long*)((addr>>20)&0xffc); //取目录地址
    if((*page_table) & 1) //如果对应页面在内存中存在,则取出页地址
        page_table=(unsigned long *)(0xfffff000 & *page_table);

    page_table[(addr>>12)&0x3ff]=0; //去掉页中存在的内存地址

}


你可能感兴趣的:(linux内核(操作系统))