操作系统 --- 内存管理
地址空间 (Address space)
物理地址空间 (physical address space)
什么是物理地址空间
- 物理内存就是计算机内存条上的实际内存,计算机物理内存的大小是固定的,而物理地址就是物理内存中内存的实际地址
- cpu可以直接进行对物理内存寻址,但是寻址的空间取决于cpu地址总线(Address Bus)的数量
- 如果CPU有32根地址线,则最大寻址空间为2^32 = 4GB
- 也就是支持最大内存为4GB, 超过4GB的内存是没有用的
进程直接使用物理内存的缺点
- 当多个程序需要运行时,必须保证这些程序用到的内存总量要小于计算机实际的物理内存的大小。
- 内存使用效率低,内存空间不足,就需要将其他程序展示拷贝到硬盘当中,然后将新的程序装入内存。然而由于大量的数据装入装出,内存的使用效率会非常低
- 进程地址空间不隔离,由于程序是直接访问物理内存的,所以每一个进程都可以修改其他进程的内存数据,设置修改内核地址空间中的数据,所以有些恶意程序可以随意修改别的进程,就会造成一些破坏
逻辑地址空间(logical address space)
- 每个进程的代码都有一个地址,比如在C中用取地址操作符输出的地址结果
- 但是这个地址不对应真正的物理地址, 而是针对这个进程生成的逻辑地址或者叫虚拟地址需要CPU转换成真正的物理地址
虚拟内存 (virtual memory)
什么是虚拟内存
- 为了解决直接使用物理内存的缺点,就出现了虚拟内存
- 虚拟内存: 每个进程都运行在属于自己的内存沙盘中, 这个沙盘就是虚拟内存
- 虚拟内存的大小取决于操作系统的位数,32位操作系统最多支持4GB的虚拟内存
- 虚拟内存不代表会给每个正在运行的进程分配4GB的空间,而是每个进程可以映射4GB的地址空间, 也就是每个进程可以使用4GB的地址空间中任何的地址,所以每个进程真正使用的物理空间其实很小
- 虚拟内存对应的实际物理空间可以在内存中也可以在硬盘中,也就是逻辑地址可以对应内存或者硬盘
- 下图就是进程的虚拟空间的具体分配
关于内存大小
- 内存大小取决于 Min(CPU地址线的数量,操作系统的位数,内存的实际大小)
地址转换 (Address binding)
- 逻辑地址可以在程序运行的不同阶段被转换为物理地址
- 具体分为static binding和dynamic binding
Development binding
- 在开发阶段就使用物理地址
- 可以减少地址转换产生的延时
- 只在很少的情况下使用,如OS的最底层或者在real-time,embedded system中使用
Compiler time binding
- the compiler is given the starting physical address of the program
- 但是程序只能加载进预设好的内存空间中, 并且 程序运行的时候并不能保证这块内存仍然还是空闲的
Link time binding
- linker给程序分配内存的开始地址,然后loader将load module拷贝进内存
- 但是CPU是多进程执行的,假设进程一存放在内存以1000开始的位置,可能某一时刻进程一阻塞了,而且时间还不短,这时候如果再将其放在内存里面肯定是不合理的,因此会将进程一换出到磁盘,1000开始的这个位置变成了空闲区,可能被其他进程占用了,下次进程一换入的时候可能就是在2000这个位置了,但是如果是载入时重定位就意味着在解释地址的时候还是以1000为基址
Execution time binding
- 在CPU抓取instruction的时候转换逻辑地址
- 完成地址转换的是MMU (Memory Management Unit)
- MMU中Relocation Register储存着base address, base address可以变化
- 在context switch时RR会被储存进Process Control Block
内存分区 (memory partition)
- 内存被分为User Program和OS两部分
Fixed partition
- 在系统启动时, 用户区被分为不用的partition
- 每个partition的大小可以相等也可以不相等
- 所以partition的大小以及如何将进程分配给partition非常影响系统的性能
分配方式一: multiple queues
- 每个partition有一个queue
- 一个进程会被分配给the smallest partition that satisfies its memory requirements
- 问题一: heap和stack的大小会不断增长, 导致目前的partition大小不够需要移动
- 问题二: 可能有些partition永远不会被使用,因为进程会被放进最小的符合内存大小的partition的queue
分配方式二: single queue
- 进程被分配给最小的可用的partition
- 问题一: 进程的大小受限制于最大的partition(也是fix partition的缺点)
- 问题二: 总有一些internal fragmens会浪费掉
Variable partition
- Partiton的大小在运行时根据进程的大小决定
External fragments
- 当一些进程终止之后会留下一些未使用的内存(holes),也叫external fragments
LinkedList approach
- 用linked list把external fragment串起来
- 每一个block两个tag以及两个指针
- 两个tag: block的大小, 这个block是否是external fragment
- 两个指针: next free block, previous free block
- 当一个block被释放之后,要检查旁边的block是否是free的,如果是要合并成一个block
- 比如上图的c被释放掉,要坚持B和D是否是free
- 可以通过C的size找到D
- 但是B就比较难找, 可以从链表的head依次查询或者在每个Block的尾部也加上信息
Bitmap approach
Buddy System approach (Linux)
Buddy System的基本原理
Buddy System把系统中的可用存储空间划分为存储块(Block)来进行管理, 每个存储块的大小必须是2的n次幂(Pow(2, n)), 即1, 2, 4, 8, 16, 32, 64, 128…
假设系统全部可用空间为Pow(2, k), 那么在Buddy System初始化时将生成一个长度为k + 1的可用空间表List, 并将全部可用空间作为一个大小为Pow(2, k)的块挂接在List的最后一个节点上, 如下图:
如何分配空间
当用户申请size个字的存储空间时, Buddy System分配的Block大小为Pow(2, m)个字大小(Pow(2, m-1) < size < Pow(2, m)).
此时Buddy System将在List中的m位置寻找可用的Block. 显然List中这个位置为空, 于是Buddy System就开始寻找向上查找m+1, m+2, 直到达到k为止. 找到k后, 便得到可用Block(k), 此时Block(k)将分裂成两个大小为Pow(k-1)的块, 并将其中一个插入到List中k-1的位置, 同时对另外一个继续进行分裂. 如此以往直到得到两个大小为Pow(2, m)的块为止, 如下图所示:
如何回收空间
如果系统在运行一段时间之后, List中某个位置n可能会出现多个块, 则将其他块依次链接可用块链表的末尾:
当Buddy System要在n位置取可用块时, 直接从链表头取一个就行了.
当一个存储块不再使用时, 用户应该将该块归还给Buddy System. 此时系统将根据Block的大小计算出其在List中的位置, 然后插入到可用块链表的末尾. 在这一步完成后, 系统立即开始合并操作. 该操作是将"伙伴"合并到一起, 并放到List的下一个位置中, 并继续对更大的块进行合并, 直到无法合并为止.
什么是伙伴(buddy)
何谓"伙伴"? 如前所述, 在分配存储块时经常会将一个大的存储块分裂成两个大小相等的小块, 那么这两个小块就称为"伙伴".在Buddy System进行合并时, 只会将互为伙伴的两个存储块合并成大块, 也就是说如果有两个存储块大小相同, 地址也相邻, 但是不是由同一个大块分裂出来的, 也不会被合并起来. 正常情况下, 起始地址为p, 大小为Pow(2, k)的存储块, 其伙伴块的起始地址为: p + Pow(2, k) 和 p - Pow(2, k).
Buddy System的优缺点
- buddy system能很快地分配和回收内存块,
- 但有内碎片,因为它要向上取2的幂的块大小,有空间浪费,但这是所有内存分配算法都避免不了的。linux系统使用它来分配内存页,很可能是因为内存页的大小是2的次幂。
Buddy System的实现
https://github.com/lotabout/buddy-system
Reference:
https://www.cnblogs.com/xkfz007/archive/2012/11/08/2760148.html
https://www.cnblogs.com/encode/p/4967259.html
更高效的使用内存—按需将进程分段加载进不连续的内存空间里
- 目前我们有两个假设:
- 当进程被执行时,整个程序必须被全部加载进内存里
- 整个进程需要被加载进连续的内存空间里
- 这两个假设都限制了对内存的使用
- 我们可以先解决如何去掉限制条件二, 也就是将程序分割成小块然后加载进不连续的内存空间里,这样可以提高内存的使用效率
方法一: 内存分段(memory segmentation)
什么是内存分段
- 对于程序员来说, 一个程序可以被分为不同的部分也就是segment, 比如methods, procedures, functions, arrays, stacks, variables…
- 我们可以将程序分为不同的segment, 然后加载进不连续的内存空间里
如何实现内存分段的logical address
- 当程序被编译时, program segment, global variable segment, stack segments, heap segments, library segments会被创建
- Loader会给每个segment分配一个segment number
- 每一个segment都有一个number和length
*所以 logical address = segment number + offset
如何转换分段的logical address
- 需要使用segment table
- segment table储存着segment number对应的一个物理起始地址 (base),和这个segment的最大长度(limit)
- CPU会对比limit和offset的大小,如果offset大于limit说明越界直接返回error
- 然后CPU将segment对应的base+offset得到真实的物理地址
- Example
logical address = <2, 53>
segment 2在下图中的segment table中base 4300, limit 400
物理地址: 4300 + 53 = 4353
分段的缺点
- 分段的缺点是当遇到swapping时 (将不用的segment移动到硬盘中, 创造更多可使用的内存空间), 很难合理的利用硬盘的内存 (找到合适的分区大小,避免fragment)
- 在当今的x86-64构架中,分段已经被淘汰了
- 但是大部分x86-64构架仍然支持分段,为了 backward compatibility
方法二: 内存分页(memory paging)
什么是内存分页
- 这是当前x86-64构架使用的技术
- 将物理内存分为很多个大小一样的块,这些块叫frames
- 每个进程的logical memory也被分为很多和frames大小一样的块,被叫做pages
- 每个进程都有独立的page分区,也就是P1有page 7,P2也有page 7
- 当进程被执行时, pages被加载进任何可用的frame
- 注意分页也会有internal fragments — 比如一个进程需要10个page,则最后一个page可能用不完
如何实现内存分页的logical address
- 每一个page都有一个page number和word number
- word: 将page分为大小相等的区域(比如每块2bytes), 每个小块被叫做一个word
- 所以 logical address = page number + word number
- 对于一个32bit的地址: 前22bit可以表示page number,后10bits可以表示word number
如何转换分页的logical address
方法一: frame table
- 每个frame作为table的index, 对应一个page
- 找到对应的frame后加上offset得到物理地址
- 因为每个进程有自己的page space,所以会有相同的page number,所以可以在logical address和frame table中加入进程ID
- 缺点是需要逐个查找,效率很慢
方法二: page table
- 每个进程有独立的page table, 每个page对应frame, 通过page table找到对应的frame
- 要想使用page table首先要找到page table所在的frame
- page table也有自己的逻辑地址 (page number, word number)
- page table的物理地址的起始地址储存在CR3( Control Register 3)里,而PCB会有一个pointer指向CR3
- 通过page table的page number 和 PTR得到对应的frame
- 通过frame和word得到page table的实际物理地址
- 找到page table后就可以使用page table了
- page table中储存的是对应的frame number也就是第几个frame
- 物理地址: frame number * page size + offset
- Example: 下图的page size是4 bytes
则对于逻辑地址<0,3>:
page 0对应的frame number是5 (也就是第五个frame)
则对应的物理地址是 5*4 + 3 = 23
Another Example:
- page size 为 1024 bytes, 假设每个word为1 byte
- 对于逻辑地址(2, 100), 对应的frame number是3
- 则物理地址为 3 * 1024 + 100 = 3172
TLB – Translation Look-aside Buffer
- 正常的paging因为需要访问两次物理内存,所以减慢了
- 对于每个logical address, 首先要访问page table的物理地址然后再访问实际的物理地址
- 所以可以在high speed cache memory中创建一个buffer用来储存一些page number以及对应的frame number, 这个buffer叫做TLB
- 对于TLB查找的过程是非常快的,要比去访问页表的物理地址要快很多
- TLB比较小, 一般只有32-1024个entries
- 当TLB满时需要replacement一些entry, 有不同的replacement policy比如LRU或者Round Robin
- 一些entry是不能被移除的,比如key kernel page
- 如果需要查找的page不在TLB则被叫做TLB miss,就只能正常的方法转换
- 如果需要查找的page在TLB则被叫做TLB hit,可以直接获取frame number
- TLB是由硬件实现的,所以OS需要根据TLB去实现paging, TLB的改动也会影响paging的实现
多级分页(Multilevel paging)
- 另外一个问题是页表可能会很大
- 假设现在logical address width是32bit, 则可以总共产生的地址空间是232 = 4GB
- 假设page size是4KB, 则总共有 4GB / 4KB = 220 = 1MB个pages
- 现在需要给这个1MB个pages创建页表, 假设页表中每个entry的大小是4bytes, 则页表的大小为1MB * 4 = 4MB
- 也就是每个进程都需要4MB的连续空间作为页表
- 下图中进程1和进程2的大小各为4MB(1048576)
- 如何减少页表的大小呢,从上图我们可以看到进程1只使用了3个page,而进程2只使用了2个page, 剩下的page table entry都是没有用到的,所以我们不需要将4GB的空间全部映射到page table中
- 我们可以使用多级页表解决这个问题
- 之前的页表大小为4MB
- 我们给这个4MB的连续空间也做一个页表
- page size还是4KB, 则4MB可以被分为 4MB / 4KB = 1024 = 1K个page
- 则新的页表有1K个entry对应1K个page, 每个entry的大小是4 bytes, 则新的页表大小是1K* 4 = 4KB
- 我们只使用用到的内存, 没有用到的全部为NULL,这样就减小了页表的大小
Memory protection in paging
Demand Paging — paging system with swapping
- 之前我们有两个限制条件:
- 当进程被执行时,整个程序必须被全部加载进内存里
- 整个进程需要被加载进连续的内存空间里
- 第二个限制条件已经通过paging被移除了
- 现在来移除第一个限制条件
Demand Paging
- pages只有在被需要的时候才会被加载进内存
- 不需要的page可以放进硬盘中中
- 为了知道所需要的page是否在内存中, 我们在page table加入一个valid-invalid bit来表示page是否在内存中
- 如果不在内存中,则被叫做page fault,然后OS会去硬盘中找到对应的page,加载进内存, 更新page table, 然后执行
Example:
Demand Paging replacement policy
- 当内存里的没有free frame时, 则需要将现存的frane踢出去,腾出空间, 这个被踢出去的frame叫做victim
- 下面是不同的选择victim的策略
- 下面假设frame size = 3, 请求的page string是 1, 2, 3, 4, 2, 1, 5, 6, 2, 1, 2, 3, 7, 6, 3, 2, 1, 2, 3, 6.
FIFO
- 踢出最早进来的
Optimal Page Replacement
- 如果我们知道后面所有需要请求的page,则踢出后面使用频率最少的那个, 如果有多个page使用频率一样,则踢出距离目前位置最远的那个
- 当然这是理想情况, 现实中我们不可能知道系统后面请求的page
LRU
- 踢出least recent used的page
Buddy System with Paging
- 此部分: 版权声明:本文为CSDN博主「liuhangtiant」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/liuhangtiant/article/details/81043815
Buddy的起点
这场旅行的起点就是buddy system初始化完毕后,在首次分配页面之前的上下文。
首先我们假设最大阶是10,下图是初始阶段pages的分配图:
空闲pages以一定的组织形式存放在链表上。order为10的链表管理order为10的pages,也就是1024个连续的物理页块,为了尽可能避免内存碎片,每一种order对应的链表会按迁移类型分成多个,为了简单起见,这里分成两个链表,分别管理可移动物理页面和不可移动的物理页面,这样划分的好处后面会看出来。
buddy system初始化完毕后,所有的物理页都是order为10的物理页面,也就是1024个连续的物理页面被分成一组挂在order为10的可移动迁移类型链表上。其他所有的order及迁移类型对应的链表都是空的,当然这里的前提是buddy system所管理的所有连续物理内存都是1024个连续物理页面对齐的。
分配order为0迁移类型为可移动的page
分配页面要到最合适的链表上去分配,首选当然是从order为0的链表上分配。不巧的是,order为0的链表是空的,那么只能去order为1的链表上分配,更不巧的是order为1的链表也是空的,后面依次查询order为2-9的链表,都是空的。最终只能在order为10的链表上分配一组pages,并将这组pages从空闲链表删除,也即移除了buddy system。
然而,实际需要的是一个page,现在分配到的确是1024个pages,这要如何处理呢?其实很简单,剩余的1023个pages重新回到buddy system,重新回到buddy system遵循尽可能回到order较大的链表的原则。我们一步一步分析:
- 1023个pages回到order为10的链表是不可能的了,因为pages数量没有达到1024。
- 1024个pages的后512个pages回到order为9的可移动迁移类型链表上,还剩余511个pages。
- 511个pages的后256个pages回到order为8的可移动迁移类型链表上,还剩余255个pages。
- 255个pages的后128个pages回到order为7的可移动迁移类型链表上,还剩余127个pages。
- 最终order为9,8,7,6,5,4,3,2,1,0的可移动迁移类型链表上,都会增加一个成员。
分配完该page后,pages的分布图如下:
分配order为2迁移类型为不可移动的pages
order为2的可移动链表上有一个成员,而其不可移动链表上没有成员,不仅如此,所有的不可移动链表都是空的。这可怎么办?难道就分配失败了?一首凉凉就要送给给它了吗?这样也太不公平了,要知道,一开始不可移动链表上就是空的。
buddy system当然不会那么有失公允,虽然buddy system没有为不可移动链表分配pages,却提供了另外一种机制,叫做偷取机制。也就是说,当分配不到某种迁移类型的物理页面时,会尝试从其他迁移类型的链表上偷取物理页面。
比如当前场景下,由于不可移动链表全部为空,此时会偷取可移动链表上的物理页面。为了防止内存碎片,偷取机制会偷取尽可能大的空间,有以下准则需要遵循:
- 从order最大的链表尝试偷。
- 偷的时候会将整个pageblock中所有的空闲物理页面都偷过去。pageblock的order一般对应最大的order,即10。
- 如果一个pageblock中有超过一半的物理页面被偷了,那么就会修改整个pageblock的迁移类型,当该pageblock的页面被释放时,会被添加到新的迁移类型对应的链表上去。这样一来,实际上相当于将整个pageblock都偷过去了。
那么当前场景下,会从order为10的可移动链表上偷一个成员,即偷取1024个物理页面。由于实际需要的是4个物理页面,1024个连续物理页面的后1020个物理页面会重新回到buddy system,但是此时会回到不可移动链表上。
分配完成后,pages分配图如下:
释放之前分配的order为0的page
释放的page会尝试与其伙伴(即相邻的order为0的page)合并,由于其伙伴没有被分配,依然在buddy system中,所以二者可以合并为order为1的pages;order为1的pages会继续试图与其伙伴合并,当前上下文可以合并为order为2的pages;最终合并成order为1024个pages,回到了原点。
page释放后,pages分布图如下:
迁移类型的好处
以上基本完成了这场旅行,不过我们这里要加个小插曲,解释下为何要加入迁移类型。迁移类型的目的是为了尽可能避免内存碎片。
我们已经看到,在释放pages的时候,释放的pages会尝试与其伙伴合并成更大order的pages,也就是合并成更大的内存块。如果某order的pages被永久申请,即便其伙伴是空闲的,也无法合并,这就导致内存碎片。
而迁移类型加入后,需要永久申请的pages从不可迁移链表申请,就会大大减轻内存碎片的产生。