内核技术中文网»首页 论坛 圈点 查看内容

0 评论

0 收藏

分享

剖析内核态和用户态的内存分配方式

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

<pre class="public-DraftStyleDefault-pre" data-offset-key="7gu39-0-0"><pre class="Editable-styled" data-block="true" data-editor="fn6a5" data-offset-key="7gu39-0-0"><div data-offset-key="7gu39-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"><span data-offset-key="7gu39-0-0"><span data-text="true">➜ 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 </span></span></div></pre></pre>

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的大小都有哪些:

<pre class="public-DraftStyleDefault-pre" data-offset-key="4e4ab-0-0"><pre class="Editable-styled" data-block="true" data-editor="fn6a5" data-offset-key="4e4ab-0-0"><div data-offset-key="4e4ab-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"><span data-offset-key="4e4ab-0-0"><span data-text="true">#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</span></span></div></pre></pre>

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

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

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

<pre class="public-DraftStyleDefault-pre" data-offset-key="dpjf9-0-0"><pre class="Editable-styled" data-block="true" data-editor="fn6a5" data-offset-key="dpjf9-0-0"><div data-offset-key="dpjf9-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"><span data-offset-key="dpjf9-0-0"><span data-text="true">➜ 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</span></span></div></pre></pre>

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

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

  1. 用户态申请内存时的”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。

原文作者:落尘纷扰

原文链接:http://t.csdn.cn/5aPeO(版权归原作者所有,如有侵权,留言联系删除

回复

举报 使用道具

全部回复
暂无回帖,快来参与回复吧
主题 1545
回复 0
粉丝 2
扫码获取每晚技术直播链接
快速回复 返回顶部 返回列表