linux铁三角之内存(二)

  • 作者: 雪山肥鱼
  • 时间: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),应该如何去管理

libc,slab buddy.png
  • 内核空间申请通过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 info.png

常用数据结构会做slab,比如,kmalloc-32、kmalloc-64等,粒度更小的管理方式

通过 cat /proc/meminfo 看总的slab


Slab.png

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 的概念。

lazzy allocation.png

此时 p = malloc(100M), 内核立刻返回,并且此时打印100M里面的内容,居然还都是0。这一切的现象都是linux内核的假象,在骗你。

  1. 创建所谓的VMA virtual memory area, R+W 可读可写,记录了起始1G,终于1G+100M
  2. 将100M内存,全部都映射到一个zero page 0页的页面。(linux启动,将这一固定的页清零)
  3. 1G+100M内的所有地址都指向这个0页,所以都没有拿到物理地址。
  4. 在页表中,这100M内存都是read only
  5. 写1G+50M这里,由于是read only,没有写权限,mmu给cpu发送page fault 缺页中断。
  6. 在硬件中,有两个寄存器可以记录 page fault 发出的地址和原因
    6.1 linux 内核 对page fault的处理程序,首先读到page fault 的地址1G+50M, 这个地址在VMA中,所以地址合法
    6.2 内核找对应错误原因,原来是要写,但是页表里面的权限是readonly, VMA中又标明了有r+w 写的权限,linux内核,不会给进程发段错误信号。
    6.3 反而会用底层buddy算法 帮他申请一页内存,同时刷新页表,将虚拟地址指向刚刚申请的新的页内存。
  7. 内存中除了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/vmalloc/ioremap.png
  • 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 (关掉 申请内存是否大于本地内存)


    killed.png

linux 内核在 allocate memory 在杀进程的时候,会在dmesg中打印信息

dmesg.png

打分最高 自己牺牲了。

配合 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内存申请示意图

内存申请分布示意图.png

左边物理内存一个小方框就代表一页内存

  • 低端区域开机就已经映射了,即低端内存映射,但没有被内核拿走。
    • 内核申请内存,一样要用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

你可能感兴趣的:(linux铁三角之内存(二))