linux 内存管理 (二) 内存的动态申请和释放

该文章参考宋宝华老师的内存管理课程,详细可以去听阅码场宋老师的课程。

1、Buddy 的问题 -- 分配的粒度太大

上一章我们了解到,我们所有的内存都是通过 buddy 算法来的,因为 buddy 是直面最底层的内存,它底下会拆分为 1页 2页 4页 8页 .....等等的内存组合。

申请内存,最底层的API ,都是buddy 级别的API ,get_free_pages / page_alloc 都是 buddy级别的API ,但是 buddy 级别的申请都很大,最小都是1页,

我们在应用程序或者内核中,经常会用到 申请 16/32个字节,但是这样你底层给它申请1页,不就浪费了吗

基于这样的原理,linux 在内存管理的时候,就分为两层来管理 堆内存。 

用户空间的malloc / free , 和 驱动层的 kmalloc / kfree 都不会直接调用 buddy 算法。而是分别通过 libc 和 slab 来申请的。

slab 的意思,就是 妈妈买一大块牛肉,每天给你切一片同样大小的吃。

2、slab 的原理

 slab 是有很多种的,可以是不同大小的slab , 可以是1页,可以是4页等。但是同一个slab 分成的 object 都是同等大小的

可以从 buddy 级别申请 一个 1页大小的 slab ,将slab 可以分为同为 8byte的 object , 这个 object 可以有很多。

可以从 buddy 级别申请 一个 4页大小的 slab ,将slab 可以分为同为 16byte的 object , 这个 object 可以有很多。

linux 系统当中有很多这样的 slab ,因为有很多数据结构要用到的,可以通过 /proc/slabinfo 来查看 系统里的 slab 分配的情况

因为在内核里有很多常用的数据结构,内核就会为这些数据结构专门申请和释放 slab 。

 这里面的参数,

pagesperslab  代表每个 slab 是多少页

objperslab   每个slab有多少个object

objsize  每个object 是多大

num_objs  一共有多少个 object 

slab 也可能造成内存泄漏,这时候就要看哪一个 slab 会越来越肥(可通过meminfo来看)。

cat /proc/meminfo 

可以看到 slab一共有多大,SReclaimable 是可回收的slab有多大(文件系统中的icache dcache,可以手动回收)。SUnreclaim 是不可回收的(自己在驱动写的kmalloc,一般都是标志成不可回收的)。

3、理解内存二级分配的一个例子

为了理解 内存分配的二级原理,这里举一个 写实时性要求比较高的例子。

mallopt(M_TRIM_THRESHOLD , -1UL ) 这条代码的意思是,告诉 libc, 我在释放内存的时候,这个内存要达到 -1UL(最大的正整数),你才能释放给内核。

之后申请一个很大的内存,然后释放给 libc , 但是这个申请的堆内存还是被 libc hold 住的,属于该进程所拥有,这样你的程序就可以很快。

虽然我在程序里有 free的动作,但是也是讲内存 释放给 libc, 并没有释放给内核。要理解这个内存二级分配的原理。

但是有人说,释放给 libc , 会不会被其他进程用掉呢? 这个你放120个心吧,多进程只会共享 libc的代码段( 只读),并不会共享它的堆空间。

4、kmalloc 和 vmalloc 的区别 

根据上一章的内容,

  • 我们知道 低端内存区域,一开机就会被线性映射到 内核空间(可以使用 phys_to_virt / virt_to_phy)。所以  kmalloc 不存在 映射的过程(很快),而 vmalloc 需要有映射的过程(很慢)。
  • vmalloc 和 ioremap 都是以 page为单位的,关于 vmalloc 的区域 可以看  /proc/vmallocinfo 
  • 注意低端内存一开机就被映射到内核空间,并不是被内核用掉了,它还是空闲的,受buddy 管理(buddy是管谁是空闲的,保证你每次申请拿到的都是空闲的页,buddy非常清楚哪些页是空闲的,哪些是被占用的),所有被映射和被申请是两码事。
  • 低端内存可以被任何人用,可以被kmalloc用,可以给 vmalloc用,可以给malloc用
  • ioremap 是映射寄存器的,所有没有一个申请的过程,只需要在 vmalloc 虚拟地址空间里,找一片空间,映射过去就可以。
  • vmalloc 映射 kmalloc映射,至少都是1页(映射和申请是两码事),因为页表里面,虚实转换的关系,权限管理的关系,都是以page为单位的
  • kmalloc 的申请的内存,会间接受 slab的管理,所以颗粒度小

5、用户空间 malloc(100M)的 全部过程(page_fault发生的过程)

我们知道 在内核里 使用 kmalloc vmalloc 是可以直接拿到内存的,但是在应用层 malloc 不是这样的,只要当你读写内存的时候,你才能真正拿到

malloc(100M) 的整个过程是这样的,我们知道进行的虚拟地址空间4G , 其中用户空间是0-3G ,其中有数据段,代码段,堆空间,栈空间等。

malloc(100M) 首先会在空闲的堆空间中 找到100M的空间,比如1GB -- 1GB+100M ,(这100M的空间都会被指向 一个页 zero page ,这个一开机的时候就决定了,

MMU里 zero page 里都是0 ,而且权限是RO的,所以你可以正常的读)然后,用户空间这里会给 这100M 开辟一个 VMA (virtual memory area), 并且权限标记为 R+W ,

当我们去写 1G+50M 的时候,这个时候,zero page 是没有W权限的,所以正常的话出触发 page fault , 但是在这里如果监测到VMA中有W权限,非但不会触发page fault ,也不会发 信号,

而是通过内核的buddy 算法去申请 1page的内存给 1G+50M(Lazy 分配,写时分配) , 并且在MMU页表里标记权限为R+W,当写其他地址的时候也是这样的过程,没有写过的内存都是指向zero page。

也就是说 malloc 拿到内存的时候,其实我们什么都没拿到,只要去写的时候才拿到,这个只是第一次的时候是这样的过程。这叫 demanding page 按需分配的页。

注意:VMA 的作用,表明你的预期的权限,地址的范围(判断你的地址合不合法,权限合不合法)。内核到底发不发段错误的信号,有时候就取决于这里。

         页表里的是你真实的权限,你真实的权限和以和VMA里面的权限不一样,这种情况下产生的page fault ,Linux 会做一个比对,如果根据VMA的权限是可以满足你的,就满足你,比如写。满足不了你的,就触发page fault ,比如你去执行X.

         这个VMA的范围,就是 VSS ( 虚拟地址空间 ,virtual set size ) ,  malloc成功,这个时候你的 vss ( 虚拟地址空间 ,virtual set size ) 会增大。

这就是用户态申请内存的时候一个非常特殊的地方。

   malloc成功,不要以为你真的拿到了内存,这个时候你的 vss ( 虚拟地址空间 ,virtual set size ) 会增大,但是你的 rss ( 驻留在内存条上的内存,占用的物理内存,resident set size ) 会随着写到每一页而缓慢增大,所以分配成功的那一刻,你顶多是被忽悠了,和你实际占用还是不占用,暂时没有半毛钱的关系。
举例来说明,如下图,最初的堆是8KB, 这8KB 也写过了,所以堆的 vss 和 rss 都是 8KB 。此后,我们调用 brk() 把堆变大到 16KB, 但是实际上它占据的内存 rss 还是 8KB ,因为第3页还没有写,根本没有真正的从内存条上拿到内存。直到写到第3页,堆的 rss 才变成 12KB, 这就是 linux 针对 app 的 lazy 分配机制,它的出发点,当然也是防止应用程序傻逼了。

代码段的内存、堆的内存、栈的内存都是这样懒惰地拿到,demanding page。

 

6、Linux 的 某一个进程究竟耗费了多少内存?VSS/RSS/PSS/USS
 

这个问题很复杂,除了上面的 vss  ,rss 外,还有 pss 和 uss ,这些都是 Linux  不同于 RTOS 的显著特点之一。Linux 各个进程既要做到隔离,但是隔离中又要实现共享,比如1000个进程都要用到 libc ,libc的代码段显然在内存中只有1份。

下面的一幅图上有3个进程,pid为1044的 bash、pid为1045的 bash和pid为1054的 cat。每个进程透过自己的页表,把虚拟地址空间指向内存条上面的物理地址,每次切换一个进程,即切换一份独特的页表。

仅从此图而言,进程1044的 vss 和 rss 分别是:

vss= 1+2+3 

rss= 4+5+6

但是是不是“4+5+6”就是1044这个进程耗费的内存呢?这显然也是不准确的,因为4明显被3个进程指向,5明显被2个进程指向,坏事是大家一起干的,不能1044一个人背黑锅。

这个时候,就衍生出了一个pss(按比例计算的驻留内存, Proportional Set Size )的概念,仅从这一幅图而言,进程1044的pss为:

pss= 4/3 +5/2 +6

最后,还有进程1044独占且驻留的内存 uss(Unique Set Size ),仅从此图而言,

uss = 6

所以,分析Linux,我们不能模棱两可地停留于表面

7、内存耗尽-OOM

前面讲到了 内存管理的 lazy allocation , 会先忽悠你内存分配好了,但是实际上写的时候才会给你分配,要是到兑现的时候,无法兑现改怎么办呢?

这时候就会走到 内存耗尽的程序流程 OOM (out-of- memory),找到一个最该杀死的进程,把它杀掉。

那如何找到这个最该死的程序呢,需要对每一个进程进行打分。可以看到每个进程都有一个 oom_score,分数越高,越容易被杀死。

 那这个是如何评分的呢?

oom_score 可以是 负数 

oom_adj 是按比例调整 oom_score,oom_adj 越大,oom_score会成倍的增大。

oom_score_adj 是 线性的加减,oom_score_adj 越大, oom_score也会越大

注意:

oom_adj  oom_score_adj 可以 手动调整  echo  -5 > /proc/8779/oom_adj  ,调整完之后,oom_score会立刻改变,但是在该用户权限之内,是无法逆向的,你可以再执行

echo  10 > /proc/8779/oom_adj  但是无法再执行  echo  -15 > /proc/8779/oom_adj ,除非你用root权限, sudo  sh -c 'echo  -15 > /proc/8779/oom_adj ' 来做。

我们可以来做个测试,测试之前需要先做两间事情

总内存大小是1G,首先要执行 swapoff -a 关掉交换分区,然后 执行 echo 1 > /proc/sys/vm/overcommit_memory  ,允许系统可以申请大于实际物理内存的空间。

像这样的都可以在 内核文档里搜索到:

注意:

  • 0xFFFFF 是 1M 对齐 。 1MB = 1024*1024 B = 2^20B   
  • 0-0xFFFFF 正好是1M空间 ,0xFFFFF + 1 = 0x10 0000  
  • i 每次走 1 ,p 走 4个字节,所以 i 在 1M对齐的时候,p是 4M对齐
  • 写一个整型 i , 是写了4个字节,申请了 2000MB的内存,i >> 18 是写了多少MB  =  i*4/ 2^20 

 在运行的时候,是下面这样。dmesg 也能看到OOM的信息,打分最高,被杀掉。耗内存最高。

注意:

badness() 随着内核的发展,一直再改进,现在的内核 已经不看 root权限了。

8、动态申请内存的大总结

左侧是物理内存 ,每个小格子是一个 page,

  • vmalloc 优先从高端内存(high memory)拿,如果没有空闲的,去低端内存(low memory , normal_zone 和 dma_zone )拿, normal zone没有,再去 dma_zone 找。应用程序申请内存,也是这个顺序。
  • 用户拿到的内存,也有可能是低端,高端,
  • 如果在内核中要访问 高端内存,要用特殊的API  kmap ,因为低端内存早就映射好了,高端内存要自己映射。
  • 在32位处理器中,x86平台和ARM 平台会有点不一样。虚拟地址空间的划分会不一样。X86平台的内核空间是从3G开始的,而ARM 32平台是从3G - 16M 开始的。
  • 3G~3G-2M 的空间,是专门给内核用的 高端内存(high memory)映射区。3G-16M 3G-2M之间就是内核用来放内核模块的,3G~3G+6M存放内核代码
  • 内核一般不从high memory 申请,因为内核使用的内存有限,一般normal内存就够了,但是应用程序一般是先从high memory 里面找,没有再从normal memory找 ,还没有再从dma zone ,按照这个顺序。对于arm 内核如果非要使用high memory ,一般映射到3G-2M 到3G的虚拟机中访问。(这种从高到低的顺序,是为了保护内核能正常运行,这是一种常识性的逻辑)
  • 内核里还是经常用到 kmap的,但是kmap 一般都是一些临时性的动作,用完一般就会 umap掉了,比如在内核里你只知道物理地址,但是不知道虚拟地址,你就可以使用kmap来做,就是可以在内核里临时访问某个页面(已经存在),所以不涉及一个申请的行为。kmap 也不是只能访问高端内存,如果传入的参数不是高端内存,直接就会返回低端内存的地址,如果是高端内存,才会去映射。

  • 2
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值