手写操作系统(二十七)-内存管理系统

代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。

为保证OS的正常运行,将物理内存池分为用户物理内存池和内核物理内存池。

内存池中的内存也得按单位大小来获取,这个单位大小是4KB,称为页,内存池中管理的是一个个大小为4KB的内存块,从内存池中获取的内存大小至少为4KB或者为4KB的倍数。

为了方便实现,把这两个内存池的大小设为一致,即各占一半的物理内存。

内存分配过程:

内核申请内存:当内核申请内存时,从内核自己的虚拟地址池中分配虚拟地址,再从内核物理池(内核专用)中分配物理内存,然后在内核自己的页表中将两种地址建立好映射关系。

用户进程申请内存:OS从用户进程自己的虚拟地址池中分配空闲虚拟地址,然后再从用户物理内存池(所有用户进程共享的)中分配空闲的物理内存,然后在该用户进程自己的页表中将这两种地址建立好映射关系。

/kernel/memory.h

#ifndef __KERNEL_MEMORY_H
#define __KERNEL_MEMORY_H
#include "stdint.h"
#include "bitmap.h"
//虚拟地址池,用于虚拟地址管理
struct virtual_addr
{
    struct bitmap vaddr_bitmap; //虚拟地址用到的位图结构
    uint32_t vaddr_start;       //虚拟地址起始地址
};

extern struct pool kernel_pool, user_pool;
void mem_init(void);
#endif

主要就是定义struct virtual_addr,此结构就是虚拟地址池,用于虚拟地址管理。

struct virtual_addr 包含两个成员,一个是 vaddr_bitmap,它的类型是位图结构体 struct bitmap,用来以页为单位管理虚拟地址的分配情况,虚拟地址也要分配。 虽然多个进程可以拥有相同的虚拟地址,但究其原因,是因为这些虚拟地址所对应的物理地址是不同的。 但是,在同一个进程内的虚拟地址必然是唯一的,这通常是由链接器为其分配的,由链接器负责虚拟地址(程序内地址)的唯一性。 但进程在运行时可以动态从堆中申请内存,系统为其分配的虚拟地址也属于此进程的虚拟地址空间,也必须要保证虚拟地址的唯一性,所以,用位图来记录虚拟地址的分配情况。 vaddr_start 用来记录虚拟地址的起始值,咱们将来在分配虚拟地址时,将以这个地址为起始分配。 其他的部分是一些声明,它们都在memory.c中有具体的实现。

 

/kernel/memory.c

完整代码如下:

#include "memory.h"
#include "stdint.h"
#include "print.h"

#define PG_SIZE 4096
//4KB

/*位图地址
因为0xc009_f000是内核主线程栈顶,0xc009_e000是内核主线程的pcb。
一个页框大小的位图可表示128MB内存,位图位置安排在地址0xc009_a000,
这样本系统最大支持4个页框的位图,即512MB
*/
#define MEM_BITMAP_BASE 0xc009a000
/****************/

/*0xc000_0000是内核从虚拟地址3G起
0x10_0000是指跨过低端1MB内存,使虚拟地址在逻辑上连续*/
#define K_HEAP_START 0xc0100000

//内存池结构,生成两个实例用于管理内核内存池和用户内存池
struct pool{
    struct bitmap pool_bitmap;  //本内存池用到的位图结构,用于管理物理内存
    uint32_t phy_addr_start;    //本内存池所管理物理内存的起始地址
    uint32_t pool_size;         //本内存池的字节容量
};

struct pool kernel_pool,user_pool;  //生成内核内存池和用于内存池
struct virtual_addr kernel_vaddr;   //用来给内核分配虚拟地址

//初始化内存池
static void mem_pool_init(uint32_t all_mem){
    put_str("   mem_pool_init start\n");
    uint32_t page_table_size = PG_SIZE * 256;
    //页表大小=1页的页目录表 + 第0和第768个页目录项指向同一个页表 + (769~1022)个页目录项共指向254个页表 = 256
    uint32_t used_mem = page_table_size + 0x100000;     //0x10_0000是指跨过低端1MB内存 + 页表,使虚拟地址在逻辑上连续
    uint32_t free_mem = all_mem - used_mem;
    uint16_t all_free_pages = free_mem/PG_SIZE;
    uint16_t kernel_free_pages = all_free_pages / 2;
    uint16_t user_free_pages = all_free_pages - kernel_free_pages;

    //为简化位图操作,余数不处理
    uint32_t kbm_length = kernel_free_pages / 8;
    uint32_t ubm_length = user_free_pages / 8;
    uint32_t kp_start = used_mem;   //内核内存池起始地址
    uint32_t up_start = kp_start + kernel_free_pages*PG_SIZE;   //用户内存池起始地址

    kernel_pool.phy_addr_start = kp_start;
    user_pool.phy_addr_start = up_start;

    kernel_pool.pool_size = kernel_free_pages*PG_SIZE;
    user_pool.pool_size = user_free_pages*PG_SIZE;

    kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length;
    user_pool.pool_bitmap.btmp_bytes_len = ubm_length;

    /*内核内存池和用户内存池位图
    位图是全局数据,长度不固定
    全局/静态数组需要在编译时知道长度
    我们需要根据总内存大小算出需要多少字节,所以改为指定一块内存来生成位图
    */
    kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE;
    user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length);

    /*输出内存池信息*/
    put_str("   kernel_pool_bitmap_start:");
    put_int((int)kernel_pool.pool_bitmap.bits);
    put_str("   kernel_pool_phy_addr_start:");
    put_int(kernel_pool.phy_addr_start);
    put_str("\n");
    put_str("user_pool_bitmap_start:");
    put_int((int)user_pool.pool_bitmap.bits);
    put_str("   user_pool_phy_addr_start:");
    put_int((int)user_pool.phy_addr_start);
    put_str("\n");

    /*将位图置0*/
    bitmap_init(&kernel_pool.pool_bitmap);
    bitmap_init(&user_pool.pool_bitmap);

    /*初始化内核虚拟地址的位图,按照实际物理内存大小生成数组*/
    kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_length;
    //用于维护内核堆的虚拟地址,所以要和内核内存池大小一致

    /*位图的数组指向一块未使用的内存,目前定位在内核内存池和用户内存池之外*/
    kernel_vaddr.vaddr_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length + ubm_length);
    kernel_vaddr.vaddr_start = K_HEAP_START;
    bitmap_init(&kernel_vaddr.vaddr_bitmap);
    put_str("   mem_pool_init done\n");
}

/*内存管理部分初始化入口*/
void mem_init(){
    put_str("mem_init start\n");
    uint32_t mem_bytes_total = (*(uint32_t*)(0xb00));
    mem_pool_init(mem_bytes_total);
    put_str("mem_init done\n");
}

代码解释:

#define PG_SIZE 4096
//4KB

/*位图地址
因为0xc009_f000是内核主线程栈顶,0xc009_e000是内核主线程的pcb。
一个页框大小的位图可表示128MB内存,位图位置安排在地址0xc009_a000,
这样本系统最大支持4个页框的位图,即512MB
*/
#define MEM_BITMAP_BASE 0xc009a000

宏PG_SIZE,用以表示页的尺寸,其值为4096,即4KB。

宏MEM_BITMAP_BASE,用以表示内存位图的基址,其值为0xc009a000,选择这个数考虑进程 PCB 或线程 TCB (PCB 程序控制块,TCB 线程控制块)结构。 比如将来我们所实现的PCB要占用1页内存,即PCB要占用4KB大小的内存空间,不过要注意的是PCB所占用的内存必须是自然页,自然页就是页的起始地址必须是0xXXXXX000,终止地址必须是oxXXXXXfff。 也就是不能跨页占用,PCB必须是完整、单独地占用一个物理页框。 PCB是进程或线程的“身份证”,任何一个进程都包含一个PCB结构。

为方便叙述PCB的结构,假如PCB的地址是0xXXXXX000,PCB结构是这样的,在PCB的最低处0xXXXXX000以上存储的是进程或线程的信息,这包括pid、进程状态等,PCB的最高处OxXXXXXfff以下用于进程或线程在0特权级下所使用的栈。因为压栈操作的原理是栈指针esp先自减,然后再往自减后的地址处存储数据,故,任何进程或线程初始的栈顶便是此页框的最顶端+1,即下一个页框的起始处,也就是0xXXXXXfff+1。

程序都有个主线程,我们的内核也是一样,这个主线程就是指正式进入内核时所运行的程序,其实就是main线程。main线程一直存在,它调用了init_all来做各种初始化的工作,将来还要初始化线程等。也就是说,它必须也要有个PCB。我们早已经为main预留了PCB的空间,正如咱们在loader. S 中所做的,在进入内核之前,通过mov esp, 0xc009f000将内核所使用的栈顶指向0xc009f000,将来主线程的PCB地址是0xc009e000。

正是因为 PCB 要占用一个自然页,所以,在低端 1MB 的内存布局中,明明有一段 Ox7E00-0x9FBFF的可用内存空间,我们偏偏把0x9f000作为内核栈顶(0xc009f000对应的物理地址是0x9f000,因为在页表中,我们把低端1MB的内存做的是对等映射),而不是mov esp, 0xc009fc00。

当前虚拟机配置了32MB的物理内存,这32MB物理内存需要1024字节的位图,也就是仅占四分之一页,故一页大小的位图可管理128MB的内存。 在此打算支持4页内存的位图,即最大可管理512MB的物理内存。 既然0xc009e000已经是主线程的PCB,一页大小为0x1000,故再减去4页,即0xc009e000 – 0x4000 =0xc009a000,故我们的位图地址为0xc009a000。

 

//内存池结构,生成两个实例用于管理内核内存池和用户内存池
struct pool{
    struct bitmap pool_bitmap;  //本内存池用到的位图结构,用于管理物理内存
    uint32_t phy_addr_start;    //本内存池所管理物理内存的起始地址
    uint32_t pool_size;         //本内存池的字节容量
};

宏K_HEAP_START用来表示内核所使用的堆空间起始虚拟地址,其值为0xc0100000。内核也是程序,它偶尔也需要动态申请内存来完成某项工作,动态申请的内存都是在堆空间中完成的,我们为此也定义了内核所用的堆空间,堆也是内存,内存就得有地址。

在loader中我们已经通过设置页表把虚拟地址0xc0000000~0xc00fffff映射到了物理地址0x00000000~0x000fffff (低端1MB的内存),故我们为了让虚拟地址连续,将堆的起始虚拟地址设为0xc0100000。物理地址0x100000~0x101fff,是我们已经在loader.S中定义好的页目录及页表,因此将来的内核虚拟地址0xc0100000~0xc0101fff并不映射到这两个物理地址,必须要绕过它们。

物理内存池结构体struct pool,用它来管理本内存池中的所有物理内存。

此结构中定义了三个成员:

  • pool_bitmap是本内存池用于管理内存的位图结构。
  • phy_addr_start是本内存池的物理内存起始地址。
  • pool_size是本物理内存池的内存容量,因为物理地址是有限的。

struct pool 与struct virtual_addr同样都是地址池结构,但和struct pool 相比,struct virtual_addr 中没有 pool_size成员,尽管虚拟地址空间最大是4GB,但相对来说是无限的,不需要指定地址空间大小。因此虚拟地址池和物理地址池分别定义了两个结构。

 

struct pool kernel_pool,user_pool;  //生成内核内存池和用于内存池
struct virtual_addr kernel_vaddr;   //用来给内核分配虚拟地址

这里定义内核物理内存池变量kernel_pool和用户物理内存池变量user_pool,它们都是全局变量,以后的内存管理都需要用到这两个变量,它们在函数mem_pool_init中被初始化。

 

//初始化内存池
static void mem_pool_init(uint32_t all_mem){
    put_str("   mem_pool_init start\n");
    uint32_t page_table_size = PG_SIZE * 256;
    //页表大小=1页的页目录表 + 第0和第768个页目录项指向同一个页表 + (769~1022)个页目录项共指向254个页表 = 256
    uint32_t used_mem = page_table_size + 0x100000;     //0x10_0000是指跨过低端1MB内存 + 页表,使虚拟地址在逻辑上连续
    uint32_t free_mem = all_mem - used_mem;
    uint16_t all_free_pages = free_mem/PG_SIZE;
    uint16_t kernel_free_pages = all_free_pages / 2;
    uint16_t user_free_pages = all_free_pages - kernel_free_pages;

    //为简化位图操作,余数不处理
    uint32_t kbm_length = kernel_free_pages / 8;
    uint32_t ubm_length = user_free_pages / 8;
    uint32_t kp_start = used_mem;   //内核内存池起始地址
    uint32_t up_start = kp_start + kernel_free_pages*PG_SIZE;   //用户内存池起始地址

    kernel_pool.phy_addr_start = kp_start;
    user_pool.phy_addr_start = up_start;

    kernel_pool.pool_size = kernel_free_pages*PG_SIZE;
    user_pool.pool_size = user_free_pages*PG_SIZE;

    kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length;
    user_pool.pool_bitmap.btmp_bytes_len = ubm_length;

    /*内核内存池和用户内存池位图
    位图是全局数据,长度不固定
    全局/静态数组需要在编译时知道长度
    我们需要根据总内存大小算出需要多少字节,所以改为指定一块内存来生成位图
    */
    kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE;
    user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length);

    /*输出内存池信息*/
    put_str("   kernel_pool_bitmap_start:");
    put_int((int)kernel_pool.pool_bitmap.bits);
    put_str("   kernel_pool_phy_addr_start:");
    put_int(kernel_pool.phy_addr_start);
    put_str("\n");
    put_str("user_pool_bitmap_start:");
    put_int((int)user_pool.pool_bitmap.bits);
    put_str("   user_pool_phy_addr_start:");
    put_int((int)user_pool.phy_addr_start);
    put_str("\n");

    /*将位图置0*/
    bitmap_init(&kernel_pool.pool_bitmap);
    bitmap_init(&user_pool.pool_bitmap);

    /*初始化内核虚拟地址的位图,按照实际物理内存大小生成数组*/
    kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_length;
    //用于维护内核堆的虚拟地址,所以要和内核内存池大小一致

    /*位图的数组指向一块未使用的内存,目前定位在内核内存池和用户内存池之外*/
    kernel_vaddr.vaddr_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length + ubm_length);
    kernel_vaddr.vaddr_start = K_HEAP_START;
    bitmap_init(&kernel_vaddr.vaddr_bitmap);
    put_str("   mem_pool_init done\n");
}

函数mem_pool_init接受一个参数,all_mem,此参数表示内存容量,函数的功能是根据内存容量all_mem 的大小初始化物理内存池的相关结构。

前面将位图地址选在低端 1MB 以下,就是因为一般的内存管理系统所管理的是那些空闲的内存,即已被使用的内存是不在内存池中的,“已使用的内存”当然包括内存管理相关数据结构所占的内存,位图就是用于管理内存的数据结构,这也是位图地址选为0xc009a000 (0x9a000)的原因,此地址位于低端1MB之内,这里面的内存几乎都被占用了,因此我们就不用考虑它占用的内存了。

先定义了变量 page_table_size,它用来记录页目录表和页表占用的字节大小,总大小等于页目录表大小+页表大小。 页目录大小为1页框,第0和第768个页目录项指向同一个页表,它们共享这1页框空间,第769~1022个页目录项共指向254个页表,故页表总大小等于256*PG_SIZE,共计0x200000字节,2MB。 最后一个页目录项(第1023个pde)指向页目录表,因此不重复计算空间。

然后定义了变量used_mem,它用来记录当前已使用的内存字节数,这包括页表大小page_table_size 和低端0x100000 字节(1MB)内存。 变量free_mem,它用来存储目前可用的内存字节数,用总内存all_mem减去used_mem便是 free_mem。 变量all_free_pages,它用来保存可用内存字节数free_mem转换成的物理页数,因为内存池中的内存单位是物理页。

变量kernel_free_pages用来存储分配给内核的空闲物理页,变量user_free_pages把分配给内核后剩余的空闲物理页作为用户内存池的空闲物理页数量。变量kbm_length,它用来记录位图的长度。因为位图中的1位表示1页,故用kernel_free_pages除以8以获得位图的长度。为方便写程序,余数就不处理了,这样做的好处是不用做内存的越界检查,因为位图表示的内存少于实际物理内存,坏处是会丢(1~7页)*2的内存:内核物理内存池+用户物理内存池。

ubm_length是用户内存池位图的长度,同kbm_length一个道理。kp_start用于记录内核物理内存池的起始地址,值就是used_mem。up_start用于记录用户物理内存池的起始地址,其值等于kp_start加上内核物理内存池中的内存字节数,即加上kernel_free_pages * PG_SIZE。用以上的两个起始物理地址初始化各自内存池的起始地址,即kernel_pool. phy_ addrstart=kp_start, user_pool. phy_addr_start =up_start。

然后用各自内存池中的容量字节数(物理页数乘以PG_SIZE)初始化各自内存池的pool_size。接着用各自内存池的位图长度kbm_length和ubm_length初始化各自内存池的位图中的位图字节长度成员 btmp_bytes_len。 然后是初始化各自内存池所使用的位图。

位图可以用数组来实现,但是位图是全局数据结构,全局或静态的数组需要在编译时知道其长度,而位图的长度取决于具体要管理的内存页数量,因此是无法预计的,所以我们就只在struct bitmap结构中定义了bits成员用来记录上层模块提供的位图的地址。 这里提到的上层模块,就是指此处的mempool_init函数,由此函数为struct bitmap中的bits提供位图地址。变长数组只在c99中支持,并且数组占用的内存是堆空间,而且那还是在操作系统的支持下,而我们目前所做的正是在构建操作系统,而本节我们的内存管理系统,其实也是在构建堆内存管理,我们的两个内存池就相当于堆。综上所述,由于编译器必须要“事先”知道数组长度才能定义静态数组,但我们用作位图的数组,其长度取决于内存大小,我们需要在程序运行过程中根据总内存大小算出位图数组需要多少字节,也就是说在“将来”才能确定数组长度。因此改为指定一块内存来生成位图,这样就不需要固定长度了。

初始化内核位图的指针kernel_pool.pool_bitmap.bits,其值为MEM_BITMAP_BASE,也就是0x9a000。

初始化用户位图的指针user_pool.pool_bitmap. bits,其值为MEM_BITMAP_BASE + kbm_length,也就是紧跟在内核位图之后。

后面一串put_str函数,打印内存池的信息,这包括内存池的所用位图的起始地址和内存池的起始物理地址。内存池中的位图还需要初始化,位值为0表示该位对应的内存页未分配,位值为1表示该位对应的内存页已分配。然后调用函数bitmap_init将位图初始化为0,之后开始初始化内核虚拟地址池,为其所使用的位图指针初始化,将其安排在紧挨着内核内存池和用户内存池所用的位图之后,

即:

kernel_vaddr.vaddr_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length + ubm_length);

虚拟内存池的起始地址为K_HEAP_START,即0xc0100000,之后调用bitmap_init将其位图初始化。

变量mem_bytes_total,它用来存储机器上安装的物理内存总量。在loader.S中,为了获取内存容量,我们用了三种BIOS方法,最终把获取到的内存容量保存在汇编变量total_mem_bytes中,其物理地址为0xb00。 total_mem_bytes是用伪指令dd来定义的,其宽度是32位,因此,我们先把0xb00转换成32位整型指针,再通过*对该指针做取值操作,这样就获取到了内存容量。这就是代码“*(*(uint32_t*)(0xb00))”的意义。让函数mem_pool_init将它分配给各物理内存池。

mem_init是内存管理的初始化入口,它和之前的idt_init, timer_init一样,要被放到init.c中的init_all函数中才行。

/kernel/init.c修改如下:

#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"
#include "memory.h"

/* 负责初始化所有模块 */
void init_all() {
    put_str("init_all\n");
    idt_init();     // 初始化中断
    timer_init();   // 初始化 PIT
    mem_init();     // 初始化内存池
}

 

makefile修改

修改OBJS的值

OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \
       $(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o \
       $(BUILD_DIR)/debug.o $(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o \
       $(BUILD_DIR)/bitmap.o
## OBJS用来存储所有目标文件名,不要用%.o,因为不能保证链接顺序

还有

$(BUILD_DIR)/string.o: lib/string.c lib/string.h \
    kernel/debug.h kernel/global.h
    $(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/memory.o: kernel/memory.c kernel/memory.h \
                   lib/stdint.h lib/kernel/bitmap.h kernel/debug.h lib/string.h
    $(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/bitmap.o: lib/kernel/bitmap.c lib/kernel/bitmap.h \
                   lib/string.h kernel/interrupt.h lib/kernel/print.h kernel/debug.h
    $(CC) $(CFLAGS) $< -o $@

 

结果如下:

内核物理内存池所用的位图地址在 0xc009a000,其内存池中第一块物理页地址是0x200000。

 

参考

郑钢著操作系统真象还原

田宇著一个64位操作系统的设计与实现

丁渊著ORANGE’S:一个操作系统的实现

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇