OS——内存管理+程序加载
内存的管理和使用
- 内存是被分成一个个4KB大小的块来管理和使用的,每个块可以叫做一个页框(或者页帧)。
- 内存与磁盘进行读写交互都是按照页框为单位。即使此次要读或者写的数据不够4KB,也是按照4KB进行读写(可能会多读或多写,但由于局部性原理,这也无伤大雅)。加载完所有代码后发现最后一个页框的一部分没有被使用,它也不能被别人使用,浪费就浪费了(这就造成了内存内碎片)
- 所有的页框(内存块),都被一个struct page结构体进行描述,所有的struct page结构体被一个数组组织起来统一管理,所以我们对内存块的操作就转换成了对这个数组的增删改查。(比如要申请一个空闲内存块,就遍历这个数组看看哪个struct page中的标记位是未被使用)
- 比较巧妙的是,数组中的每一个struct page对应的下标就是这个struct page所管理的内存块在整个内存中的偏移量。利用这一点,我们可以发现:通过struct page,我们可以用其在数组中的下标*4KB来找到它所管理的内存块对应的起始地址;通过内存地址/4KB可以得到页框号(对应的内存块),再把页框号当作下标就可以直接找到该内存块对应的struct page管理结构。page知道自己管理那块,通过地址也可以找到管理者以及块首地址,整个过程都是O(1)的时间复杂度,非常高效。
综上,内存分块使用,被数据结构管理,会导致内存内碎片问题。一个文件缓冲区实际上就是一个struct page链表,而释放一个内存就是修改struct page的标志位而已。
真正的页表
我们都知道,虚拟地址空间与实际内存空间的映射关系是记录在页表上的,但实际上这种映射不是逐字节映射的,而是逐内存框映射。
先来看一下如果逐字节映射会出现什么情况:
实际上,虚拟地址逻辑上也被分块,所以页表只是做页框到页框之间的映射:
页表的大小最大是4MB,但是大概率根本用不到后面的众多页表,也就不会开空间,所以实际上是远远小于4MB的。
初次之外,页表中还有很多的标志位:
写时拷贝的原理就是把子进程的页表项的标志位都改成只读,这样子进程在修改的时候就会发生错误,然后陷入软中断,由OS重新申请内存并映射到子进程页表上。
缺页异常(懒加载)的原理也是如此,发现是否命中为否就会触发软中断,将数据加载到内存上。
越界不一定会报错,有可能判断的时候虚拟地址恰好是在合法的范围内,这时就把它当成了缺页中断,加载后访问了随机内容。
内存外碎片以及读内存效率问题的解决
虚拟地址空间可以让逻辑上连续的代码和数据映射到不同的地址处,这样一来就不会出现两个内存块中间的内存块由于太小分配不出去而导致的内存内碎片问题。
不过如果把一个变量拆分存放到两个页框,这样的话对于CPU读取来说需要读两次,还需要切分拼接等操作,效率会变低。因此在存放数据时进行内存对齐实际上避免了将一个变量存储在两个不同的页框中的情况,也就提高了CPU读内存的效率。内存对齐实际上是一种以空间换取时间的做法。
程序加载
- 当我们点击运行一个程序的时候,OS创建一个进程PCB,当前OS知道这个程序的存放位置(通过环境变量或者命令行的当前工作目录),所以OS会把可执行文件的全部或者部分内容加载到内存中,并将内存地址与PCB的虚拟地址空间建立映射。
- 接着OS把形成的页目录表的地址放到CR3寄存器中,以供MMU转换虚拟地址使用。
- 由于可执行文件是ELF格式,CPU可以从文件特定位置取出main函数的地址放到PC寄存器,至此程序就可以跑起来了。OS也根据这种ELF格式给不同的代码或数据设置访问权限。
综上,程序的运行,和硬件,OS以及文件本身的格式都有关系
