当我还是一个懵懂无知的少年时,内心也曾升起这样的疑问这内存咋来的捏❓
有个帅气的小哥哥给我甩了一篇博客:对,就是这篇,看完后总感觉意犹未尽,似乎少了点什么的样子。。。
“这。。不是刚给过吗,怎么又来要,先给个假的吧,反正你不急着用!”
内核并没有立即分配物理内存,而是先返回虚拟内存给上层,真正分配要等有人往这块地址中写数据才开始。
不久之后,不知道哪个倒霉蛋开始往那个假地址写数据了,正好一脚踩中了内核埋的陷阱——缺页中断!
具体情况是这样的:上面派人拿着那个虚拟地址和数据,来内核层写入。写入的时候会分析虚拟地址的合法性(没错,就是自己给出去的),尝试计算出物理内存的地址,发现页表中映射关系不存在,就触发了缺页中断!
伙伴算法
底层分配物理内存使用kmalloc(),大块通过伙伴算法分配,内核用这个算法维护了一堆块,就像这样的:
伙伴算法分配器管理的空闲列表,分别为2^0 ~ 2^10个页, 即4KB~1MB。
申请空间依次从小到大在空闲列表上匹配,没找到就往下一个找,分割下一个大块,一部分返回,另一部分挂在对应链表上;
两个具有相同大小、物理地址连续、且从同一个大块中拆出来的块叫伙伴。
释放的时候先检查对应size的链表中是否有伙伴,有就合并;然后往下一个size的链表中找,继续检查伙伴合并,直到没有伙伴或者合并为了最大块,再把块挂到对应size的链表上;
因为伙伴的块大小都是固定的,能分能合,所以不存在外碎片问题。
slab/slub分配器
slab分配器也叫slab层,小块且需频繁分配释放的通过slab层分配,slab通常为2^n字节。slab提前分配好固定size的块,挂在链表上,分为已满列表、部分满、空闲,分配时先从部分满列表分配,没有则从空闲列表分配,再没有则开辟一个新的空闲列表分配。
slab缓存队列管理复杂,其用于管理内存的数据结构本身开销较大,在其基础上又做了改进,就有了slub:
slub可以动态调整内存块大小(类似伙伴算法),支持合并小内存块为较大内存块,减少外碎片;
slub为每cpu维护一个本地缓存,只有本地缓存没有空闲,才向全局缓存申请,可以减少线程间竞争。
对于应用层老大的内存需求,不敢不重视,所以都是按页分配内存,也就是是通过伙伴算法分配了,slab/slub分配器一般是内核自己内部用~
页表
一个页是4KB,一台机器如果有4GB内存的话,有多少个页?
怎么把这些页管理起来呢?
我管理页的地址不就好了:
一个PTE(页表项)占4B,标识一页物理内存的起始地址,1024个PTE组成一张页表,一张页表大小也正好是4KB,可以维护1024*4KB个页的内存(4MB);
这样只需要1024个页表就能管理好高达4GB的内存!可是,这1024个页表也有点多,该怎么管理呢?类似地,我管理页表的地址不就好了:
一个PDE(页目录项)占4B,标识一张页表的起始地址,1024个PDE组成一张页目录表,一张页目录表大小也正好是4KB,可以维护1024*4MB内存,即4GB。这样,我只需要记录顶级的页目录表的起始地址就可以了,这个就叫页基址。每cpu都给他一个寄存器,存这个页目录基址,这样cpu访问进程的页就更快啦~
虚拟地址也叫线性地址,高10位表示页目录项偏移,最高级页目录基址 + 高10位 * 4B 即为页目录项地址;
页目录项里记录了对应页表基地址,页表基地址 + 中10位 * 4B 即为物理页地址;
物理页地址 + 低12位 即为真正的物理地址,这里映射操作由MMU硬件(内存管理单元)自动计算,快到起飞~
当然了,虚拟内存也是有限的,在物理内存不足时,内核会将不常用的页换出到磁盘上的swap分区,需要访问时再换入内存。
好像有点扯远了,刚刚在伙伴算法分配器那里,好像没有找到空闲内存页哎~
怎么办,交不了差,难道要我跑路?
是的,当系统内存不足时,内核会使用 OOM(Out of Memory)机制来释放一些内存。通常情况下,内核会杀掉低优先级进程以释放内存。这种情况下,释放内存的过程被称为 OOM-kill。
再逼我,我疯起来,连自己都杀!