1.使用buddy系统管理ZONE
所有zone都是通过buddy系统管理的,buddysystem由HarryMarkowitz在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不行,DMAengine上面没有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”特指structkmem_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的各阶。
相关视频推荐
90分钟了解Linux显存构架,numa的优势,slab的实现,vmalloc原理
看懂显存管理,先从Linux内核中的《内存管理构架》开始
学习地址:C/C++Linux服务器开发/后台构架师【零声教育】-学习视频教程-腾讯课堂
须要C/C++Linux服务器构架师学习资料加qun812855908(资料包括C/C++,Linux,golang技术,内核,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,解释器,DPDK,ffmpeg,大厂笔试题等)
注意,vmalloc是直接向buddy要显存的,不经过slab,因而vmalloc申请显存的最小单位是一页。slab只从lowmem申请显存,因而领到的显存在数学上是连续的,vmalloc可以从低端和高端拿显存。而用户态的malloc是通过brk/mmap系统调用每次向内核申请一页,之后在标准库里再做进一步管理供用户程序使用。
2.用户态申请显存时的”lazyallocation”
用户态申请显存时,库函数并不会立刻从内核里去拿,而是COW(copyonwrite)的,要不然用户态细细碎碎的申请释放都要跟内核打交道,那频繁系统调用的代价太大,而且内核每次都是一页一页的分配给用户态,小显存让内核很难受的。
然而库函数会误导用户程序,你申请10MB,就让你以为早已拥有了10MB显存,而且只有你真的要用的时侯,C库就会一点一点的从内核申请,直至10MB都领到,这就是用户态申请显存的lazy模式。有时用户程序为了立刻领到这10MB显存,在申请完以后,会立刻把显存写一遍(比如memset为0),让C库将显存都真正申请到。Linux会误导用户程序,但不误导内核,内核中的kmalloc/vmalloc就真的是要一个字节显存就没了一个字节。
因而malloc在刚申请(brk或mmap)的时侯,10MB所有页面在页表中全都映射到同一个零化页面(ZERO_PAGE,全局共享的页,页的内容总是0,用于zero-mappedmemoryareas等用途),内容全是0,且页表上标记这10MB是只读的,在写的时侯发生pagefault,才去一页一页的分配显存和更改页表。所以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标记,这样,在pagefault处理的时侯kernel就晓得落在了stack区域,都会通过expand_stack(vma,address)将栈扩充(vma区域的vm_start增加),这时假若扩充超过RLIMIT_STACK或RLIMIT_AS的限制,都会返回-ENOMEM。因而,对于进程栈,用户态不用做申请显存的动作,只需将sp下移即可,这是每位函数都要做的事情。
3.OOM(OutOfMemory)
OOM即是显存不够用了,在内核中会选择杀掉某个进程来释放显存,内核会给所有进程打分linux软件下载,分最高的则被杀掉,打分的根据主要是看谁占的显存多(其实是杀掉占显存的多的进程能够释放更多显存)。每位进程的/proc/pid/oom_score就是当前得分,OOM的时侯才会选择分数最高的那种杀掉。被杀死然后,OOM的复印中也会复印出这个进程的score。
评分标准(mm/oom_kill.c中的badness()给每位进程一个oomscore)有:
按照resident显存、pagetable和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上面的值linux查看c内存,是一个系数,因而会在原score上乘上系数。数值越大,score结果都会显得越大,数值为正数时,score都会显得比原值小。
更改了3、4以后,/proc/pid/oom_score中的值都会急剧改变。这样的话,就可以人为地干预OOM杀掉的进程。
注意,任何一个zone显存不足,就会触发OOM。
Android就借助了更改评分标准的特性,对于转向后台的进程打分增强,对前台进程的打分增加一点,尽可能避免前台进程退出。而进程步入后台时,Android并不杀害它,而是让他活着,假如这时系统显存足够,这么后台进程就仍然活着,上次再调出这个进程时就很快。而倘若某时刻显存不够了,那种OOM都会按照评分优先杀掉后台进程,让前台进程活着linux系统介绍,而后台进程的重启只是稍为影响用户体验而已。
补充:怎么满足DMA的连续显存需求
我们说buddy容易碎,但DMA一般只能操作一段数学上连续的显存,因而我们应当保证系统有足量的连续显存以使DMA正常工作。
1.预分配一块显存
可以在系统启动时就预留出部份显存给DMA专用,这一般要在bootmem的阶段做,使这部份显存和buddy系统分离。而且须要提供申请释放显存的API给每位有需求的device。可以借用bigphysarea来完成。这些做法的缺点是这块连续显存永远不能给其他地方用(虽然有没有被使用),可能被浪费,而且须要额外的数学显存管理。但这些预分配的思想在好多场合是最省力也很常用的。
2.IOMMU
假如device的DMA支持IOMMU(MMUforI/O),也就是DMA内部有自己的MMU,就相当于MMU之于CPU(将virtualaddress映射到physicaladdress),IOMMU可以将deviceaddress映射到physicaladdress。这样就不再须要化学地址连续了。IOMMU相比普通DMA访存要历时且耗电,不太常见。
3.CMA
处理DMA中的碎片有一个神器,CMA(连续显存分配器,在kernelv3.5-rc1即将被引入)。和c盘碎片整理类似,这个技术也是将显存碎片整理,整合成连续的显存。由于对一个虚拟地址,它可以在不同时间映射到不同的化学地址,只要内容不变就行,对程序员是透明的。
前面讲了,化学地址映射关系对于CPU来讲是透明的,因而可以说虚拟显存是可联通(movable)的,但内核的显存通常不联通,应用程序通常就可以。应用程序在申请显存的时侯可以标记我的这块显存是__GFP_MOVABLE的,让CMA觉得可搬动。
CMA的原理就是标记一段连续显存,这段显存平常可以作为movable的页面使用。这么应用程序在申请显存时假如打上movable的标记,就可以从这段连续显存里申请。其实,这段显存渐渐就碎了。当一个设备的DMA须要连续显存的时侯,CMA就可以发挥作用了:例如设备想申请16MB连续显存,CMA都会从其他显存区域申请16MB,这16MB可能是碎的,之后将自己区域中早已被分出去的16MB的页面一一搬动到新申请的16MB页面中,这时CMA原先标记的显存就空下来了。CMA还要做一件事情,就是去更改被搬离的页所属的什么进程的页表,这样就能让用户程序在毫无知觉的情况下继续正常运行。
注意,CMA的API是封装到DMA上面,所以你不能直接调用CMA插口linux查看c内存,DMA的底层才用CMA(其实DMA也可以不用CMA机制,假如你的CPU不带CMA就更不用说了)。假如你的系统支持CMA,dma_alloc_coherence()内部就可能用CMA实现。
dts上面可指定哪部份显存是可CMA的。可以是全局的CMApool,也可以为某个特定设备指定CMApool。具体填法见内核源码中的Documentationdevicetreebindingsreserved-memoryreserved-memory.txt。
本文原创地址://lrxjmw.cn/srjxbxtzzncg.html编辑:刘遄,审核员:暂无