Linux内存管理 —— 内核态和用户态的内存分配方式

 1. 使用buddy系统管理ZONE
我的这两篇文章buddy系统和slab分配器已经分析过buddy和slab的原理和源码,因此一些细节不再赘述。
所有zone都是通过buddy系统管理的,buddy system由Harry Markowitz在1963年提出。buddy的工作方式我就不说了,简单来说buddy就是用来管理内存的使用情况:一个页被申请了,别人就不能申请了。通过/proc/buddyinfo可以查看buddy的内存余量。
由于buddy是zone里面的一个成员,所以每个zone都有自己的buddy系统来管理自己的内存(因此,buddy管理的也是物理内存哦)。

➜  vdbench cat /proc/buddyinfo      
Node 0, zone      DMA      3      2      3      0      3      2      0      0      1      1      3 
Node 0, zone    DMA32      8      5      3      6      3     10      5      9      7      7    724 
Node 0, zone   Normal  22694  22791  58053  23855   9955   6270   3532   2158   1375   1083   2743 

buddy的问题就是容易碎掉,即没有大块连续内存。对应用程序和内核非线性映射没有影响,因为有MMU和页表,但DMA不行,DMA engine里面没有MMU,一致性映射后必须是连续内存。

可以通过alloc_pages(gfp_mask, order)从buddy里面申请内存,申请内存大小都是2^order个页的大小,这样显然是不满足实际需求的。因此,基于buddy,slab(或slub/slob)对内存进行了二次管理,使系统可以申请小块内存。

Slab先从buddy拿到数个页的内存,然后切成固定的小块(称为object),再分配出去。从/proc/slabinfo中可以看到系统内有很多slab,每个slab管理着数个页的内存,它们可分为两种:一个是各模块专用的,一种是通用的。

在内核中常用的kmalloc就是通过slab拿的内存,它向通用的slab里申请内存。我们也就知道,kmalloc只能分配一个对象的大小,比如你想分配40B,实际上是分配了64B。在include/linux/kmalloc_sizes.h可以看到通用cache的大小都有哪些:

#if (PAGE_SIZE == 4096)
    CACHE(32)
#endif
    CACHE(64)
#if L1_CACHE_BYTES < 64
    CACHE(96)
#endif
    CACHE(128)
#if L1_CACHE_BYTES < 128
    CACHE(192)
#endif
    CACHE(256)
    CACHE(512)
    CACHE(1024)
    CACHE(2048)
    CACHE(4096)
    CACHE(8192)
    CACHE(16384)
    CACHE(32768)
    CACHE(65536)
    CACHE(131072)
#if KMALLOC_MAX_SIZE >= 262144
    CACHE(262144)
#endif
#if KMALLOC_MAX_SIZE >= 524288
    CACHE(524288)
#endif
#if KMALLOC_MAX_SIZE >= 1048576
    CACHE(1048576)
#endif
#if KMALLOC_MAX_SIZE >= 2097152
    CACHE(2097152)
#endif
#if KMALLOC_MAX_SIZE >= 4194304
    CACHE(4194304)
#endif
#if KMALLOC_MAX_SIZE >= 8388608
    CACHE(8388608)
#endif
#if KMALLOC_MAX_SIZE >= 16777216
    CACHE(16777216)
#endif
#if KMALLOC_MAX_SIZE >= 33554432
    CACHE(33554432)
#endif

上述两种slab缓存,专用slab主要用于内核各模块的一些数据结构,这些内存是模块启动时就通过kmem_cache_alloc分配好占为己有,一些模块自己单独申请一块kmem_cache可以确保有可用内存。而各阶的通用slab则用于给内核中的kmalloc等函数分配内存。

要注意在slab分配器里面的“cache”特指struct kmem_cache结构的实例,与CPU的cache无关。在/proc/slabinfo中可以查看当前系统中已经存在的“cache”列表。

通过slabtop命令可以查看当前系统中slab内存的消耗情况,和top命令类似,是按照已分配出去的内存多少的顺序打印的:

➜  vdbench sudo slabtop --once
 Active / Total Objects (% used)    : 1334118 / 1408668 (94.7%)
 Active / Total Slabs (% used)      : 44508 / 44508 (100.0%)
 Active / Total Caches (% used)     : 93 / 130 (71.5%)
 Active / Total Size (% used)       : 391667.18K / 409063.58K (95.7%)
 Minimum / Average / Maximum Object : 0.01K / 0.29K / 22.88K

  OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME                   
474981 456075   0%    0.10K  12179       39     48716K buffer_head            
231924 231924 100%    0.19K  11044       21     44176K dentry                 
168810 168810 100%    1.06K   5627       30    180064K ext4_inode_cache       
 59261  56996   0%    0.20K   3119       19     12476K vm_area_struct         
 57324  57324 100%    0.04K    562      102      2248K ext4_extent_status     
 47124  32578   0%    0.57K   1683       28     26928K radix_tree_node        
 42990  42990 100%    0.13K   1433       30      5732K kernfs_node_cache      
 37056  33588   0%    0.06K    579       64      2316K pid                    
 36992  25420   0%    0.06K    578       64      2312K kmalloc-64             
 29532  29532 100%    0.69K   1284       23     20544K squashfs_inode_cache   
 20746  18902   0%    0.09K    451       46      1804K anon_vma               
 19720  18888   0%    0.02K    116      170       464K lsm_file_cache         
 19152  17347   0%    0.25K   1197       16      4788K filp                   
 17498  15795   0%    0.59K    673       26     10768K inode_cache            
 15640  10343   0%    0.05K    184       85       736K ftrace_event_field     
 14848  13304   0%    0.03K    116      128       464K kmalloc-32             
 14592  13074   0%    0.02K     57      256       228K kmalloc-16             
  9912   9847   0%    0.07K    177       56       708K Acpi-Operand           
  9216   8452   0%    0.01K     18      512        72K kmalloc-8              
  8610   6010   0%    0.09K    205       42       820K kmalloc-96             
  8372   8344   0%    0.14K    299       28      1196K ext4_groupinfo_4k      
  6018   5996   0%    0.04K     59      102       236K Acpi-Namespace         
  5901   5513   0%    0.19K    281       21      1124K kmalloc-192            
  3933   3709   0%    0.70K    171       23      2736K shmem_inode_cache      
  3680   3439   0%    1.00K    230       16      3680K kmalloc-1024           
  2880   2800   0%    0.66K    120       24      1920K proc_inode_cache       
  2752   2139   0%    0.12K     86       32       344K kmalloc-128            
  2482   2336   0%    0.05K     34       73       136K mbcache                
  2384   2290   0%    0.50K    149       16      1192K kmalloc-512            
  2247   1768   0%    0.19K    107       21       428K cred_jar               
  2240   2240 100%    0.12K     70       32       280K eventpoll_epi          
  2016   1865   0%    2.00K    126       16      4032K kmalloc-2048           
  2001   1928   0%    0.69K     87       23      1392K sock_inode_cache       
  2000   1826   0%    0.25K    125       16       500K kmalloc-256            
  1978   1935   0%    0.09K     43       46       172K trace_event_file       
  1932   1652   0%    0.69K     84       23      1344K i915_vma               
  1912   1877   0%    4.00K    239        8      7648K kmalloc-4096 

因此,slab和buddy是上下级的调用关系,slab的内存来自buddy;它们都是内存分配器,只是buddy管理的是各ZONE映射区,slab管理的是buddy的各阶。

注意,vmalloc是直接向buddy要内存的,不经过slab,因此vmalloc申请内存的最小单位是一页。slab只从lowmem申请内存,因此拿到的内存在物理上是连续的,vmalloc可以从高端和低端拿内存。而用户态的malloc是通过brk/mmap系统调用每次向内核申请一页,然后在标准库里再做进一步管理供用户程序使用。

 



2. 用户态申请内存时的”lazy allocation”

用户态申请内存时,库函数并不会立即从内核里去拿,而是COW(copy on write)的,要不然用户态细细碎碎的申请释放都要跟内核打交道,那频繁系统调用的代价太大,并且内核每次都是一页一页的分配给用户态,小内存让内核很头疼的。
但是库函数会欺骗用户程序,你申请10MB,就让你以为已经拥有了10MB内存,但是只有你真的要用的时候,C库才会一点一点的从内核申请,直到10MB都拿到,这就是用户态申请内存的lazy模式。有时用户程序为了立即拿到这10MB内存,在申请完之后,会立即把内存写一遍(例如memset为0),让C库将内存都真正申请到。Linux会欺骗用户程序,但不欺骗内核,内核中的kmalloc/vmalloc就真的是要一个字节内存就没了一个字节。
因此malloc在刚申请(brk或mmap)的时候,10MB所有页面在页表中全都映射到同一个零化页面(ZERO_PAGE,全局共享的页,页的内容总是0,用于zero-mapped memory areas等用途),内容全是0,且页表上标记这10MB是只读的,在写的时候发生page fault,才去一页一页的分配内存和修改页表。所以brk和mmap只是扩展了你的虚拟地址空间,而不是去拿内存。在你实际去写内存的时候,内核会先把这个只读0页面拷贝到给你新分配的页面,然后执行你的写操作。

由于上面的COW,用户申请了10MB,系统不会立即给你,但告诉你申请成功了。Linux内核的lazy模式可以减少不必要的内存浪费,因为用户态程序的行为不可控制,如果有一个程序申请100M内存又不用,就浪费了。如果一段时间后,系统内存被其他地方消耗,已经不足以给你10MB了,你这时慢慢通过COW使用内存的时候,C库就拿不到内存了。就会出现OOM。

补充一点,由于用户进程申请内存是zero页面拷贝的,因此用户态向kernel新申请的页面都是清0的,这样可以防止用户态窃取内核态的数据。但是如果用户程序用完释放了,但还没还给内核,这时相同的进程又申请到了这段内存,那内存里就是原来的数据,不清0的。而内核的kmalloc/vmalloc就没这个动作了,想要清0页面可以用kzalloc/vzalloc。

进程栈的内存分配也是lazy的,因为进程栈对应的VMA的vma->vm_flags带有VM_GROWSDOWN标记,这样,在page fault处理的时候kernel就知道落在了stack区域,就会通过expand_stack(vma, address)将栈扩展(vma区域的vm_start降低),这时如果扩展超过RLIMIT_STACK或RLIMIT_AS的限制,就会返回-ENOMEM。因此,对于进程栈,用户态不用做申请内存的动作,只需将sp下移即可,这是每个函数都要做的事情。
3. OOM(Out Of Memory)

OOM即是内存不够用了,在内核中会选择杀掉某个进程来释放内存,内核会给所有进程打分,分最高的则被杀掉,打分的依据主要是看谁占的内存多(当然是杀掉占内存的多的进程才能释放更多内存)。每个进程的/proc/pid/oom_score就是当前得分,OOM的时候就会选择分数最高的那个杀掉。被干掉之后,OOM的打印中也会打印出这个进程的score。
评分标准(mm/oom_kill.c中的badness()给每个进程一个oom score)有:

    根据resident内存、page table和swap的使用情况,采用百分比乘以10,因此最高1000分,最低0分。
    root用户进程减30分。
    oom_score_adj: oom_score会加上这个值。可以在/proc/pid/oom_score_adj中修改(可以是负数),这样来人为地调整score结果。
    oom_adj: -16~15的系数调整。修改/proc/pid/oom_adj里面的值,是一个系数,因此会在原score上乘上系数。数值越大,score结果就会变得越大,数值为负数时,score就会变得比原值小。

修改了3、4之后,/proc/pid/oom_score中的值就会随之改变。这样的话,就可以人为地干预OOM杀掉的进程。

注意,任何一个zone内存不足,都会触发OOM。

Android就利用了修改评分标准的特点,对于转向后台的进程打分提高,对前台进程的打分降低一点,尽可能防止前台进程退出。而进程进入后台时,Android并不杀死它,而是让他活着,如果这时系统内存足够,那么后台进程就一直活着,下次再调出这个进程时就很快。而如果某时刻内存不够了,那个OOM就会根据评分优先杀掉后台进程,让前台进程活着,而后台进程的重启只是稍微影响用户体验而已。
补充:如何满足DMA的连续内存需求

我们说buddy容易碎,但DMA通常只能操作一段物理上连续的内存,因此我们应该保证系统有足量的连续内存以使DMA正常工作。
1. 预分配一块内存
可以在系统启动时就预留出部分内存给DMA专用,这通常要在bootmem的阶段做,使这部分内存和buddy系统分离。并且需要提供申请释放内存的API给每个有需求的device。可以借用bigphysarea来完成。这种做法的缺点是这块连续内存永远不能给其他地方用(即使有没有被使用),可能被浪费,并且需要额外的物理内存管理。但这种预分配的思想在很多场合是最省力也很常用的。
2. IOMMU
如果device的DMA支持IOMMU(MMU for I/O),也就是DMA内部有自己的MMU,就相当于MMU之于CPU(将virtual address映射到physical address),IOMMU可以将device address映射到physical address。这样就不再需要物理地址连续了。IOMMU相比普通DMA访存要耗时且耗电,不太常见。
3. CMA
处理DMA中的碎片有一个利器,CMA(连续内存分配器,在kernel v3.5-rc1正式被引入)。和磁盘碎片整理类似,这个技术也是将内存碎片整理,整合成连续的内存。因为对一个虚拟地址,它可以在不同时间映射到不同的物理地址,只要内容不变就行,对程序员是透明的。

上面讲了,物理地址映射关系对于CPU来讲是透明的,因此可以说虚拟内存是可移动(movable)的,但内核的内存一般不移动,应用程序一般就可以。应用程序在申请内存的时候可以标记我的这块内存是__GFP_MOVABLE的,让CMA认为可搬移。
CMA的原理就是标记一段连续内存,这段内存平时可以作为movable的页面使用。那么应用程序在申请内存时如果打上movable的标记,就可以从这段连续内存里申请。当然,这段内存慢慢就碎了。当一个设备的DMA需要连续内存的时候,CMA就可以发挥作用了:比如设备想申请16MB连续内存,CMA就会从其他内存区域申请16MB,这16MB可能是碎的,然后将自己区域中已经被分出去的16MB的页面一一搬移到新申请的16MB页面中,这时CMA原来标记的内存就空出来了。CMA还要做一件事情,就是去修改被搬离的页所属的哪些进程的页表,这样才能让用户程序在毫无知觉的情况下继续正常运行。

注意,CMA的API是封装到DMA里面,所以你不能直接调用CMA接口,DMA的底层才用CMA(当然DMA也可以不用CMA机制,如果你的CPU不带CMA就更不用说了)。如果你的系统支持CMA,dma_alloc_coherence()内部就可能用CMA实现。
dts里面可指定哪部分内存是可CMA的。可以是全局的CMA pool,也可以为某个特定设备指定CMA pool。具体填法见内核源码中的 Documentation\devicetree\bindings\reserved-memory\reserved-memory.txt。

 

https://blog.csdn.net/jasonchen_gbd/article/details/79461385

posted @ 2019-04-22 12:08  kissrule  阅读(2960)  评论(0编辑  收藏  举报