- 作者: 雪山肥鱼
- 时间:20210318 06:16
- 目的:深入理解内存
# Slab、Buddy 与 内存的二级分配
## 了解Slab 与 Buddy原理及工作机制
## libc 与 buddy
# 常见内核申请API: kmalloc、vmalloc、ioremap
# OOM 简介
## 举例
# Linux内存申请示意图
Slab、Buddy与二级分配
所有的内存肯定都是从buddy算法来的,buddy是最直面最底层的内存。也就是说无论谁通过何种方式申请内存,底层都会调用get_free_page()、page_alloc() 等buddy级的API.
但buddy的粒度实在太大,每次分配的都至少是1页的4KB内存。 那么APP只申请16个字节,那么4kb-16字节岂不是都废掉了?
所以linux申请内存是一个二级分配的过程。
拿到2^n个内存页(4k),应该如何去管理
- 内核空间申请通过kmalloc/kfree 都是通过slab去申请
- 用户层的 malloc 和 free 是通过libc去申请内存,libc底层通过brk ,sbrk, mmap 去管理内存,brk是进行堆扩展,mmap进行匿名映射。
- 都是一个二级管理,底层一定是向buddy 索要内存。
Slab与buddy原理及工作机制 - 了解
linux的内存管理有Slab,Slub,Slot. 先介绍Slab. 有所了解,后续遇到再说,相关参考链接:
https://blog.csdn.net/FreeeLinux/article/details/54754752
http://www.secretmango.com/jimb/Whitepapers/slabs/slab.html
slab的放到CS中的解释,应该是把...分成厚片.
定义:The linux 2.4 kernal implemenmts a chaching memory allocator to hold caches(called slabs).
我的理解是,slab可以理解成一种cahce(非硬件上的,理解成 32b的是一类cache,64b大小的是一类slab) 去管理小额度内存。用来频繁申请和释放小额度内存。
工作原理:
- 申请32b内存,那么slab向buddy 拿2^0 1页内存
- 将这一页切割成n 个 32b 大小的块
- 32b 的 slab 是一类slab(cache), 具有identical objects(等分)
- 不会产生重叠,即32b slab中,会有64b的一块内存
- 用完了,再从buddy 中 要2^n 个page
- 其实就是 identical objects 瓜分 2^n 个 page
通过 cat /proc/slabinfo 查看 slab 相关信息
常用数据结构会做slab,比如,kmalloc-32、kmalloc-64等,粒度更小的管理方式
通过 cat /proc/meminfo 看总的slab
SReclaimable: 可回收的slab, 比如文件系统的inode cache。内存不够是可以被回收掉的。
SUnreclaim: 不可回收的,自己写驱动申请的内存就是不可回收的。(暂时了解)
Slab 也是查找内存泄漏的一种方式,在meminfo 中查看 slab 越来越大,再去slabinfo 查看,谁越来越大
libc 与 buddy
参照上面二级分配的原理图。malloc 与 free 都是通过libc 与buddy 进行打交道。
//libc 中有一个API mallopt, 参数 M_TRIM_THRESHOLD
//应用程序在free内存的时候 free到多少的时候,libc才会把内存还给buddy
//设置位-1 最大的正整数,无论我free多少内存,内存都hold在libc中,不会还给buddy
int main(int argc, char ** argv)
{
unsigned char * buffer;
int i;
if(!mlockall(MCL_CURRENT | MCL_FUTURE ))
mallopt( M_TRIM_THRESHOLD, -1UL);
mallopt(M_MMAP_MAX, 0);
buffer = malloc(SOMESIZE);
if( !buffer )
exit(-1);
/*
* Touch each page in this piece of memory to get it
* mapped into RAM
*/
for( i = 0; i < SOMESIZE; i+= 4*1024)
buffer[i] = 0;
free(buffer);
while(1);
return 0;
}
- 上述代码适用于对实时性要求很高的场景。
- 申请一块很大的内存,写一次(后续再提),释放掉
- 并没有还给buddy, 而是被libc hold住了
- free 个libc后 这一块内存会被其他进程拿走吗?
- 不可能,几个进程共享libc,只是共享libc的代码段
- 数据 都是内存独立的。即动态链接库的代码时共享的,数据不会共享,基本概念要搞清楚!
体现了二级分配的原理
用户空间VSS RSS 概念
kmalloc,vmalloc申请即拿到内存,而应用态并非如此。引入了之前文章所提到过 早年 VMS/VMX 操作系统中的 demanding page 、 zero page 的概念。
此时 p = malloc(100M), 内核立刻返回,并且此时打印100M里面的内容,居然还都是0。这一切的现象都是linux内核的假象,在骗你。
- 创建所谓的VMA virtual memory area, R+W 可读可写,记录了起始1G,终于1G+100M
- 将100M内存,全部都映射到一个zero page 0页的页面。(linux启动,将这一固定的页清零)
- 1G+100M内的所有地址都指向这个0页,所以都没有拿到物理地址。
- 在页表中,这100M内存都是read only
- 写1G+50M这里,由于是read only,没有写权限,mmu给cpu发送page fault 缺页中断。
- 在硬件中,有两个寄存器可以记录 page fault 发出的地址和原因
6.1 linux 内核 对page fault的处理程序,首先读到page fault 的地址1G+50M, 这个地址在VMA中,所以地址合法
6.2 内核找对应错误原因,原来是要写,但是页表里面的权限是readonly, VMA中又标明了有r+w 写的权限,linux内核,不会给进程发段错误信号。
6.3 反而会用底层buddy算法 帮他申请一页内存,同时刷新页表,将虚拟地址指向刚刚申请的新的页内存。 - 内存中除了1G+50M开始的4k拿到了内存(因为被分配到1个page),其他位置还是指向zero page.
1G - 1G+100M 这一段内存 被称为demanding page 需要的时候才会区申请内存,只有写的时候才会拿到内存。
当然上述只是第一次申请内存,如果第二次,可能就不会走buddy算法了,直接从libc中拿内存。
VMA存在的价值就是标明你这段地址是否是合法的!期待权限是否为R+W,加入你写的是1G+100M+4k,那么显然不是合法的,直接page fault,当然如果PC指针走到这里了,不是R+W,也会发生段错误
所以有此看出,VSS 可以比 RSS 大很多。
上述阐述的 demanding page、和 zero page 的 内存 lazy allocation的管理方案不仅适用于heap,也适用于代码段和栈, 即何时需要,何时加载。
常见内核申请API kmalloc、vmalloc、ioremap
- kmalloc申请内存 一定在低端内存,并且开机就支持了与低端内存的一 一映射,slab从低端内存里拿资源,而且并不需要再做虚拟地址和物理地址的映射
- vmalloc映射,在vmalloc映射区里找一片空闲的虚拟地址,并且找到一片空闲的物理内存,两者进行映射。所以,用vmalloc申请内存时,存在一个映射的过程。但是kmalloc申请内存,开机就映射好了。
- ioremap, 也是在vmalloc映射区里找一篇空间,但是ioremap针对的是寄存器,不是申请内存,并不是走buddy算法。 寄存器虽然在内存空间,但是不是内存。只要开了mmu 一切地址都是虚拟地址,这里针对寄存器也不例外。
- 被映射和被申请是两个概念,低端内存一开机就映射到了低端内存区,但并没有被内核用掉,还是空闲的,低端 内存可以被任何人用,可以被kmalloc vmalloc,malloc用
- 申请过程是在物理内存和虚拟空间找空闲,并且产生虚实映射关系。
- buddy 也会帮我们解决 内存使用冲突,kmalloc 申请的一快内存被使用,那么vmalloc 就一定不会再使用这块内存
上面的流程都是以page为单位,最小是1页
vmalloc 中也是包含ioremap
cat /proc/vmallocinfo | grep ioremap
OOM 简介
上文提到的 p=malloc(100M), 当你用到的时候,才会从buddy算法中拿出一页,如果此时操作系统不能兑现你要的内存,也就是说内存不够了。就会走到内核的OOM (out of memory)的流程中. 找到最该死的进程,并杀掉,将内存腾出
linux对于每一个进程都有一个该死程度的打分
//每一个进程下面都有,杀掉分数最高的
cd /proc
find ./ -name "*oom_score*"
打分依据badness() 的主要因素
- 所耗的rss
- 所耗页表
举例:
int main(int argc, char ** argv)
{
int max = -1;
int mb = 0;
char * buffer;
int i;
#define SIZE 2000
unsigned int *p = malloc(1024*1024*SIZE);
printf("malloc buffer:%p\n", p);
for( i = 0; i< 1024*1024 *(SIZE/sizeof(int)); i++) {
p[i] = 123;
if((i & 0xFFFFF == 0) {
printf("%dMB written\n", i >> 18);
usleep(100000);
}
}
}
在运行这个实验之前,我们要做以下操作,给ubuntu总的内存才1个G:
- sudo swapoff -a 交换分区关掉
-
echo 1 > /proc/sys/vm/overcommit_memory (关掉 申请内存是否大于本地内存)
linux 内核在 allocate memory 在杀进程的时候,会在dmesg中打印信息
打分最高 自己牺牲了。
配合 oom_adj, oom_score 修改每个进程的oom分数
- 同时打开一个firefox 浏览器,通过 pidof firefox,查看pid,进入/proc/pid 文件目录,查看oom打分。
- 修改 pid文件目录下 的oom_adj echo 5 > oom_adj, (5是系数,越大分数越高)
- cat oom_score 查看分数
- 细节 linux 只准让oom_adj 的系数调高,不能调低
- echo 3 > oom_adj :permission denied 调低意味着让别人牺牲嘛,肯定不行的。
实验结果: 先死的是 firefox。后死的是实验进程
开启多个进程,设备可以在这n个进程之间来回切换,顺滑。其实很大一部分在于内存的大小,可以保证多个进程在内存里驻留,而不会因为OOM被杀掉。
cd /proc/sys/vm
cat paint_on_oom
内存泄漏被杀掉后,进程依然可以重启
Linux内存申请示意图
左边物理内存一个小方框就代表一页内存
- 低端区域开机就已经映射了,即低端内存映射,但没有被内核拿走。
- 内核申请内存,一样要用kmalloc,申请的内存一定在低端内存(第二页,绿色)
- vmalloc 申请内存优先在高端寻找,其次在低端内存寻找。
- 上面的第二页已经被拿走了,不会产生冲突,就会去拿第一页(Buddy 算法维护)
- 用户拿走的内存,也有可能在低端,也有可能在高端。第3页、第4页。
- 内核如果要访问high mem ,在内存中可能有一块很小的内存映射区。
- 比如 3G-2m 到 3G 这一块,调用kmap,第5页映射到高端映射区
- 所以对arm 32位处理器的内核空间是从 3G-16M开始的,因为ARM在3G-16M和 3G-2M之间专门放了一些KO。了解,后续遇到再学习
- 映射和分配时两个概念,分配就是已经拿走了。不空闲了。映射只是映射而已,空闲的还是空闲的,并不代表被拿走,所有的页还是被buddy管理。
- kmalloc 开机即映射,申请直接申请即可
- vmalloc 映射 + 申请
- malloc 更奇葩,啥也没做,也没产生映射,更没去申请
- 集合 demanding page + zero page, 才会去修改页表,真正做到申请。
- 应用程序 有限从高到底找内存,因为内核使用的是低端内存,优先让给内核。堆栈代码段数据段都可以使用
- 高端内存被访问
- malloc
- vmalloc
- kmap(用到再学习)
- 非常临时性的动作,用掉就会unmap
- 比如再COW 的时候, 一个进程fork另一个进程,某一页做写时拷贝,从一页拷贝给另一页,那么内核再访问这一页的时候,就需要一个虚拟地址,用kmap 来映射,拷贝完后,unmap掉。用虚拟地址过度一下
- 目的就是为了让内核能够临时性的访问某一块highmem 地址。如果地址时lowmem 也就不用kmap了,直接线性映射即可。
- 源码课参考 memory.c 中的cow_user_page