实验为IPADS《现代操作系统原理与实现》配套实验,慕课上有完整的课程教学视频,以下为个人实验过程和踩坑记录。

物理内存布局

当bootloader和内核被加载到内存中的时候,物理内存到底是如何布局的?

首先是bootloader和内核的加载,起始地址img_startimg_end是在链接脚本里面指定的(script/linker-aarch64.lds.in),这一段由于是加载的镜像,因此是不能作为空闲物理内存被分配给应用的。在kernel/mm.c中,变量free_mem_start就是img_end之后的第一个页的起始地址,即真正可以被使用的空闲物理地址的开头。将mm_init中的kdebug全部改成kinfo可以打印出这个具体的值:​

打印物理内存布局

chcore定义可以分配给程序使用的物理内存为从0xffffff0001800000(24M)开始,到0xffffff0020c00000结束的一段连续空间(在mm.c开头的宏定义中由NPAGES*BUDDY_PAGE_SIZE定义,共500M)。可以发现在地址0xffffff0001800000img_end中间还空出来一部分区域,这部分区域是用来存储元信息的。

为了管理500M的物理内存,chcore采用了伙伴系统,伙伴系统为了管理物理内存,把内存分成了一个一个大的块(页,这里的页不是虚拟内存,只是一块连续物理内存的意思),而其元信息就存储在这空出来的区域。

mm.c中定义的变量page_meta_start就是第一个页面元信息的起始地址,real_start_vadd是第一个物理页的起始地址,即当前的物理内存布局如下:(注意在后续的实验中由于内核镜像变大,具体地址会发生改变,但是相对顺序还是一致的)​

物理内存布局

另外这里可以频繁看到phys_to_virt这个宏的使用:

#define phys_to_virt(x) ((vaddr_t)((paddr_t)(x) + KBASE))
#define virt_to_phys(x) ((paddr_t)((vaddr_t)(x) - KBASE))

这是因为初始化的时候在init_c.c中已经开启了MMU,因此内核代码使用的所有地址都会被看成是虚拟地址,然后被MMU通过页表进行地址转化。譬如我们想让page_meta_start能指向img_end,但是如果我们直接把img_end赋值给page_meta_start,这个地址通过页表转化之后实际指向的物理地址就不再是我们想要的地址了。因此这里需要把物理地址转化成虚拟地址在赋值,这样内核代码在用到这个虚拟地址的时候,MMU就会通过页表转化成物理地址,这个物理地址就是img_end

观察phys_to_virt可以发现这里其实只是加了一个偏移量KBASE,这是因为内核页表在初始化的时候只是将物理地址加上这个偏移量映射到虚拟地址,具体的映射在boot/mmu.c中完成。在实验手册中已经给出了映射的结果:

内核虚拟空间映射对应到代码是如下这段:

/** TTBR1_EL1 0-1G* KERNEL_VADDR: L0 pte index: 510; L1 pte index: 0; L2 pte index: 0.*/
kva = KERNEL_VADDR;
boot_ttbr1_l0[GET_L0_INDEX(kva)] = ((u64) boot_ttbr1_l1)| IS_TABLE | IS_VALID;
boot_ttbr1_l1[GET_L1_INDEX(kva)] = ((u64) boot_ttbr1_l2)| IS_TABLE | IS_VALID;start_entry_idx = GET_L2_INDEX(kva);
/* Note: assert(start_entry_idx == 0) */
end_entry_idx = start_entry_idx + PHYSMEM_BOOT_END / SIZE_2M;
/* Note: assert(end_entry_idx < PTP_ENTIRES) */

截止这里完成了0级和1级页表的初始化,接下来通过2级页表直接映射2M页(多级页表和2M页等内容后面虚拟地址管理部分还会有解释)。

/** Map each 2M page* Usuable memory: PHYSMEM_START ~ PERIPHERAL_BASE*/
for (idx = start_entry_idx; idx < end_entry_idx; ++idx) {boot_ttbr1_l2[idx] = (PHYSMEM_START + idx * SIZE_2M)| UXN  /* Unprivileged execute never */| ACCESSED  /* Set access flag */| INNER_SHARABLE   /* Sharebility */| NORMAL_MEMORY    /* Normal memory */| IS_VALID;
}

截止这里完成了0-256M的映射,虚拟地址起始为kva,通过宏KERNEL_VADDR来定义,值等于KBASE,因此对应到上面的表格的第三行。

/* Peripheral memory: PERIPHERAL_BASE ~ PHYSMEM_END */
start_entry_idx = start_entry_idx + PERIPHERAL_BASE / SIZE_2M;
end_entry_idx = PHYSMEM_END / SIZE_2M;/* Map each 2M page */
for (idx = start_entry_idx; idx < end_entry_idx; ++idx) {boot_ttbr1_l2[idx] = (PHYSMEM_START + idx * SIZE_2M)| UXN  /* Unprivileged execute never */| ACCESSED  /* Set access flag */| DEVICE_MEMORY    /* Device memory */| IS_VALID;
}

截止这里这里完成了512M-1G的物理地址空间映射,页粒度仍然为2M。对应上面表格第一行的一部分,可以看到此时256M-512M是没有被映射的,因此上面表格第二行是内核空洞。

/** Local peripherals, e.g., ARM timer, IRQs, and mailboxes** 0x4000_0000 .. 0xFFFF_FFFF* 1G is enough. Map 1G page here.*/
kva = KERNEL_VADDR + PHYSMEM_END;
boot_ttbr1_l1[GET_L1_INDEX(kva)] = PHYSMEM_END | UXN   /* Unprivileged execute never */| ACCESSED      /* Set access flag */| DEVICE_MEMORY    /* Device memory */| IS_VALID;

最后再映射1G的页面(1级页表中页面块的大小为1G),只要填写一项即可完成1G-2G物理地址空间的映射,这里页粒度是1G。(所以这里2G-4G好像是没有被映射的,不知道为什么手册中表格第一行是512M-4G)。

上面涉及虚拟内存管理部分后面还有篇幅会解释,这里主要是理解为什么需要有vaddr_tpaddr_t以及他们之间的转化。实际上内核不需要页表也是知道如何将虚拟地址转化为实际物理地址的,只要通过加上或者减去KBASE即可,因此在需要得到实际物理内存的时候,直接通过上述两个宏就可以互转。

同时注意如果是paddr_t是不能作为指针来解引用的,因为这个地址是实际物理地址,通过MMU转化必然会出错(差了一个KBASE偏移量)。实际上paddr_tvaddr_t都是u64类型,只是在可读性上做一个区分。

在定义free_mem_start的时候就用到了物理地址到虚拟地址的转化,img_end是在链接脚本中赋值的,指向的是镜像的结尾,这里要转化成内核可用的虚拟地址再赋值给free_mem_start

free_mem_start = phys_to_virt(ROUND_UP((vaddr_t) (&img_end), PAGE_SIZE));

这里应该是书写有点错误,img_end类型应该是paddr_t,不过因为这里都是u64所以正确性上没有问题。ROUND_UP是保证了地址对齐。同时这里对链接脚本中定义的变量img_end的使用方法具体可以参考:https://sourceware.org/binutils/docs/ld/Source-Code-Reference.html

伙伴系统

可参考:https://en.wikipedia.org/wiki/Buddy_memory_allocation

从直觉上可以用一个简单例子理解一下:

假设现在有一块1G的空闲内存,然后此时来了一个请求需要80M,如果直接把1G给出去就太浪费了,因此先对半分一下,512M,还是太大,再对半分一下,128M,还是太大,再分一下,64M,太小了不能满足需求,因此还是用128M来响应这个需求,此时内存就会像下面这样:

文件结构

其中块A会被分配给应用,块A和块B互为伙伴。当块A被释放的时候,首先他会找到他的伙伴块B,然后发现块B是空闲的,就和他合并成512M的块D,然后再找到D的伙伴块C,发现C是空闲的,就再合并成1G的大块。

从分割的过程可以发现,伙伴是连续的两块相同大小的连续内存块,因此实际上从地址上看,块的起始地址只是一个bit不一样,而且这个bit所在的位置就是块的大小决定的,例如假设D的起始地址为0,那么C的起始地址就为0x20000000,即为0 + 块大小512M,因此块大小决定了相邻两伙伴的起始地址到底哪一位不一样。也就是说,当我们有了一个块的起始地址和他的大小,我们可以直接得到他的伙伴。在mm/buddy.c中也是这么计算的:

static struct page *get_buddy_chunk(struct phys_mem_pool *pool,struct page *chunk)
{u64 chunk_addr;u64 buddy_chunk_addr;int order;/* Get the address of the chunk. */chunk_addr = (u64) page_to_virt(pool, chunk);order = chunk->order;/** Calculate the address of the buddy chunk according to the address* relationship between buddies.*/
#define BUDDY_PAGE_SIZE_ORDER (12)buddy_chunk_addr = chunk_addr ^(1UL << (order + BUDDY_PAGE_SIZE_ORDER));/* Check whether the buddy_chunk_addr belongs to pool. */if ((buddy_chunk_addr < pool->pool_start_addr) ||(buddy_chunk_addr >= (pool->pool_start_addr +pool->pool_mem_size))) {return NULL;}return virt_to_page(pool, (void *)buddy_chunk_addr);
}

首先我们得到当前块的起始地址,然后计算块的大小。在计算块大小的时候,order表示阶,即2的(order+BUDDY_PAGE_SIZE_ORDER)次幂为块的大小。这里BUDDY_PAGE_SIZE_ORDER是12,也就是一个块最小为4K,order==0,那么这个块就是4K,若order==1,那么这个块就是8K,以此类推。当我们得到块的大小的时候,做一个异或操作,就可以对当前块的某一位取反(0变成1,1变成0,这是异或运算符的结果,可以分类讨论得出),此时得到的结果就是伙伴块的起始地址。同时注意这里伙伴块是可能超出管理地址范围的,因此最后要判断一下是否还在可用的物理空间范围之内。

上面这样我们基本就可以实现内存的管理,但是考虑实现还有一个小问题,order存储在哪里?这个时候就要用到前面说的页面元信息。我们把整个物理内存分成连续的4K页(也就是order为0的最小块内存),对应每一个页,在页面元信息部分存储其元信息,包括是否被分配,order阶信息等:

/* `struct page` is the metadata of one physical 4k page. */
struct page {/* Free list */struct list_head node;/* Whether the correspond physical page is free now. */int allocated;/* The order of the memory chunck that this page belongs to. */int order;/* Used for ChCore slab allocator. */void *slab;
};

因此上面函数中的virt_to_pagepage_to_virt就可以对页和页的元信息进行互相转化:

struct page *virt_to_page(struct phys_mem_pool *pool, void *addr)
{struct page *page;page = pool->page_metadata +(((u64) addr - pool->pool_start_addr) / BUDDY_PAGE_SIZE);return page;
}

这里就是计算这个块是从头开始第几个块,对应取出从头开始的第几个元信息。

另外在实现上,为了效率考虑,将大小相同的块用链表管理起来,这样查找的话直接去对应大小的链表查找有没有空闲块即可。用一个简单例子从直觉上理解一下:

假设现在维护了10条链表,第一条是order为0的空闲块(大小为4K),第二条是order为1的空闲块(大小为8K),以此类推,最后一条链表为order为9的空闲块,当我需要一个10K的内存时,只要从第三条链表开始查找(order为2,大小为16K),如果该链表为空,表示没有16K大小的空闲块,此时一直往上,直到找到一块空闲的块,或是都无空闲块,即为空闲内存不能满足需求为止。

实现

有几个待实现的函数:

void buddy_free_pages(struct phys_mem_pool *pool, struct page *page)
static struct page *merge_page(struct phys_mem_pool *pool, struct page *page)
struct page *buddy_get_pages(struct phys_mem_pool *pool, u64 order)
static struct page *split_page(struct phys_mem_pool *pool, u64 order, struct page *page)

free就是将一个已经被分配的块放回空闲链表,放回之后调用merge_page进行合并,这个过程显然是递归的。

get的时候先是一个查找的过程,当找到一个块的时候,有可能这个块很大,需要split,割成最小的可以满足需求的块(即最小满足需求的order),这个过程显然也是递归的。

/** split_page: split the memory block into two smaller sub-block, whose order* is half of the origin page.* pool @ physical memory structure reserved in the kernel* order @ order for origin page block* page @ splitted page* * Hints: don't forget to substract the free page number for the corresponding free_list.* you can invoke split_page recursively until the given page can not be splitted into two* smaller sub-pages.*/
static struct page *split_page(struct phys_mem_pool *pool, u64 order,struct page *page)
{// <lab2>if(order == page->order)return page;/* delete the page need to be split */list_del(&page->node);pool->free_lists[page->order].nr_free--;struct page *head = page;int half_order = head->order-1;/* spilt the page into two equal parts *//* first part, modify the order */for(int i = 0; i < (1 << half_order); i++){head->order = half_order;head++;}list_add(&page->node, &pool->free_lists[page->order].free_list);pool->free_lists[page->order].nr_free++;/* second part */head->order = half_order;list_add(&head->node, &pool->free_lists[head->order].free_list);pool->free_lists[head->order].nr_free++;/* modify the order */for(int i = 0; i < (1 << half_order); i++){head->order = half_order;head++;}/* continue to split the first part */return split_page(pool, order, page);// </lab2>
}/** buddy_get_pages: get free page from buddy system.* pool @ physical memory structure reserved in the kernel* order @ get the (1<<order) continous pages from the buddy system* * Hints: Find the corresonding free_list which can allocate 1<<order* continuous pages and don't forget to split the list node after allocation   */
struct page *buddy_get_pages(struct phys_mem_pool *pool, u64 order)
{// <lab2>/* find the first index of free_lists whose size larger than the (1 << order) */int list_index = order;while(list_index < BUDDY_MAX_ORDER && pool->free_lists[list_index].nr_free == 0){list_index++;}/* not found */if(list_index == BUDDY_MAX_ORDER)return NULL;/* get the page based on the list node */struct page *page = list_entry(pool->free_lists[list_index].free_list.next, struct page, node);/* split the page to best fit the size */page = split_page(pool, order, page);/* delete from the list and modify the nr_free field */list_del(&page->node);pool->free_lists[page->order].nr_free--;/* modify the allocated flag */struct page *head = page;for(int i = 0; i < (1 << page->order); i++){head->allocated = 1;head++;}return page;// </lab2>
}/** merge_page: merge the given page with the buddy page* pool @ physical memory structure reserved in the kernel* page @ merged page (attempted)* * Hints: you can invoke the merge_page recursively until* there is not corresponding buddy page. get_buddy_chunk* is helpful in this function.*/
static struct page *merge_page(struct phys_mem_pool *pool, struct page *page)
{// <lab2>struct page *buddy_chunk = get_buddy_chunk(pool, page);/* no need to merge */if(buddy_chunk == NULL || buddy_chunk->allocated==1 || page->order == BUDDY_MAX_ORDER-1|| buddy_chunk->order != page->order){return page;}/* merge *//* delete the buddy chunk from its free list */list_del(&buddy_chunk->node);pool->free_lists[buddy_chunk->order].nr_free--;/* delete the page chunk from its free list */list_del(&page->node);pool->free_lists[page->order].nr_free--;/* choose lower address to continue merge */if(buddy_chunk < page){buddy_chunk->order++;list_add(&buddy_chunk->node, &pool->free_lists[buddy_chunk->order].free_list);pool->free_lists[buddy_chunk->order].nr_free++;return merge_page(pool, buddy_chunk);} else {page->order++;list_add(&page->node, &pool->free_lists[page->order].free_list);pool->free_lists[page->order].nr_free++;return merge_page(pool, page);}// </lab2>
}/** buddy_free_pages: give back the pages to buddy system* pool @ physical memory structure reserved in the kernel* page @ free page structure* * Hints: you can invoke merge_page.*/
void buddy_free_pages(struct phys_mem_pool *pool, struct page *page)
{// <lab2>/* set the allocated flag */struct page *head = page;for(int i = 0; i < (1 << page->order); i++){head->allocated = 0;head++;}/* add to the free list */list_add(&page->node, &pool->free_lists[page->order].free_list);pool->free_lists[page->order].nr_free++;/* merge with other free blocks */head = merge_page(pool, page);/* modify the order field at one time, notice merge_page returned head pags's order is always correct */int order = head->order;for(int i = 0; i < (1 << order); i++){head->order = order;head++;}return;// </lab2>
}

操作系统实验_Chcore -- 上交IPADS操作系统银杏书配套Lab实验笔记 - Lab2内存管理(一)...相关推荐

  1. 单处理机系统的进程调度实验_Chcore -- 上交IPADS操作系统银杏书配套Lab实验笔记 - Lab3进程与异常(一)...

    实验为IPADS<现代操作系统原理与实现>配套实验,慕课上有完整的课程教学视频,以下为个人实验过程和踩坑记录. Capability 为什么需要有capability?用户进程是不能直接访 ...

  2. 操作系统概念学习笔记 15 内存管理(一)

    操作系统概念学习笔记 15 内存管理(一) 背景 内存是现代计算机运行的中心.内存有非常大一组字或字节组成,每一个字或字节都有它们自己的地址.CPU依据程序计数器(PC)的值从内存中提取指令.这些指令 ...

  3. 操作系统概念学习笔记 16 内存管理(二) 段页

    操作系统概念学习笔记 16 内存管理 (二) 分页(paging) 分页(paging)内存管理方案允许进程的物理地址空间可以使非连续的.分页避免了将不同大小的内存块匹配到交换空间上(前面叙述的内存管 ...

  4. 现代操作系统-原理与实现(下)【银杏书-读书笔记】

    上篇链接戳这里 目录 第7章-进程间通信 多进程协助的目的 进程间通信IPC 数据传递 基于共享内存的消息传递 操作系统辅助的消息传递 控制流转移 单向和双向 同步和异步 超时机制 通信连接管理 直接 ...

  5. MIT JOS lab2内存管理实验记录

    本次Lab主要完成JOS中关于虚拟内存映射.页表管理和分配的几个函数,旨在加深对内存管理的认识. 代码链接 https://download.csdn.net/download/qhaaha/1374 ...

  6. c语言上机实验指导西南交通大学,操作系统原理与应用实验指导书-西南交通大学.doc...

    操作系统原理与应用实验指导书-西南交通大学 <操作系统原理与应用> 实验指导书 西南交通大学经济管理学院 电子商务与信息管理系 王明亮编写 2007年7月 实验名称:1. 安装Linux操 ...

  7. 计算机系统结构实验报告Linux,计算机操作系统体系结构实验报告.doc

    操作系统实验报告 实验目的: 随着操作系统应用领域的扩大,以及操作系统硬件平台的多样化,操作系统的体系结构和开发方式都在不断更新,目前通用机上常见操作系统的体系结构有如下几种:模块组合结构.层次结构. ...

  8. 操作系统原理实验(5):内存管理

    一.实验目的 分页内存管理是内存管理的基本方法之一.本实验的目的在于全面理解分页式内存管理的基本方法以及访问页表,完成地址转换等的方法. 二.实验过程&错误 内容(一):设计不同的方式引发页错 ...

  9. 实验三:Windows7操作系统安全

    目录 一.实验目的及要求 二.实验原理 三.实验环境 四.实验步骤及内容 4.1账户与口令 4.1.1删除不再使用的口令 4.1.2启用账户策略. 4.1.3用户和用户组权限管理. 4.2审核与日志 ...

最新文章

  1. Linux系统守护进程详解
  2. 合肥php开发培训费用,合肥PHP开发培训之PHP文件基础操作
  3. Python3.9又更新了:dict内置新功能,正式版十月见面
  4. 骁龙660_高通骁龙660可以带动6g运行内存吗?
  5. Zookeeper选举算法( FastLeader选主)
  6. 零基础应该先学习 java、php、前端 还是 python?
  7. spark基础之spark sql运行原理和架构
  8. 为numpy数组增加一个维度的方法
  9. 计算机应用教程卢湘鸿,计算机应用教程
  10. pythonqt5教程从零开始_pyQt5 QtDesigner 简易入门教程
  11. AVOD、SVOD、TVOD、PVOD:视频点播商业模式
  12. JNCIS翻译文档之------接口2
  13. JS实现视频录制-以Cesium为例
  14. 磁带备份迁移到磁盘备份前的准备工作
  15. Appium 自动化测试 H5页面元素定位
  16. 卷积网络中的通道(channel)和特征图(feature map)
  17. 中国科学技术大学课程资源 最新github地址
  18. 全球及中国镀银铜纳米粒子行业供需预测与发展格局分析报告2021~2026年
  19. 思科——通往新商道的金桥
  20. 京东又一高管辞职 隆雨辞去京东集团首席法务官职务

热门文章

  1. python安装盒怎么打开_安装MySQL-python报错
  2. game module 停止运行_恒温摇床长时间运行的注意事项
  3. 软件测试报告doc,软件测试报告.doc
  4. android 广播 event,无法接收android.intent.action.EVENT_REMINDER广播
  5. java motherfree video_Java Config 下的Spring Test方式
  6. Windows 与 Linux 通过Xshell 文件互传
  7. 如何解决ValueError: unknown is not supported
  8. 网站开发中敏感信息加密
  9. 第三:Python发送邮件时中文附件下载乱码
  10. html大学生活主题班会,我的大学生活主题班会策划书