文章目录

  • 内存规划
  • 位图法
    • 位图管理内存
  • 内存管理
    • 内存管理初始化
    • 内存的分配
      • 虚拟内存的申请
      • 物理内存的申请
      • 建立物理地址和虚拟地址的映射
    • 内存回收
      • 根据虚拟地址获得物理地址
    • 清除内核物理位图
    • 删除页表
    • 清除虚拟位图
  • 参考文献

写在前面:自制操作系统Gos 第三章第一篇:主要内容是如何管理内存,实现自己的内存池对物理内存进行管理。
有关内存和内存池的知识见以下几篇博客:内存、内存——CPU、内存以及磁盘是如何交互的、malloc底层原理剖析——ptmalloc内存池
Gos完整代码:Github

内存规划

我们都知道,为了共享内核给我们提供的系统调用以及各种硬件资源。Linux对一个进程的内存进行了如下规划:1G的内核空间+3G的用户空间。而这个内存规划只是在虚拟内存的层面上进行的。

那么对于实际上的物理内存呢?想要共享内核的前提是内核肯定是运行在物理内存上的,那么我们就需要对物理内存进行规划。比如说多大的内存用于运行内核呢?这种问题必须给出答案,不如一旦内核或者进程用的内存大小超过界限,都会导致另一方无内存可用,直接挂掉。

我们的最终目的是实现一个简单的操作系统,本来说我们可用直接把所有内存都分配给内核的,但是为了之后我们还要运行其他进程,所以我们这里把实际的物理内存分为两部分,两个内存池各占一半物理内存大小。其中一个叫做内核物理内存池,其内存全部给我们的内核使用;另一个叫做用户物理内存池,其中的物理内存用于分配给用户进程。如下图所示:


这个内存总量是一开始我们写道bochs配置环境中的:

代码表示如下:

//内存池结构
struct pool
{struct bitmap pool_bitmap; //用于管理物理内存uint32_t phy_addr_start;   //管理的物理内存的起始地址uint32_t pool_size;        //本内存池字节容量struct lock lock;          //申请内存时互斥
};struct pool kernel_pool, user_pool; //生成内核内存池和用户内存池

位图法

在刚刚的物理内存划分的时候,我们看到了一个特殊的数据结构bitmap,其被标记为用来管理物理内存。

其他几个变量我们都很好理解,是描述这个内存池的元信息。但是这个位图法存在的意义是什么呢?
我们分配内存的时候,是不是首先得保证这块内存当前没有正在被使用才会把它分配出去。bitmap就是表述内存是否正在被使用的数据结构。

在操作系统设计的时候,内存分配时以页为单位的,一个页是4KB,那么对于我们32MB的物理内存来说,设置一个整型变量来表示一个页是否被使用,那么我们就需要大概8196个变量,那么这就需要32KB大小的物理空间。32MB就需要32KB大小的空间用于管理内存,那么更大的4GB、8GB、甚至16GB呢?

这个开销太大了。我们知道计算机的每一位都有0和1两个状态,而内存页只有被使用和未被使用两个状态。那么我们就可以用一个位来表示一个内存页是否被使用了,这样我们只需要用1KB就可以表示32MB大小的空间了。这就是位图法,如图所示:

位图管理内存

那么我们如何去用位图去管理内存呢?首先我们来看一下位图的数据结构:

struct bitmap
{uint32_t btmp_bytes_len;    //位图总共字节大小uint8_t *bits;              //位图指针
};

首先,内存在刚接上电源的时候,可以看成是空的,那么这个时候我们的位图,其实是空的。也就是说,我们需要在位图初始化的时候将其清空:

/** @brief 初始化结构体bitmap为0* @param btmp 结构体bitmap的指针*/
void bitmap_init(struct bitmap *btmp)
{memset(btmp->bits, 0, btmp->btmp_bytes_len);
}

之后,我们的程序就开始运行起来了。这个时候无可避免的就需要申请内存空间,那么对位图来说,其实就是从头往后开始扫描哪个位是0,因为是0表示这个内存页没有被用过。如果申请连续多个空间,则需要从头往后扫描至有多个连续为0的这种情况:

/** @brief 在bitmap中申请连续count个位,成功返回其起始位下标;失败,返回-1* @param btmp 结构体bitmap的指针* @param cnt 申请的位个数* @return 申请的起始下标,如果不够用就返回-1*/
int bitmap_scan(struct bitmap *btmp, uint32_t cnt)
{uint32_t idx_byte = 0; //记录空闲位所在字节//略过分配过的字节while ((0xff == btmp->bits[idx_byte]) && (idx_byte < btmp->btmp_bytes_len)){idx_byte++;}//判读是否到末尾了ASSERT(idx_byte < btmp->btmp_bytes_len);if (idx_byte == btmp->btmp_bytes_len){return -1;}int idx_bit = 0;//找这个字节第一个空闲的位while ((uint8_t)(BITMAP_MASK << idx_bit) & btmp->bits[idx_byte]){idx_bit++;}//现在定位到bitmap的第idx_byte个字节的第idx_bit位是空闲的int bit_idx_start = idx_byte * 8 + idx_bit; //定位到具体是bitmap的哪一位if (cnt == 1){return bit_idx_start;}//记录还有多少位可以判断uint32_t bit_left = (btmp->btmp_bytes_len * 8 - bit_idx_start);uint32_t next_bit = bit_idx_start + 1;uint32_t count = 1; //记录找到的空闲位的个数bit_idx_start = -1;while (bit_left-- > 0){//判断next_bit是否为1if (!(bitmap_scan_test(btmp, next_bit))){count++;}else{count = 0;}//找到了连续的cnt个空位if (count == cnt){bit_idx_start = next_bit - cnt + 1;break;}next_bit++;}return bit_idx_start;
}

这样,如果我们找到这个我们需要的位之后,直接将其置为1就好啦:

/** @brief 将bitmap的bit_idx位置为value* @param btmp 结构体bitmap的指针* @param bit_idx bitmap的位下标* @param value 待赋给bitmap的bit_idx位的值*/
void bitmap_set(struct bitmap *btmp, uint32_t bit_idx, int8_t value)
{ASSERT((value == 0) || (value == 1));uint32_t byte_idx = bit_idx / 8;uint32_t bit_odd = bit_idx % 8;if(value){btmp->bits[byte_idx] |= (BITMAP_MASK << bit_odd);}else{btmp->bits[byte_idx] &= ~(BITMAP_MASK << bit_odd);}
}

至此为止,我们为物理内存建立了管理结构,下一步就是虚拟内存了。

内存管理

对于所有任务来说,它们都有4GB大小的虚拟地址空间,这个也是需要管理起来的。这就需要我们有一个虚拟内存池:

struct virtual_addr
{struct bitmap vaddr_bitmap; //虚拟地址用到的位图结构uint32_t vaddr_start;       //虚拟地址起始地址
};

当内核(用户进程)申请内存时,就从内核(用户进程)自己的虚拟内存池中分配虚拟地址,再从内核(用户)物理内存池中分配物理内存,之后再在页表中将这两种地址建立好映射关系.

内存管理初始化

根据这些理论,我们现在基本上对内存初始化理清了脉络:

  1. 计算总共有多少页的空间是可以用的
    //页表大小 = 1页的页目录表+第0和第769页目录项指向同一个页表+第769~1022目录项指向254个页表,//共256个页框uint32_t page_table_size = PG_SIZE * 256;       //记录内核所用的页目录项和页表所占用的字节大小uint32_t used_mem = page_table_size + 0x100000; //总共使用的内存,低端1M内存已经被存放内核实体了uint32_t free_mem = all_mem - used_mem;uint16_t all_free_pages = free_mem / PG_SIZE; //剩余多少页
  1. 分别计算内核和用户可以用多少内存空间
    //计算内核和用户分别所剩的空间大小uint16_t kernel_free_pages = all_free_pages / 2;uint16_t user_free_pages = all_free_pages - kernel_free_pages;
  1. 分别设置用户内存池和内核内存池的可用空间
    //内核内存池起始地址uint32_t kp_start = used_mem;//用户内存池起始地址uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE;
  1. 计算内核位图和用户进程位图分别所占的空间大小,之后初始化这两个内存池的元信息
    //kernel bitmap的长度,以字节为单位uint32_t kbm_length = kernel_free_pages / 8;//user bitmap长度,以字节为单位uint32_t ubm_length = user_free_pages / 8;//初始化元信息kernel_pool.phy_addr_start = kp_start;kernel_pool.pool_size = kernel_free_pages * PG_SIZE;kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length;user_pool.phy_addr_start = up_start;user_pool.pool_size = user_free_pages * PG_SIZE;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);
  1. 初始化内核和用户进程位图
    //位图置为0,内存初始化bitmap_init(&kernel_pool.pool_bitmap);bitmap_init(&user_pool.pool_bitmap);
  1. 初始化内核虚拟内存池的元信息
    //初始化内核虚拟地址的位图,按照实际物理内存大小生成数组//用于维护内核堆的虚拟地址,所以和内核内存池大小一致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);

这样在实际的内存中便会有如下的内存布局:

所以总结一下,内核低端1M物理内存空间(0x00 0000 ~ 0x0f ffff)的中,物理地址为0x9 a000地方开始存放的是kernel_pool的位图信息;物理地址为0x9a000+kbm_length的地方存放的是user_pool的位图信息;物理地址为0x9a000 + kbm_length + ubm_length的地方存放的kernel_vaddr的位图信息。

而物理内存0x10 0000 ~ 0x20 0000这个地方存放的就是页表信息啦,再往上就是我们可用的物理内存空间了。

内存的分配

初始化完成我们就开始涉及到内存分配的问题,让我们自顶向下的看一下内存分配的步骤,之后我们进行逐个剖析:

  1. 通过vaddr_get在虚拟内存池中申请一个虚拟地址
/*** @brief 从pf所代表的内存池(内核/用户)分配pg_cnt个内存块,并返回起始地址,其中会建立页表映射* @param pf 代表是内核内存池还是用户内存池* @param pg_cnt 要分配的内存块的数量* @return 分配的内存的起始地址,失败返回NULL*/
void *malloc_page(enum pool_flags pf, uint32_t pg_cnt)
{void *vaddr_start = vaddr_get(pf, pg_cnt);...
}
  1. 通过palloc在物理内存池中申请物理页
{....uint32_t cnt = pg_cnt;...while (cnt-- > 0){void *page_phyaddr = palloc(mem_pool);...}...
}
  1. 使用page_table_add函数建立虚拟地址到物理地址的映射
    uint32_t vaddr = (uint32_t)vaddr_start;uint32_t cnt = pg_cnt;struct pool *mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;while (cnt-- > 0){void *page_phyaddr = palloc(mem_pool);if (page_phyaddr == NULL){return NULL;}page_table_add((void *)vaddr, page_phyaddr);vaddr += PG_SIZE; //下一个虚拟页}

虚拟内存的申请

现在,我们深入探究一下这三个步骤的过程,首先就是如何如申请虚拟内存。

  1. 确定是在内核虚拟内存池申请还是在用户虚拟内存池申请,确定这个的原因是因为我们首先要把位图控制信息对应位置为1
  2. 返回虚拟地址,虚拟地址计算公示为:vaddr = vaddr_start+4K*bit_index_start
/*** @brief 根据标记pf从内核/用户内存空间中得到pg_cnt块内存* @param pf pool_flags结构体,用于标记是内核空间还是用户空间* @param pg_cnt 内存块数量* @return 得到内存块集合的起始地址*/
static void *vaddr_get(enum pool_flags pf, uint32_t pg_cnt)
{int vaddr_start = 0;int bit_idx_start = -1;uint32_t count = 0;//内核内存池中申请空间if (pf == PF_KERNEL){bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt);...//将位图置为使用状态while (count < pg_cnt){bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + count++, 1);}vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;}else //用户进程池中申请内存{struct task_struct *current_thread = running_thread();bit_idx_start = bitmap_scan(&current_thread->userprog_vaddr.vaddr_bitmap, pg_cnt);...while (count < pg_cnt){//内存位图置为被使用bitmap_set(&current_thread->userprog_vaddr.vaddr_bitmap, bit_idx_start + count++, 1);}//获得起始地址vaddr_start = current_thread->userprog_vaddr.vaddr_start + bit_idx_start * PG_SIZE;//保证不进入内核区域ASSERT((uint32_t)vaddr_start < (0xc0000000 - PG_SIZE));}return (void *)vaddr_start;
}

物理内存的申请

获取物理内存也是同样的道理,我们传入要申请的物理内存池的标记m_pool

  1. 扫描位图中满足条件位,得到位的起始下标:
    int bit_idx = bitmap_scan(&m_pool->pool_bitmap, 1); //找到一个未使用的物理页bitmap_set(&m_pool->pool_bitmap, bit_idx, 1); //将此位置为1
  1. 根据位的下标,我们就可以计算出物理内存的起始位置了:
    uint32_t page_phyaddr = ((bit_idx * PG_SIZE) + m_pool->phy_addr_start);

之后返回这个物理内存地址就可以了

建立物理地址和虚拟地址的映射

其实就是写入页表的过程:

  1. 得到页目录项和页表项的地址
    //得到页表项和页表的地址uint32_t *pde = pde_ptr(vaddr);uint32_t *pte = pte_ptr(vaddr);
  1. 判断此目录项是否在内存中,在的话就直接将物理地址写入页表就可以
 *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
  1. 页目录项不在的话,我们就需要申请页目录项内存,之后建立映射
    //不存在就申请页目录项内存,建立映射uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool);*pde = (pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1);//清空页表项memset((void *)((int)pte & 0xfffff000), 0, PG_SIZE);ASSERT(!(*pte & 0x00000001));*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);

内存回收

内存的回收和内存的分配是两个相反的过程,主要步骤如下:

  1. 调用addr_v2p函数,通过传入一个vaddr获得一个物理地址:
    pg_phyaddr = addr_v2p(vaddr); //获得物理地址
  1. 调用pfree清空物理地址位图中的相应位
 pfree(pg_phyaddr);
  1. 再调用page_table_pte_remove删除页表
 page_table_pte_remove(vaddr);
  1. 清除虚拟地址位图中的相应位
 vaddr_remove(pf, vaddr_, pg_cnt);

根据虚拟地址获得物理地址

这一步其实就是一个查页表的过程,还记得我们之前介绍的对于一个虚拟地址来说,其前10位标识页目录项信息,中间十位标识页表信息,最后12位就是物理地址在页表中的偏移地址信息。这样我们就可以直接(*pte & 0xfffff000) + (vaddr & 0x00000fff)便是实际物理地址了。

/*** @brief 通过传入一个虚拟地址,查表得到它的物理地址* @param vaddr 虚拟地址* @return 物理地址*/
uint32_t addr_v2p(uint32_t vaddr)
{uint32_t *pte = pte_ptr(vaddr);//物理页起始地址+物理页内偏移量return ((*pte & 0xfffff000) + (vaddr & 0x00000fff));
}

清除内核物理位图

这个过程中我们传入的是物理地址,首先根据物理地址,计算页在位图中的下标,其次把这个位置为1就可以了

/*** @brief 将物理地址pg_phyaddr回收到物理内存池* @param pg_phyaddr 物理地址*/
void pfree(uint32_t pg_phyaddr)
{struct pool *mem_pool;uint32_t bit_idx = 0;if (pg_phyaddr >= user_pool.phy_addr_start){//用户物理内存池mem_pool = &user_pool;//得到是第几个页bit_idx = (pg_phyaddr - user_pool.phy_addr_start) / PG_SIZE;}else{//内核物理内存池mem_pool = &kernel_pool;bit_idx = (pg_phyaddr - kernel_pool.phy_addr_start) / PG_SIZE;}bitmap_set(&mem_pool->pool_bitmap, bit_idx, 0);
}

删除页表

删除页表的本质则是,将页表中的P位置为0,标识其不在内存中,之后再重新更新页表高速缓存就可以了。

/*** @brief 去掉页表中vaddr的映射,只去掉pte就行* @param vaddr 虚拟地址*/
static void page_table_pte_remove(uint32_t vaddr)
{uint32_t *pte = pte_ptr(vaddr);*pte &= ~PG_P_1; //页表项pte的p位置为0//下面的命令更新页表高速缓存tlb,这里只用更新vaddr对应的页表项就可以了asm volatile("invlpg %0" ::"m"(vaddr): "memory");
}

清除虚拟位图

这个过程和清除物理内存位图是一样的。

/*** @brief 在虚拟地址池中释放vaddr起始的pg_cnt个虚拟页地址* @param pf 标记是内核还是用户虚拟地址池* @param vaddr_  起始地址* @param pg_cnt 虚拟地址页的个数*/
static void vaddr_remove(enum pool_flags pf, void *vaddr_, uint32_t pg_cnt)
{uint32_t bit_idx_start = 0;uint32_t vaddr = (uint32_t)vaddr_;uint32_t cnt = 0;if (pf == PF_KERNEL){//移出内核的pte//得到具体位图信息bit_idx_start = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;while (cnt < pg_cnt){//位图置0,表示释放内存bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0);}}else{//用户虚拟内存池struct task_struct *current_thread = running_thread();bit_idx_start = (vaddr - current_thread->userprog_vaddr.vaddr_start) / PG_SIZE;while (cnt < pg_cnt){//位图置0,表示释放内存bitmap_set(&current_thread->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0);}}
}

参考文献

[1] 操作系统真相还原

Gos ——内存管理系统相关推荐

  1. FFmpeg源码分析:内存管理系统

    FFmpeg有专门的内存管理系统,包括:内存分配.内存拷贝.内存释放.其中内存分配包含分配内存与对齐.内存分配与清零.分配指定大小的内存块.重新分配内存块.快速分配内存.分配指定最大值的内存.分配数组 ...

  2. 《操作系统-真象还原》08. 内存管理系统

    文章目录 实现 ASSERT 断言 实现开.关中断的函数 实现 ASSERT \_\_VA_ARGS\_\_ 测试断言 位图 bitmap 及其函数的实现 位图简介 位图的定义与实现 内存管理系统 内 ...

  3. 操作系统真象还原第8章:内存管理系统

    8.1makefile简介 虽则代码模块越来越多,靠自己一个个编译代码和链接文件无疑是一件十分不讨好的事情,于是有了makefile来帮助我们 其基本规则如下 xxx.bin:xxx.ogcc xxx ...

  4. PHP基础——’tasklist‘内存管理系统

    功能: 1. 利用tasklist获取进程列表,保存进数据库. 2. 进程列表显示,表格排版.点击某列表头能够按该列排序. 3. 组合查询:可按映像名称.会话名模糊搜索,内存使用范围搜索. 内存管理系 ...

  5. 垃圾回收 内存管理 python

    20220225 https://mp.weixin.qq.com/s/94SmSNEkwmz-Eu-hBUo0Lg Python的内存管理机制 在windows 中直接在任务管理其中关掉python ...

  6. 深入理解Java虚拟机——第二章——Java内存区域与内存溢出异常

    运行时数据区域 Java虚拟机运行时数据区域 程序计数器 程序计数器可以看做是当前线程所执行的字节码的行号指示器.字节码解释器工作时就是通过改变这个计数器的值来选取下一条所需要执行的字节码指令,分支. ...

  7. python基于值得内存_为什么说Python采用的是基于值的内存管理模式

    匿名用户 1级 2018-01-31 回答 先从较浅的层面来说,Python的内存管理机制可以从三个方面来讲 (1)垃圾回收 (2)引用计数 (3)内存池机制 一.垃圾回收: python不像C++, ...

  8. [二]Java虚拟机 jvm内存结构 运行时数据内存 class文件与jvm内存结构的映射 jvm数据类型 虚拟机栈 方法区 堆 含义...

    前言简介 class文件是源代码经过编译后的一种平台中立的格式 里面包含了虚拟机运行所需要的所有信息,相当于 JVM的机器语言 JVM全称是Java Virtual Machine  ,既然是虚拟机, ...

  9. .net内存管理与指针

    本人前段时间准备做个TIN三角网的程序,思想是是分割合并法,分割的同时建立平衡二叉树,然后子树建三角网并相互合并,再向上加入父亲的点集.由于我对.net语言熟点,就准备用c#语言实现.但是不知从那听过 ...

  10. 内核中的内存申请:kmalloc、vmalloc、kzalloc、kcalloc、get_free_pages【转】

    转自:http://www.cnblogs.com/yfz0/p/5829443.html 在内核模块中申请分配内存需要使用内核中的专用API:kmalloc.vmalloc.kzalloc.kcal ...

最新文章

  1. 《Effective C#》读书笔记——条目11:理解短小方法的优势C#语言习惯
  2. 【Linux】26_文件服务FTP Server
  3. java 异常_Java学习——异常与异常处理
  4. 好的英文视频照片网站
  5. 2021年的最后7天,和我的伙伴们合个影吧
  6. java中Mark接口_JVM源码分析之Java对象头实现
  7. fritz 使用手册_Fritz对象检测指南:使用机器学习在Android中构建宠物监控应用
  8. win10 VScode配置GCC(MinGW)
  9. 矩阵 II : 线性组的线性相关性
  10. 作为一个上市公司HR,跟大家分享一些面试的真相
  11. MySQL中实现并、交、差
  12. Python分布式爬虫1
  13. stderr和stdout详细解说
  14. 数据库的水平扩展与垂直扩展
  15. Apache Flink 在斗鱼的应用与实践
  16. 基于brctl工具搭建网桥
  17. 计算机二级考试主要学什么,计算机二级考试需要学习什么内容
  18. 全网多种方法解决未连接到互联网 代理服务器出现问题,或者地址有误的错误
  19. 云主机是什么?可以用来干嘛?
  20. 最简单的单层神经网络实现鸢尾花分类

热门文章

  1. MMO游戏设计一:角色行走
  2. 白泽六足机器人_ros_v1——单腿RVIZ仿真
  3. 笔记本电脑桌面不显示计算机,笔记本电脑屏幕不显示怎么回事
  4. 计算机视觉之图像分类
  5. C语言switch语句无break
  6. 大饼趋势逐渐明朗,黎明就在眼前!
  7. kali源代码简单说明
  8. Android、IOS和Java三个平台一致的加密工具
  9. idea-2017破解教程
  10. ios模拟器装ipa包_用iOS模拟器安装App的方法