操作系统学习-2.操作系统的内存初始化和使用

当一个操作系统被启动后它还有多少内存可用,以及应用程序的地址如何去使用这部分内存就是目前要解决的问题。比如说,一个应用程序它没有被限定的时候在编译器里面他是可以寻址到4G的而实际上我们不可能给他这么多的内存。这里就需要一个关系映射。

在这里插入图片描述

一.物理内存探测

首先我们要对目前真实的物理内存进行探测。一般来说,获取内存大小的方法由 BIOS 中断调用和直接探测两种。但BIOS 中断调用方法是一般只能在实模式下完成,而直接探测方法必须在保护模式下完成。通过 BIOS 中断获取内存布局有三种方式,都是基于INT 15h中断,分别为88h e801h e820h。但是 并非在所有情况下这三种方式都能工作。在 Linux kernel 里,采用的方法是依次尝试这三 种方法。

1.1.探测物理内存分布和大小的方法

操作系统需要知道了解整个计算机系统中的物理内存如何分布的,哪些被可用,哪些不可用。其基本方法是通过BIOS中断调用来帮助完成的。其中BIOS中断调用必须在实模式下进行,所以在bootloader进入保护模式前完成这部分工作相对比较合适。这些部分由boot/bootasm.S中从probe_memory处到finish_probe处的代码部分完成完成。通过BIOS中断获取内存可调用参数为e820h的INT 15h BIOS中断。BIOS通过系统内存映射地址描述符(Address Range Descriptor)格式来表示系统物理内存布局,其具体表示如下:

Offset  Size    Description
00h    8字节   base address               #系统内存块基地址
08h    8字节   length in bytes            #系统内存大小
10h    4字节   type of address range     #内存类型

看下面的(Values for System Memory Map address type)
Values for System Memory Map address type:

01h    memory, available to OS
02h    reserved, not available (e.g. system ROM, memory-mapped device)
03h    ACPI Reclaim Memory (usable by OS after reading ACPI tables)
04h    ACPI NVS Memory (OS is required to save this memory between NVS sessions)
other  not defined yet -- treat as Reserved

INT15h BIOS中断的详细调用参数:

eax:e820h:INT 15的中断调用参数;
edx:534D4150h (即4个ASCII字符“SMAP”) ,这只是一个签名而已; ebx:如果是第一次调用或内存区域扫描完毕,则为0。 如果不是,则存放上次调用之后的计数值;
ecx:保存地址范围描述符的内存大小,应该大于等于20字节; es:di:指向保存地址范围描述符结构的缓冲区,BIOS把信息写入这个结构的起始地址。 此中断的返回值为:
cflags的CF位:若INT 15中断执行成功,则不置位,否则置位;
eax:534D4150h (‘SMAP’) ;
es:di:指向保存地址范围描述符的缓冲区,此时缓冲区内的数据已由BIOS填写完毕
ebx:下一个地址范围描述符的计数地址
ecx :返回BIOS往ES:DI处写的地址范围描述符的字节大小
ah:失败时保存出错代码 这样,我们通过调用INT 15h BIOS中断,递增di的值(20的倍数),让BIOS帮我们查找出一个一个的内存布局entry,并放入到一个保存地址范围描述符结构的缓冲区中,供后续的ucore进一步进行物理内存管理。这个缓冲区结构定义在memlayout.h中:

struct e820map {
                  int nr_map;
                  struct {
                                    long long addr;
                                    long long size;
                                    long type;
                  } map[E820MAX];
};

1.2.物理内存探测的实现

物理内存探测是在bootasm.S中实现的,相关代码很短,如下所示:

probe_memory:
//对0x8000处的32位单元清零,即给位于0x8000处的
//struct e820map的成员变量nr_map清零
           movl $0, 0x8000
                  xorl %ebx, %ebx
//表示设置调用INT 15h BIOS中断后,BIOS返回的映射地址描述符的起始地址
                  movw $0x8004, %di
start_probe:
                  movl $0xE820, %eax // INT 15的中断调用参数
//设置地址范围描述符的大小为20字节,其大小等于struct e820map的成员变量map的大小
                  movl $20, %ecx
//设置edx为534D4150h (即4个ASCII字符“SMAP”),这是一个约定
                  movl $SMAP, %edx
//调用int 0x15中断,要求BIOS返回一个用地址范围描述符表示的内存段信息
                  int $0x15
//如果eflags的CF位为0,则表示还有内存段需要探测
                  jnc cont
//探测有问题,结束探测
                  movw $12345, 0x8000
                  jmp finish_probe
cont:
//设置下一个BIOS返回的映射地址描述符的起始地址
                  addw $20, %di
//递增struct e820map的成员变量nr_map
                  incl 0x8000
//如果INT0x15返回的ebx为零,表示探测结束,否则继续探测
                  cmpl $0, %ebx
                  jnz start_probe
finish_probe:

上述代码正常执行完毕后,在0x8000地址处保存了从BIOS中获得的内存分布信息,此信息按照struct e820map的设置来进行填充。这部分信息将在bootloader启动系统后,由系统的的page_init函数来根据struct e820map的memmap(定义了起始地址为0x8000)来完成对整个机器中的物理内存的总体管理。
在这里插入图片描述

实验打印的map的结果,也就是说我们有这么多内存可以供后面的程序使用。
在linux中的物理内存探测参考如下文章:
https://www.cnblogs.com/hehehaha/archive/2013/04/03/6332942.html

二、内存划分

探测完成物理内存后,我们要把这些内存划分成一个可以映射的也表。这个过程就是逻辑地址—》线性地址的映射。这里面有几个概念

  • 逻辑地址:由段地址和段内偏移两部分组成。
  • 线性地址(虚拟地址):将逻辑地址进行解释得到的地址形式,它是一个32位无符号整数,值的范围为0x00000000到0xffffffff可对4GB的空间进行寻址。之所以又称为虚拟地址,是因为后来的虚拟存储技术,使得线性地址并不会直接对应到相应的物理地址,而要经过一定的映射,将线性地址转换为物理地址,为了与物理地址相对应,又将线性地址称为虚拟地址。
  • 物理地址:对内存芯片进行寻址时的地址,进行寻址时CPU会将这个地址以电信号的形式发送给内存芯片进行寻址。
    表示如下
    在这里插入图片描述
    在这里插入图片描述
    上面两张图大概概括了,段机制和页机制的配合结构。
    代码如何实现上面的功能? 或者说给出一个虚拟地址la 怎么找到物理地址pa。其过程是怎么样的。

2.1.使用页单位划分物理内存

在物理内存探测中,我们已经探测好了,空闲的物理内存。现在是时候用一个list,讲这些空闲的物理内存连起来。

static void
page_init(void) {
    struct e820map *memmap = (struct e820map *)(0x8000 + KERNBASE);
    uint64_t maxpa = 0;

    cprintf("e820map:\n");
    int i;
    for (i = 0; i < memmap->nr_map; i ++) {
        uint64_t begin = memmap->map[i].addr, end = begin + memmap->map[i].size;
        cprintf("  memory: %08llx, [%08llx, %08llx], type = %d.\n",
                memmap->map[i].size, begin, end - 1, memmap->map[i].type);
        if (memmap->map[i].type == E820_ARM) {
            if (maxpa < end && begin < KMEMSIZE) {
                maxpa = end;
            }
        }
    }
    if (maxpa > KMEMSIZE) {
        maxpa = KMEMSIZE;
    }

    extern char end[];

    npage = maxpa / PGSIZE;
    pages = (struct Page *)ROUNDUP((void *)end, PGSIZE);

    for (i = 0; i < npage; i ++) {
        SetPageReserved(pages + i);
    }

    uintptr_t freemem = PADDR((uintptr_t)pages + sizeof(struct Page) * npage);

    for (i = 0; i < memmap->nr_map; i ++) {
        uint64_t begin = memmap->map[i].addr, end = begin + memmap->map[i].size;
        if (memmap->map[i].type == E820_ARM) {
            if (begin < freemem) {
                begin = freemem;
            }
            if (end > KMEMSIZE) {
                end = KMEMSIZE;
            }
            if (begin < end) {
                begin = ROUNDUP(begin, PGSIZE);
                end = ROUNDDOWN(end, PGSIZE);
                if (begin < end) {
                    init_memmap(pa2page(begin), (end - begin) / PGSIZE);
                }
            }
        }
    }
}

上面的代码就是使用探测好的地址,从开始到结束 给了init_memmap 它会负责将所有探测出来的地址进行页的管理。

2.2.使能页机制

使能页机制主要有一下三个方面要做的。

boot_pgdir = boot_alloc_page();
boot_cr3 = PADDR(boot_pgdir);
boot_map_segment(boot_pgdir, KERNBASE, KMEMSIZE, 0, PTE_W);
enable_paging();
gdt_init();
static void
enable_paging(void) {
    lcr3(boot_cr3);
    // turn on paging
    uint32_t cr0 = rcr0();
    cr0 |= CR0_PE | CR0_PG | CR0_AM | CR0_WP | CR0_NE | CR0_TS | CR0_EM | CR0_MP;
    cr0 &= ~(CR0_TS | CR0_EM);
    lcr0(cr0);
}

A:获取一个页目录表
B:将页目录表和页表和物理地址和虚拟地址建立该有的链接
C:使能页机制重启全局描述符gdt

最难理解的是第二个。我们一行一行代码理解。

boot_map_segment(boot_pgdir, KERNBASE, KMEMSIZE, 0, PTE_W);
boot_pgdir 参数为我们获取的一页4k,将用作目录表,起始boot_pgdir的地址将设置到cr3寄存器,cpu运行代码的虚拟地址la 将找到这个内存位置。
KERNBASE 参数为线性偏移地址 0xC00000000
KMEMSIZE 参数为内存总大小
0 为物理内存起始地址
PTE_W 表示写使能

static void
boot_map_segment(pde_t *pgdir, uintptr_t la, size_t size, uintptr_t pa, uint32_t perm) {
    assert(PGOFF(la) == PGOFF(pa));
    size_t n = ROUNDUP(size + PGOFF(la), PGSIZE) / PGSIZE;
    la = ROUNDDOWN(la, PGSIZE);
    pa = ROUNDDOWN(pa, PGSIZE);
    for (; n > 0; n --, la += PGSIZE, pa += PGSIZE) {
        pte_t *ptep = get_pte(pgdir, la, 1);
        assert(ptep != NULL);
        *ptep = pa | PTE_P | perm;
    }
}
pte_t * get_pte(pde_t *pgdir, uintptr_t la, bool create) {
    pde_t *pdep = &pgdir[PDX(la)];
    if (!(*pdep & PTE_P)) {
        struct Page *page;
        if (!create || (page = alloc_page()) == NULL) {
            return NULL;
        }
        set_page_ref(page, 1);
        uintptr_t pa = page2pa(page);
        memset(KADDR(pa), 0, PGSIZE);
        *pdep = pa | PTE_U | PTE_W | PTE_P;
    }
    return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)];
}

boot_map_segment 函数中 for 循环从 分别从 la 到 pa 线性增大 他们的关系将在 get_pte中做链接。

get_pte中PDX是右移22为 就剩下高10为 作为数组下标拿到刚才页目录表(PDT)的位置,如图位置
在这里插入图片描述

在来到if语句 如果这个表已经存在 就会通过

 return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)];  

这句话拿到页表(PT)的位置,如下
在这里插入图片描述
从1为什么能找到2,看if语句里面的内容。当一开始如果这个表不在,就创建一个page 作为物理内存的起止pa地址存放中哪里? 存放到pdep里面去。所以1能找到2.
在这里插入图片描述
同样的原理2能找到3--------》回到for循环最后一句,将3的地址保存在2中。

所以现在大家明白了 给你一个la 通过1 2 3 找到pa;

2.3.寻址过程–关于段页和缺页异常

在这里插入图片描述
首先要关注的是全局页表描述符,GDT 和全局中断描述符IDT一样,
首先我们要使用它,对他进行设置,然后别人来查询的时候就知道你的页表在哪里,如同IDT一样,别人设置好以后别人就知道你的终端在哪里。

这里GDT 设置的过程是这样的

static struct segdesc gdt[] = {
    SEG_NULL,
    [SEG_KTEXT] = SEG(STA_X | STA_R, 0x0, 0xFFFFFFFF, DPL_KERNEL),
    [SEG_KDATA] = SEG(STA_W, 0x0, 0xFFFFFFFF, DPL_KERNEL),
    [SEG_UTEXT] = SEG(STA_X | STA_R, 0x0, 0xFFFFFFFF, DPL_USER),
    [SEG_UDATA] = SEG(STA_W, 0x0, 0xFFFFFFFF, DPL_USER),
    [SEG_TSS]   = SEG_NULL,
};

static struct pseudodesc gdt_pd = {
    sizeof(gdt) - 1, (uintptr_t)gdt
};

分配好 各个线性地址空间有多大,是哪个段,代码段还是数据段,

static void
gdt_init(void) {
    // set boot kernel stack and default SS0
    load_esp0((uintptr_t)bootstacktop);
    ts.ts_ss0 = KERNEL_DS;

    // initialize the TSS filed of the gdt
    gdt[SEG_TSS] = SEGTSS(STS_T32A, (uintptr_t)&ts, sizeof(ts), DPL_KERNEL);

    // reload all segment registers
    lgdt(&gdt_pd);

    // load the TSS
    ltr(GD_TSS);
}

然后初始化的时候设置进去。

然后进程逻辑地址 去寻址的时候,就通过这个gdt 转换成线性地址,但是我们看到 这里的gdt 是没有改变的,也就是说没有限制的,这样是很危险的,我把他改一下。

static struct segdesc gdt[] = {
    SEG_NULL,
    [SEG_KTEXT] = SEG(STA_X | STA_R, 0x0, 0x000000FF, DPL_KERNEL),
    [SEG_KDATA] = SEG(STA_W, 0x000000FF, 0x0000FFF, DPL_KERNEL),
    [SEG_UTEXT] = SEG(STA_X | STA_R, 0x0000FFF 0xFFFFF000, DPL_USER),
    [SEG_UDATA] = SEG(STA_W, 0xFFFFF000, 0xFFFF0000, DPL_USER),
    [SEG_TSS]   = SEG_NULL,
};

只是随便改一改,
这样就可以按照这个表找到不同区间的线性地址。

有了线性地址以后,
去访问内存,
在这里插入图片描述
现在看下 第二步是怎么执行的,一个线性地址,怎么找到页表,
是这样的cpu 首先会记录 pdt的 初始的地址,在cr3 里面。

static void
enable_paging(void) {
    lcr3(boot_cr3);

    // turn on paging
    uint32_t cr0 = rcr0();
    cr0 |= CR0_PE | CR0_PG | CR0_AM | CR0_WP | CR0_NE | CR0_TS | CR0_EM | CR0_MP;
    cr0 &= ~(CR0_TS | CR0_EM);
    lcr0(cr0);
}

关于也表的建立 我就不多说了,

然后进行 线性地址的映射 通页表顺利的找到了物理地址,
但是如果如果不顺利呢?
不顺利的情况最通常的是------页表没有建立,产生缺页异常。
这个时候,cpu产生缺页中断,然后通软件形式建立好页表,然后恢复到中断的位置继续执行。OK。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

沈万三djh

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

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

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

打赏作者

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

抵扣说明:

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

余额充值