龙空技术网

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

linux技术栈 868

前言:

眼前大家对“nginx1197”可能比较珍视,大家都需要学习一些“nginx1197”的相关文章。那么小编也在网摘上汇集了一些关于“nginx1197””的相关资讯,希望兄弟们能喜欢,咱们快快来学习一下吧!

1. 使用buddy系统管理ZONE

所有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的各阶。

相关视频推荐

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

标签: #nginx1197