HIT Linux-0.11 实验七 地址映射与内存共享 实验报告

进行本次实验前需要先完成 实验六 信号量的实现与运用。
实验要求与实验指导见 实验楼。
实验环境为 配置本地实验环境。

一、实验目标

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

二、实验内容和结果

(一). 跟踪地址翻译过程

  这节实验的目的是用 Bochs 的调试功能获取变量的虚拟地址映射的物理地址。

  在 Linux-0.11 中运行下面的死循环程序:

#include 

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

  在上述程序的运行过程中,在命令行窗口按下 Ctrl+C,进入 Bochs 的调试功能,使用 u /8命令获取反汇编代码,如图:

HIT Linux-0.11 实验七 地址映射与内存共享 实验报告_第1张图片

可知 i 的虚拟地址为 ds:0x3004

  使用 sreg命令查看段寄存器的值如图:

HIT Linux-0.11 实验七 地址映射与内存共享 实验报告_第2张图片

  LDTR 的值为 0x0068 = 0000000001101000。根据段选择符的结构,最后两位表示请求特权级 RPL = 00,倒数第三位为表指示标志 TI - 0,表示该选择符存放在 GDT 中,前面的 13 位为索引值 Index = 1101 (二进制) = 13 (十进制),表示该进程的 LDT 表存放在 GDT 表中的 13 号位置。

  GDTR 的值为 0x00005cb8,于是目的 LDT 表的地址为 0x00005cb8+13*8:

在这里插入图片描述

  所得结果为 0x52d00068 0x000082fd,根据段描述符的格式:

HIT Linux-0.11 实验七 地址映射与内存共享 实验报告_第3张图片

这里将上述结果组合为 0x00fd52d0,即为 LDT 表的物理地址。查看 LDT 表的内容如图所示:

在这里插入图片描述

  Linux-0.11 中,LDT 表的第一项为空,第二项为代码段,第三项为数据和堆栈段 ds&ss。于是所需的 ds 的段描述符为 0x00003fff 0x10c0f300,组合为 0x10000000,即为 ds 段的起始地址。

  于是 i 即 ds:0x3004的线性地址为 0x10003004

  接下来通过页表将线性地址映射到物理地址,规则如下:

HIT Linux-0.11 实验七 地址映射与内存共享 实验报告_第4张图片

  首先计算线性地址的页目录号为 64,页表号为 3,页内偏移为 4。

  页目录表的物理地址保存在 CR3 寄存器中,使用 creg命令获取其值:

HIT Linux-0.11 实验七 地址映射与内存共享 实验报告_第5张图片

  可知页目录表的基址为 0,这是 Linux-0.11 启动时 boot/head.s设置的。目的页目录号为 64,查看该目录项:

在这里插入图片描述

  页表项中的 12-31 位为页帧地址(见《Linux内核完全注释》P99),故目的页帧号为 0x00fa6。接着查 3 号表项:

在这里插入图片描述

  于是线性地址对应的页帧为 0x00fa3,加上业内偏移 0x004,得到 0x00fa3004 即为变量 i 的物理地址。验证如下:

在这里插入图片描述

  使用命令 setpmem 0x00fa3004 4 0直接修改内存,将变量 i 的值设为 0,再用 c 命令继续运行 Bochs,死循环程序退出。

HIT Linux-0.11 实验七 地址映射与内存共享 实验报告_第6张图片

(二). 基于共享内存的生产者—消费者程序

  这个程序跟上次实验的不同之处在于:使用共享内存替换文件缓冲区;将生产者和消费者分成两个不同的程序,两个都是单进程的。

  Linux 中,将不同进程的虚拟地址空间通过页表映射到物理内存的同一区域即为共享内存。如图所示:

HIT Linux-0.11 实验七 地址映射与内存共享 实验报告_第7张图片

  两个进程都可以访问共享内存,但是为了确保对共享内存操作的互斥,仍需要使用一个信号量在每次读写的时候进行限制,然后由另外两个信号量保来保证共享内存中每次至多有 10 个数字。

  用共享内存和信号量实现的 producer.c代码如下:

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

#define SIZE 10
#define M 510

int main()
{
    int shm_id;
    int count = 0;
    int *p;
    int curr;
    sem_t *sem_empty, *sem_full, *sem_shm;
    sem_empty = sem_open("empty", O_CREAT|O_EXCL, 0644, SIZE);
    sem_full = sem_open("full", O_CREAT|O_EXCL, 0644, 0);
    sem_shm = sem_open("shm",  O_CREAT|O_EXCL, 0644, 1);
    shm_id = shmget(2521, SIZE, IPC_CREAT | IPC_EXCL | 0664); // 创建共享内存
    p = (int *)shmat(shm_id, NULL, 0);
    while (count <= M) {
        sem_wait(sem_empty);
        sem_wait(sem_shm);
        curr = count % SIZE;
        *(p + curr) = count;
        printf("Producer: %d\n", *(p + curr));
        fflush(stdout);
        sem_post(sem_shm);
        sem_post(sem_full);
        count++;
    }
    printf("producer end.\n");
    fflush(stdout);
    return 0;
}

  consumer.c的代码如下:

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

#define SIZE 10
#define M 510

int main()
{
    int shm_id;
    int count = 0;
    struct shmid_ds buf;
    int *p;
    int curr;
    sem_t *sem_empty, *sem_full, *sem_shm;
    sem_empty = sem_open("empty", SIZE);
    sem_full = sem_open("full", 0);
    sem_shm = sem_open("shm", 1);
    shm_id = shmget(2521, 0, 0);
    p = (int *)shmat(shm_id, NULL, 0);
    while(count <= M) {
        sem_wait(sem_full);
        sem_wait(sem_shm);
        curr = count % SIZE;
        printf("%d:%d\n", getpid(), *(p + curr));
        fflush(stdout);
        sem_post(sem_shm);
        sem_post(sem_empty);
        count++;
    }
    printf("consumer end.\n");
    fflush(stdout);
    sem_unlink("empty");
    sem_unlink("full");
    sem_unlink("shm");
    shmctl(shm_id, IPC_RMID, &buf);
    return 0;
}

  为了让两个进程在同一个终端运行,这里使用终端的后台运行功能:

./producer &
./consumer

在 Ubuntu 下的运行结果如图所示:

HIT Linux-0.11 实验七 地址映射与内存共享 实验报告_第8张图片

(三). 共享内存的实现

1. 实现共享内存

  函数 int shmget(key_t key, size_t size, int shmflg)会新建或打开一页内存,然后返回该页共享内存的 shmid。忽略 shmflg 参数后,可知一页共享内存需要保存的信息有 唯一标识符 key、共享内存的大小 size,然后还需要一个参数保存共享内存页面的地址。于是共享内存信息的结构体如下:

struct shm_tables
{
	int key;
	int size;
	unsigned long page;
};

  根据要求,shmget()函数需要获取一块空闲的内存的物理页面来创建共享内存,shmat()函数需要将该物理页面映射到进程的虚拟内存空间,然后返回其首地址。

  函数 get_free_page()能够获取一块空闲的物理页面,并且返回该页面的起始物理地址,用于 shmget()的实现。

  函数 put_page()能够把物理页面映射到指定线性地址空间处。为了能让两个进程操作这块共享内存,需要把物理页面分别映射到该进程自己的虚拟空间。内核为每个进程虚拟了一块地址空间,然后分配数据段、代码段和栈段,由函数 do_execve()实现,虚拟空间的分配如下图:

HIT Linux-0.11 实验七 地址映射与内存共享 实验报告_第9张图片

其中 start_code为代码段起始地址,brk为代码段和数据段的总长度,start_stack为栈的起始地址,这些值保存在进程的 task_struct中。brkstart_stack之间的空间为栈准备,栈底是闲置的,可将共享内存映射到这块空间。

  shm.c的代码如下:

#include 
#include 
#include 
#include 
#include 

#define _SHM_NUM 20

struct shm_tables
{
	int key;
	int size;
	unsigned long page;
} shm_tables[_SHM_NUM];

int sys_shmget(int key, int size)
{
    int i;
    unsigned long page;
    for (i = 0; i < _SHM_NUM; i++) /* 查看 key 对应的共享内存是否已存在 */
        if(shm_tables[i].key == key)
            return i;
    if (size > PAGE_SIZE) /* 内存大小超过一页 */
        return -EINVAL;
    page = get_free_page(); /* 获取物理内存页面 */
    if(!page)
        return -ENOMEM;
    for (i = 0; i < _SHM_NUM; i++) {
        if(shm_tables[i].key == 0) {
            shm_tables[i].key = key;
            shm_tables[i].size = size;
            shm_tables[i].page = page;
            return i;
        }
    }
    return -1;  /* 共享内存数量已满 */
}

void * sys_shmat(int shmid)
{
    int i;
    unsigned long data_base;
    if (shmid < 0 || shmid >= _SHM_NUM || shm_tables[shmid].key == 0) // 判断 shmid 是否合法
        return -EINVAL;
    put_page(shm_tables[shmid].page, current->brk + current->start_code); // 把物理页面映射到进程的虚拟空间
    current->brk += PAGE_SIZE;  // 修改总长度
    return (void*)(current->brk - PAGE_SIZE);
}

  修改 mm/Makefile,将 shm.c一块编译进 Image:

OBJS = memory.o page.o shm.o
# add
shm.o shm.c: ../include/asm/segment.h ../include/linux/kernel.h \
  ../include/linux/sched.h ../include/linux/mm.h ../include/errno.h

  然后对系统调用的实现做后续补充。在 include/linux/sys.h中添加:

extern int sys_shmget();
extern int sys_shmat();

fn_ptr sys_call_table[] = { ..., sys_shmget, sys_shmat };

  在 include/unistd.h及 Linux-0.11 中的 usr/include/unistd.h添加:

#define __NR_shmget     76
#define __NR_shmat      77

  最后修改 kernel/system_call.s中系统调用的数量:

nr_system_calls = 78

  共享内存实现完毕。

2. 在 Linux-0.11 运行生产者——消费者程序

  ;修改 consumer.cproducer.c,从而能够在 Linux-0.11 运行这两个程序:

#define __LIBRARY__   /* 在第一行添加 */

/* add */
_syscall2(int,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_value,sem_t *,sem)
_syscall1(int,sem_unlink,const char *,name)
_syscall2(int,shmget,int,key,int,size)
_syscall1(int, shmat, int, shmid)

int main()
{
    ...
    /* change */
    sem_empty = sem_open("empty", SIZE);
    sem_full = sem_open("full", 0);
    sem_shm = sem_open("shm", 1);
    shm_id = shmget(2521, SIZE);
    p = (int *)shmat(shm_id);
    ...
    return 0;
}

  然后在 Linux-0.11 中编译运行,结果如下(为了便于显示,这里将 M 改为 60、将 SIZE 改为 4):

HIT Linux-0.11 实验七 地址映射与内存共享 实验报告_第10张图片

三、实验总结

  Linux-0.11 同时采用了内存的分段和分页机制从而有效地使用物理内存。于是内存的地址映射需要进行以下步骤:

HIT Linux-0.11 实验七 地址映射与内存共享 实验报告_第11张图片

  在保护模式下,段寄存器存放的不再是内存的段基址,而是一个段描述符表中某一描述符项在表中的索引值。于是寻址的过程中需要使用段描述符表,包括全局描述符表 GDT、局部描述符表 LDT、中断描述符表 IDT。当 CPU 要寻址一个段时,就会使用 16 位的段寄存器中的选择符来定位一个段描述符,段寄存器中的值右移 3 位即是描述符表中一个描述符的索引值。选择符中位 2 (TI) 用来指定使用哪个表。若该位是 0 则选择符指定的是 GDT 表中的描述符,否则是 LDT 表中的描述符。

  在 Linux-0.11 中,程序逻辑地址到线性地址的变换过程使用了 CPU 的全局段描述符表
GDT 和局部段描述符表 LDT 。由 GDT 映射的地址空间称为全局地址空间,由 LDT 映射的地址空间则称
为局部地址空间,而这两者构成了虚拟地址的空间。如图所示:

HIT Linux-0.11 实验七 地址映射与内存共享 实验报告_第12张图片

  Intel 的 80386 CPU 可以提供 4G 的线性地址空间。Linux-0.11 中定义了最大进程数为 64 个,每个进程的逻辑地址范围是 64M,其中任务 0 和 1 将代码和数据区重叠在 640K 范围内,比较特殊。线性地址位置示意图如下:

HIT Linux-0.11 实验七 地址映射与内存共享 实验报告_第13张图片

  对于单个进程而言,其代码段和数据段、堆栈段的分布如图所示:

HIT Linux-0.11 实验七 地址映射与内存共享 实验报告_第14张图片

四、问题

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

最重要的四步:

  1. 获取 i 的虚拟地址
  2. 获取 LDT 表的地址
  3. 获取线性地址
  4. 获取物理地址

实验数据请见上文。

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

段基址可能会变化,因为操作系统为每个进程分配的 64M 空间位置不同,导致段基址不同。
而数据段偏移量不变,这是编译时就设置完毕的。

你可能感兴趣的:(OS,and,Linux)