【哈工大_操作系统实验】Lab7 地址映射与共享

October 15, 2024

Github代码仓库链接

本节将更新哈工大《操作系统》课程第七个 Lab 实验 地址映射与共享。按照实验书要求,介绍了非常详细的实验操作流程,并提供了超级无敌详细的代码注释。

实验目的:

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

实验任务:

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

文件名 介绍
hit-操作系统实验指导书.pdf 哈工大OS实验指导书
Linux内核完全注释(修正版v3.0).pdf 赵博士对Linux v0.11 OS进行了详细全面的注释和说明
file1615.pdf BIOS 涉及的中断数据手册
hit-oslab-linux-20110823.tar.gz hit-oslab 实验环境
gcc-3.4-ubuntu.tar.gz Linux v0.11 所使用的编译器
Bochs 汇编级调试指令 bochs 基本调试指令大全
最全ASCII码对照表0-255 屏幕输出字符对照的 ASCII 码
x86_64 常用寄存器大全 x86_64 常用寄存器大全

一、IA-32 的地址翻译过程

1、启动调试器

  • test.c 测试程序拷贝到 linux0.11 系统下
cd oslab_Lab6
sudo ./mount-hdc
cp test.c hdc/usr/root/
sudo umount hdc
// 启动 linux-0.11
./dbg-asm
c              // 运行到断点位置

Next at t=0 表示下面的指令是 Bochs 下一条要执行的软件指令

2、编译并运行 test.c

  1. linux-0.11 系统下编译运行 test.c
gcc -o test test.c
./test
// 输出:The logical/virtual address of i is 0x00003004

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

在这里插入图片描述

  1. 暂停
  • 在命令行窗口按 Ctrl+c,Bochs 会暂停运行态。绝大多数情况下都会停在 test 内。
  • 其中的 000f 如果是 0008,则说明中断在了内核里。那么就要 c,然后再 Ctrl+c,直到变为 000f 为止。
  • 如果显示的下一条指令不是 cmp ...(这里指语句以 cmp 开头),就用 n 命令单步运行几步,直到停在 cmp ...。

在这里插入图片描述

  • 使用命令 u /8,显示从当前位置开始 8 条指令的反汇编代码。
  • 这就是 test.c 中从 while 开始一直到 return 的汇编代码。变量 i 保存在 ds:0x3004 这个地址,并不停地和 0 进行比较,直到它为 0,才会跳出循环。

在这里插入图片描述

3、查段表,计算 DS 线性地址

  1. LDT 表位置
  • ds:0x3004 是虚拟地址,应用程序都有一个段表,叫 LDT。
  • ldtr 寄存器表示 LDT表 存放在 GDT 表的位置。
  • sreg 指令显示
<bochs:2> 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=0xa2d00068, dh=0x000082fa, valid=1
tr:s=0x0060, dl=0xa2e80068, dh=0x00008bfa, valid=1
gdtr:base=0x00005cb8, limit=0x7ff
idtr:base=0x000054b8, limit=0x7ff
  • 0x0068=0000000001101000,则存放在 GDT 表的 1101(二进制)=13(十进制)号位置

在这里插入图片描述

  1. 在 GDT 表中查找 LDT 表
  • GDT 表中的每一项占 64 位(8 个字节),所以我们要查找的项的地址是 0x00005cb8+13*8xp 是用于查看内存内容的调试指令,w表示显示两个字(b-BYTE, h-WORD, w-DWORD, g-DWORD64)
<bochs:5> xp /2w 0x00005cb8+13*8
[bochs]:
0x00005d20 <bogus+       0>:    0xa2d00068    0x000082fa

此时可以发现,和前面sreg指令中ldtr后面的dldh值一样(这是Bochs 调试器自动计算出的)

  1. 利用 GDT 表中描述符计算 LDT 表线性地址
  • 描述符为“0xa2d00068 0x000082fa”,-> 0x 00 fa a2d0 (前面32位对应下面行,后面32位对应上面行)

在这里插入图片描述

  • 根据该地址,找到了 LDT 表位置,前4项内容为(每一项也是64位):
<bochs:7> xp /8w 0x00faa2d0
[bochs]:
0x00faa2d0 <bogus+ 0>: 0x00000000 0x00000000
0x00000002 0x10c0fa00
0x00faa2e0 <bogus+ 16>: 0x00003fff 0x10c0f300
0x00000000 0x00fab000
  1. 段寄存器(段选择子) DS 从 LDT 表获取段描述符
  • sreg指令获取 DS 寄存器内容
<bochs:8> sreg
ds:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=3

在这里插入图片描述

  • RPL:即如果 RPL 的数值大于 CPL(数值越大,权限越小),则用 RPL 的值覆盖 CPL 的值,即 CPR, RPL ≤ DPL
  • TI:TI=0,在 GDT 表中查; TI = 1,在 LDT 表中查。
  • ds = 0x0017 = 0001 0111,即RPL = 11, TI = 1,索引 = 10,即在 LDT 表中索引为 2 的描述符(即第三个),即0x00003fff 0x10c0f300 为搜寻很久的 DS 段描述符了。

此时可以发现,和前面sreg指令中ds后面的dldh值一样(这是Bochs 调试器自动计算出的)

总结:

  • 根据段选择子ldtr在GDT表中找到了LDT表中的段描述符,计算线性地址,该线性地址其实bochs调试器在ldtr寄存器后面的dh、dl中已经计算好了,可以供我们验证;
  • 根据段选择子DS在LDT表中找到DS的段描述符,计算线性地址,该线性地址其实bochs调试器在DS寄存器后面的dh、dl中已经计算好了,可以供我们验证。
  1. 利用 LDT 表中段描述符计算 DS 线性地址(段基址)
  • 由段描述符0x00003fff 0x10c0f300 可以计算得到段基址为0x 10 00 0000,所以 ds:0x3004 的线性地址为 0x100003004
  • 使用 cals ds:0x3004 可以验证该结果

4、查页表,由线性地址计算物理地址

即计算 页目录号(10位)、页表号(10位)和页内偏移(12位)

  • 线性地址 0x100003004:页目录号 = 64, 页号 = 3, 页内偏移 = 4
  1. 在页目录表中查找
  • 页目录表的位置由 CR3 寄存器指引,creg命令可以查看:

在这里插入图片描述

  • 说明页目录表的基址为 0
  • 页目录表和页表中的内容很简单,是 1024 (2^10)个 32 位数,表大小刚好为4K。用以下指令可以在页目录表中查找到页目录项为
xp /w 0+64*4
// 0x00000100 : 0x00fa5027
  1. 在页表中查找
  • 上述可得页表所在物理内存位置为 0x00faa位置(027是属性),得3号页表项为:
xp /w 0x00faa000+3*4
// 0x00fa500c : 0x00f9b067
  1. 计算物理地址

物理页框号为 0x00f9b(067是属性),和页内偏移 0x004,得到 0x00f9b004,这就是变量 i 的物理地址。

可以通过两种方式验证:

  • 基于线性地址 0x10003004 ,使用 page 指令:
page 0x10003004
linear page 0x10003000 maps to physical page 0x00f9b000
  • 基于物理地址 0x00fa7004,使用 xp 指令查看地址中存放的一个字节:
xp /w 0x00f9b004
0x00fa7004 : 0x12345678

5、修改内存来改变 i 的值,使程序退出循环

  • 将从 0x00f9b004 地址开始的 4 个字节都设为 0
setpmem 0x00f9b004 4 0
  • 然后再用 c 命令继续 Bochs 的运行,可以看到 test 退出了,说明 i 的修改成功了,此项实验结束。

在这里插入图片描述

二、在 Linux 0.11 中实现共享内存

本次实验将在 Lab6 信号量的实现和应用 实验的基础上,使用共享内存替换文件缓冲区,来完成生产者和消费者两个程序。

Linux 中,将不同进程的虚拟地址空间通过页表映射到物理内存的同一区域,实现共享内存。

在这里插入图片描述

1、新建文件kernel/shm.c,实现共享内存

#include <asm/segment.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/mm.h>
#include <errno.h>

#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;        /* 存放共享内存地址 */

    /* 查看 key 对应的共享内存是否已存在 */
    for (i = 0; i < _SHM_NUM; i++) 
        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;

    /* 判断共享内存 shmid 是否越界 及 共享内存是否存在 */
    if (shmid < 0 || shmid >= _SHM_NUM || shm_tables[shmid].key == 0)
        return -EINVAL;
    
    /* 把物理页面映射到进程的虚拟内存空间,映射到代码段+数据段后,堆栈段前 */
    put_page(shm_tables[shmid].page, current->brk + current->start_code);
    /* 修改总长度,brk为代码段和数据段的总长度 */
    current->brk += PAGE_SIZE;
    return (void*)(current->brk - PAGE_SIZE);
}

对程序中 sys_shmat 中的虚拟内存地址进行解释:

  • brk为代码段和数据段的总长度,brk + start_code 即为代码段+数据段结束的位置;start_stack为栈的起始地址。
  • 因此,将物理内存映射到虚拟内存处,brkstart_stack之间的空间为栈准备,栈底是闲置的(栈是往低地址方向扩展的),可将共享内存映射到这块空间。

在这里插入图片描述

2、修改文件include/unistd.h,新增全局函数

/* 实验6 */
#define __NR_sem_open   72
#define __NR_sem_wait   73
#define __NR_sem_post   74
#define __NR_sem_unlink 75
/* 实验7 */
#define __NR_shmget     76
#define __NR_shmat      77

3、修改/kernel/system_call.s,需要修改总的系统调用数

nr_system_calls = 78

4、修改/include/linux/sys.h,声明全局新增函数

/* 实验6 */
extern int sys_sem_open();
extern int sys_sem_wait();
extern int sys_sem_post();
extern int sys_sem_unlink();
/* 实验7 */
extern int sys_shmget();
extern int sys_shmat();

fn_ptr sys_call_table[] = {
//...sys_setreuid,sys_setregid,sys_whoami,sys_iam,
sys_sem_open,sys_sem_wait,sys_sem_post,sys_sem_unlink, /* 实验6 */
sys_shmget, sys_shmat   /* 实验7 */
};

5、修改 linux-0.11/kernel/Makefile,添加sem.c编译规则

OBJS  = sched.o system_call.o traps.o asm.o fork.o \
	panic.o printk.o vsprintf.o sys.o exit.o \
	signal.o mktime.o who.o sem.o
// ...
### Dependencies:
sem.s sem.o: sem.c ../include/linux/sem.h ../include/linux/kernel.h \
../include/unistd.h

6、实现生产者程序 producer.c 和 消费者程序 consumer.c

为了确保对共享内存操作的互斥,仍需要使用一个信号量在每次读写的时候进行限制;同理,还需要两个信号量保来保证共享内存中缓冲区大小为 10

producer.c

#define __LIBRARY__   /* 在第一行添加 */
#include <stdio.h>
#include <stdlib.h>
#include <linux/sem.h>
#include <unistd.h>
#include <fcntl.h>

#define SIZE 10
#define M 510

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

int main()
{
    int shm_id;
    int count = 0;
    int *p;
    int curr;
    
    sem_t *sem_empty, *sem_full, *sem_shm;  /*3个信号量*/
    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);       /* 将指定物理页面映射到当前进程的虚拟内存空间 */

    /*生产多少个产品就循环几次*/
    while (count <= M) {
        /*empty大于0,才能生产*/
        sem_wait(sem_empty);    /* empty-- */
        sem_wait(sem_shm);      /* mutex-- */

        /*从上次位置继续向文件缓冲区写入一个字符*/
        curr = count % SIZE;    /*更新写入缓冲区位置,保证在0-9之间,缓冲区最大为10*/
        *(p + curr) = count;
        printf("Producer: %d\n", *(p + curr));
        fflush(stdout);

        sem_post(sem_shm);      /* mutex++ */
        sem_post(sem_full);     /* full++,唤醒消费者线程 */
        count++;
    }
    printf("producer end.\n");
    fflush(stdout);
    return 0;
}

consumer.c

#define __LIBRARY__   /* 在第一行添加 */
#include <stdio.h>
#include <stdlib.h>
#include <linux/sem.h>
#include <unistd.h>
#include <fcntl.h>

#define SIZE 10
#define M 510

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

int main()
{
    int shm_id;
    int count = 0;
    int *p;
    int curr;

    sem_t *sem_empty, *sem_full, *sem_shm;  /*3个信号量*/
    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);       /* 将指定物理页面映射到当前进程的虚拟内存空间 */

    /*生产多少个产品就循环几次*/
    while(count <= M) {
        /* full大于0,才能消费 */
        sem_wait(sem_full); /* full-- */
        sem_wait(sem_shm);  /* mutex-- */

        /*从上次位置继续向文件缓冲区写入一个字符*/
        curr = count % SIZE;    /*更新写入缓冲区位置,保证在0-9之间,缓冲区最大为10*/
        printf("%d:%d\n", getpid(), *(p + curr));
        fflush(stdout);

        sem_post(sem_shm);      /* mutex++ */
        sem_post(sem_empty);    /* empty++,唤醒生产者进程 */
        count++;
    }
    printf("consumer end.\n");
    fflush(stdout);
    /*释放信号量*/
    sem_unlink("empty");
    sem_unlink("full");
    sem_unlink("shm");
    return 0;
}

三、在linux0.11环境下编译运行

  1. 将已经修改的consumer.cproducer.cunistd.hsem.h(详见Lab6)文件拷贝到linux-0.11系统中
cd oslab_Lab6
sudo ./mount-hdc
cp ./consumer.c ./hdc/usr/root/
cp ./producer.c ./hdc/usr/root/
cp ./linux-0.11/include/unistd.h ./hdc/usr/include/ 
cp ./linux-0.11/include/linux/sem.h ./hdc/usr/include/linux/
sudo umount hdc
  1. 编译及运行Bochs
cd oslab_Lab6/linux-0.11
make all
../run
  1. 在linux0.11的Bochs中编译生产者-消费者程序
gcc -o producer producer.c
gcc -o consumer consumer.c
sync
./producer > p.txt &
./consumer > c.txt

& 表示使用终端的后台运行功能。

  1. 在Ubuntu中挂载hdc,将linux0.11输出的1.txt文件移动到Ubuntu中
sudo ./mount-hdc
cd /hdc/usr/root/
sudo cp ./p.txt ../../..
sudo cp ./c.txt ../../..

打开p.txtc.txt 文件即可见到输出如下:

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


Profile picture

Written by JokerDebug who works at Southeast University, Nanjing, China You can follow me on Github