Linux0.11地址映射与共享(HIT OS实验六)

Linux0.11地址映射与共享 HIT OS实验六

这次记录一下完成实验六的全过程,主要是要了解Linux0.11中的内存管理机制,并实现一个基于共享物理地址内存的生产者消费者问题。

照例先说一下实验内容:

实验内容

实验内容:

  1. 用Bochs调试工具跟踪Linux 0.11的地址翻译(地址映射)过程,了解IA-32(Intel Architecture 32-bit)的CPU架构下的地址翻译和Linux 0.11的内存管理机制。
    在Ubuntu上编写多进程的生产者-消费者程序,用共享内存做缓冲区(上一个实验是用文件做缓冲区)
  2. 在上一个实验(信号量的实现和在pc.c程序上的应用)的基础上,为Linux 0.11增加共享内存功能,并将生产者-消费者程序移植到Linux 0.11。

整个实验做起来其实不算难,但是需要真正的了解LInux0.11中的内存管理机制,最起码要了解对于一个逻辑地址(虚拟地址)如何转换成内存中的物理地址。

实验原理

按照惯例,先讲解基础原理,再讲解实验实现原理。

1.逻辑地址,虚拟地址,线性地址,物理地址

很多人做这个实验,连逻辑地址,虚拟地址,线性地址,物理地址都不知道是什么。我看了很多的blog,连这种基本的概念都讲错。

在 Intel 平台下,逻辑地址 logical address selector:offset 这种形式,selector 是 CS 寄存器的值,offset 是 EIP 寄存器的值。

如果用 selector 去 GDT( 全局描述符表 ) 里拿到 segment base address(段基址) 然后加上 offset(段内偏移),这就得到了 线性地址 linear address。我们把这个过程称作段式内存管理。

如果再把 linear address 切成四段,用前三段分别作为索引去PGD、PMD、Page Table里查表,最终就会得到一个页表项(Page Table Entry),那里面的值就是一页物理内存的起始地址,把它加上 linear address 切分之后第四段的内容(又叫页内偏移)就得到了最终的 物理地址 physical address。我们把这个过程称作页式内存管理。

对于虚拟地址,其实在 Intel IA-32 手册里并没有提到这个术语,因此对于虚拟地址的定义往往比较模糊。一般情况下,默认虚拟地址与逻辑地址表达的同一个意思,有些人也会将进程中打印出来的地址结果作为虚拟地址,认为虚拟地址是逻辑地址中的offset。在李老师的课程里,则是将虚拟地址与虚拟内存放在了一起讲,认为虚拟地址是逻辑地址在发生分段后的地址,即线性地址。为了避免歧义,本文都默认虚拟地址=逻辑地址,尽可能避免使用虚拟地址这个概念。

2.地址映射过程

逻辑地址到物理地址的转换应当分为如下几步:逻辑地址(虚拟地址 -> 线性地址 →物理地址)(若不开启分页,则线性地址等于物理地址)相当于实模式下的地址访问。(若开启分页,则按照分页规则进行地址转换)

大致过程可以参照下图:
在这里插入图片描述
在这里插入图片描述
大致过程如下:

  1. 逻辑地址由 ds:offset构成,ds(段选择子)可以理解为段索引(段选择符),根据索引 index 和gdt/ldt(全局为gdt表,进程中则是ldt)的基址base,可以确定段描述符(Segment Description)的位置。
    在这里插入图片描述具体计算为:addr.segment_description = base + index*(len(segment_description))
    此时可以获得段描述符的值,段描述符格式如下:
    在这里插入图片描述
    因此可以根据段描述符的格式获得线性地址空间基地址:

                    linerbase = addr(addr.segment_description.value)
    

    那么此时就完成了逻辑地址(虚拟地址)到线性地址的转换,线性地址为 linerbase+offset

  2. 此时我们已经有了线性地址 linerbase+offset,接着计算物理地址
    如果操作系统没有开启分页功能,那么此时的线性地址就是实际的物理地址(可以参照实模式分段后的地址翻译)
    当开启分段时,线性地址遵循如下翻译规则:
    在这里插入图片描述将liner_base+offset 的前10位作为页目录表的查找索引index1,中间10位作为页表1的索引index2,后面的12位为找到的最终页表的偏移值 offset_new。
            liner_base+offset = index1:index2:offset_new
            base_1 = addr(base_0[index1])
    base_0为页目录表基,base_1为页表1的基地址,addr函数则是根据base_0[index1]中的页目录表项从而获取到的基地址,根据页目录表项规则就是base_0[index1]的 位12到位31)
    在这里插入图片描述
            base_2 = addr(base_1[index2]) (base_2为第二级页表基地址)
    此时就可以得到开启分页后的最终物理地址: base_2:offset_new

3.地址翻译实例

这个参考实验手册全部过一遍即可,这样就能够对地址翻译有一个大致的了解。参考如下两个图会有一个比较好的理解:

在这里插入图片描述
在这里插入图片描述

2.实验实现

  1. shmget()与shmat()的实现
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 参数可忽略。

对于shmget相对来说比较简单,实验手册中也大致讲过如何实现。首先在全局变量中需要有一个shm_list表用来维护所有创建
的shm;若是shm_list中存在shm.key为key的,则直接返回他在list中的索引,若是没有则找到一个还未被初始化的索引初始化一个shm即可。利用 get_free_page可以初始化一个页表。

对于shmat则有点复杂,他是要求将进程中的shmaddr与shmid中的物理地址形成映射关系,即操作系统在对逻辑地址shmaddr进行翻译时,最终的翻译结果是物理地址shmid.page。
在实验手册中有提示,可以利用如下函数实现一个从线性地址到物理地址的映射:

put_page(tmp, address);

因此,若是我们拥有一个线性地址tmp,我们就可以使用 put_page(tmp.shmid->page)实现线性地址到物理地址的映射。
接下来的问题,就来到了对于一个进程的虚拟地址(也可以叫逻辑地址),如何实现逻辑地址到线性地址的映射。
这里实验手册也给了提示,参考如下代码:

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);
    }

可以看出对 set_base(current->ldt[2],data_base);就是在设置LDT(2)中的基地址,即代码段的线性基地址。还记得线性地址是怎么翻译的嘛,对于一个代码段来说:
线性地址addr = 线性基地址data_base +offset,而这里的offset就是代码的逻辑地址。举个例子,对于之前的程序代码翻译示例:
在这里插入图片描述
这个程序打印出来的地址就是我们的偏移地址 offset = 0x3004
而线性基地址 data_base则是下图中 在索引值为2时,根据段描述符0x00003fff 10c0f300得到的基地址 addr(0x00003fff 0x10c0f300) = 0x10000000.
而 0x10000000.+0x00003004则是得到了我们的线性地址:0x 1000 3004

在这里插入图片描述
由此我们可以得到一个启发:


int i;
liner_address = logic_address+liner_base = &i + data_base;

当然我们也可以不使用&i,而是使用进程程序中的一段固定内存作为我们形成映射的逻辑地址。

要在进程的虚拟内存区域划分出一段空间, 首先需要了解进程虚拟空间的分布,阅读源码(主要是exec.c)可以发现每个进程在虚拟内存空间会分配64MB的大小,分别有代码段、数据段、堆段(用来存放程序中未初始化的全局变量的一个段)、栈段,从图(待制作)看出,堆段到栈段的虚拟子内存空间是没有被使用的,我们可以在这里分割出一个页表来建立起到物理内存的映射关系。

这些段的信息都是存储在进程PCB中的,所以可以用current->brk找到从进程开始处到brk位置的长度,当然这还不是brk所在的虚拟地址,只是离分配给该进程的64MB虚拟内存开始处的偏移地址,current->brk再加上该进程开始处的虚拟地址才是我们想要的虚拟地址,当前进程开始处的虚拟地址存放在current->ldt[1]中,可以用get_base(current->ldt[1])获取,所以经过

tmp = get_base(current->ldt[1]) + current->brk;//当前进程的线性基地址地址 + brk的逻辑地址 = brk的线性地址
put_page(tmp,shm.page); //实现线性地址到物理地址的映射

在这里插入图片描述
那么就可以完成逻辑地址(虚拟地址)到物理地址的映射。这样的对于每一个进程(生产者,消费者)都可以使自己的brk映射到同一个物理地址中去,这样就实现了一个共享内存页。实现代码如下:

#define __LIBRARY__
#include <unistd.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/mm.h>
#include <errno.h>
#include <linux/shm.h>


static shm_ds shm_list[SHM_SIZE] = {{0, 0, 0}};/* unsigned int key,size,page */

int sys_shmget(unsigned int key, size_t size)
{
    int i;
    if(size>PAGE_SIZE){
        printk("shmget: size %u cannot be greater than the page size %ud. \n", size, PAGE_SIZE);
        return -ENOMEM;
    }

    if (key == 0)
    {
        printk("shmget: key cannot be 0.\n");
        return -EINVAL;
    }

    /* find the matched key  */
    for(i = 0;i<SHM_SIZE;i++){
        if(shm_list[i].key == key){
            return i;
        }
    }

    unsigned long page = get_free_page();
    if(!page){
        return -ENOMEM;
    }
    printk("shmget get memory's address is 0x%08x\n", page);
    for(i = 0;i<SHM_SIZE;i++){
        if(shm_list[i].key == 0){
            shm_list[i].key = key;
            shm_list[i].size = size;
            shm_list[i].page = page;
            return i;
        }
    }

    return -1;
}

void *sys_shmat(int shmid)
{

    unsigned long data_base,brk;
    if (shmid < 0 || SHM_SIZE <= shmid || shm_list[shmid].page == 0 || shm_list[shmid].key <= 0)
    {
        printk("In shmat: wrong shmid or wrong shmid!");
        return (void *)-EINVAL;
    }
    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;
    if (put_page(shm_list[shmid].page, brk) == 0)
    {
        return (void *)-ENOMEM;
    }

    return (void*)(current->brk-PAGE_SIZE);
}

  1. 接下来是如何使用共享内存实现消费者与生产者问题:
    如果完成过上个实验,那么这个实验也同理。只是共享内存变成shmat返回的指针标记的数组,对数字进行遍历写入与读取即可。
    producer.c
/*producer.c*/
#define   __LIBRARY__
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <linux/sem.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);
_syscall1(void*,shmat,int,shmid);
_syscall2(int,shmget,int,key,int,size);



#define NUMBER 520 /*打出数字总数*/
#define BUFSIZE 10 /*缓冲区大小*/

sem_t   *empty, *full, *mutex;

int main(){
    int buf_in = 0;
    int i,shmid;
    int *p;

    empty = sem_open("empty", 10);
    if (empty == NULL)
    {
        perror("empty create falied!\n");
        return -1;
    }
    full = sem_open("full", 0);
    if (full == NULL)
    {
        perror("full create failed!\n");
        return -1;
    }
    mutex = sem_open("mutex", 1);
    if (mutex == NULL)
    {
        perror("mutex create failed!\n");
        return -1;
    }
    if (empty && full && mutex)
    {
        printf("create semphore successed!\n");
    }

    
    shmid = shmget(1,BUFSIZE);
    printf("shmid:%d\n",shmid);
    p = (int*)shmat(shmid);

    if(shmid == -1)
    {
        return -1;
    }

    printf("producer start:\n");

    for(i = 0;i<NUMBER;i++){
        printf("p-debug_i:%d\n",i);
        sem_wait(empty);
        sem_wait(mutex);
        p[buf_in] = i;
        buf_in = (buf_in+1)%BUFSIZE;
        sem_post(mutex);
        sem_post(full);
        
    }
    printf("producer end.\n");
	fflush(stdout);
    /*释放信号量*/
    sem_unlink("full");
    sem_unlink("empty");
    sem_unlink("mutex");
    return 0;

}

consumer.c

/*comsumer.c*/
#define   __LIBRARY__
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <linux/sem.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);
_syscall1(void*,shmat,int,shmid);
_syscall2(int,shmget,int,key,int,size);


#define NUMBER 520 
#define BUFSIZE 10 

sem_t   *empty, *full, *mutex;


int main(){
    int buf_out = 0;
    int i,shmid;
    int *p;
    int data;

    empty = sem_open("empty", 10);
    if (empty == NULL)
    {
        perror("empty create falied!\n");
        return -1;
    }
    full = sem_open("full", 0);
    if (full == NULL)
    {
        perror("full create failed!\n");
        return -1;
    }
    mutex = sem_open("mutex", 1);
    if (mutex == NULL)
    {
        perror("mutex create failed!\n");
        return -1;
    }
    if (empty && full && mutex)
    {
        printf("create semphore successed!\n");
    }

    shmid = shmget(1,BUFSIZE);
    printf("shmid:%d\n",shmid);

    if(shmid == -1)
    {
        return -1;
    }

    p = (int*)shmat(shmid);
    printf("comsumer start:\n");

    for(i = 0;i<NUMBER;i++){
        /*printf("c-debug->i:%d\n",i);*/
        
        sem_wait(full);
        sem_wait(mutex);

        data = p[buf_out];
        buf_out = (buf_out+1)%BUFSIZE;
        printf("pid: %d  data: %d\n",getpid(),data);

        sem_post(mutex);
        sem_post(empty);
        
    }
    printf("comsumer end.\n");
	fflush(stdout);
    /*释放信号量*/
    sem_unlink("full");
    sem_unlink("empty");
    sem_unlink("mutex");
    return 0;

}

这样的话,整个实验就完成了。可以看出由于历史遗留问题,linux0.11的内存管理及其复杂,采用了段页式的内存管理方法,整体想要理清楚还是有点困难。其实对于每个进程创建时页分配以及写时复制等各种各样乱七八糟的机制与问题,在本实验中还没有被提及。想要彻底搞懂内存管理还有很长的路要走。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值