1、物理地址和虚拟地址
Linux采用页表机制管理内存,32位系统中页大小一般为4KB,物理内存被划分为连续的页,每一个页都有一个唯一的页号。
为了程序的的可移植性,进程往往需要运行在flat memory中;另外为了方便内核统一管理所有进程的内存布局。诸如此类的原因,Linux进程运行在虚拟地址空间(几乎所有现代操作系统都是这么设计的),虚拟地址空间也是以页为单位进行管理。
Linux进程的虚拟地址空间在实际使用的过程中需要映射到物理内存地址空间中,这是通过MMU和TLB硬件单元实现的。
虚拟地址到物理地址的映射不是一一对应的,而是乱序的,在虚拟地址空间中连续的内存页,在物理地址空间可以是不连续的。
Linux系统中,往往同时运行着很多进程。Linux采用沙箱机制,每一个进程运行在自己的虚拟地址空间,彼此隔离。这样做是为了防止单个进程异常,导致整个系统异常。
2、 MMU和TLB
Linux进程发起的内存访问请求时,都是虚拟地址。虚拟地址会被CPU直接送往MMU,MMU首先在TLBs中查找是否有对应进程(ASID)的匹配的页表项,如果存在则直接引用;否则在内存中加载对应进程的页表,从中找到匹配的页表项。页表项中记录了实际物理页的基地址,再结合虚拟地址的偏移地址(偏移地址直接引用)字段,计算出具体的物理地址。
Linux进程所有的内存访问请求,都必须先通过上述机制计算出物理地址,随后才能完成物理内存的读写。
MMU包含两个物理单元:
1)table walk unit 负责从内存读取页表
2)TLBs (Translation Lookside Buffers) 负责缓存最近使用过的页表项
这里面有一个问题是,每一个进程的虚拟内存地址空间布局基本一致,必然就存在不同进程的虚拟地址一致的情况,那么MMU怎么区分接收到的虚拟地址是属于哪一个进程的呢?答案是ASID(Application Specific ID,也有文档翻译为Address Space ID)。在进行TLB页表项查找时,TLB页表项的属性字段包含ASID标识,用来指定该页表项属于哪一个进程。MMU从内存中读取页表时,是通过task_struct->mm_struct->pgd查找对应页表项的,即ASID只在TLBs中生效。
3、页表机制
Linux内核使用页表机制管理虚拟内存地址空间,页表保存在内存中,由MMU进行加载和解析。
ARMv8 MMU支持4级页表。
Linux内核支持很多芯片平台,为了统一起见,Linux内核使用3级页表,涉及到每一个具体的芯片平台有自己的页表实现。
页表分为全局页表和局部页表,内核维护和使用全局页表,每一个进程拥有自己的局部页表。Linux内核为每一个进程维护一个task_struct结构体(即进程描述符),task_struct->mm_struct结构体成员用来保存该进程的局部页表。
Linux进程上下文切换时,内核负责加载当前进程的局部页表,即构建当前进程的虚拟地址空间。
通常来讲,Linux虚拟地址一般分为4个字段:
1)PGD(Page Global Directory)全局页表项
2)PMD(Page Middle Directory)中间页表项
3)PTE(Page Table)页表
4)Offset 偏移地址
二级页表中,PMD通常直接映射到PGD。
4、缺页中断
Linux进程从用户态进入内核态,只有两种方式——系统调用和缺页中断。
Linux系统中使用fork()系统调用(其他操作系统中通常是spawn()函数)创建进程,其实在内核态是由两个系统调用实现的,即clone()和exec()。clone()系统调用负责拷贝父进程的task_struct结构体,exec()系统调用负责创建虚拟地址空间并加载应用程序。
实际上,Linux系统采用著名的写时复制(Copy-On-Write)技术。首先分配好虚拟地址区域,但是并没有实际映射到物理内存,只有在真正要使用时,才分配、映射并读写物理内存。不管是malloc()函数还是ld加载器加载应用程序都是这么实现的。而这一切的基础就是缺页中断。
当Linux进程发起内存访问请求时,发出的是虚拟地址,这个时候MMU就会到TLBs或者内存中加载页表,并查找是否有匹配的页表项。如果没有找到匹配的页表项,则产生缺页中断,由Linux内核分配物理内存,建立新的页表项。
值得一提的是,物理内存通常是有限的,并不能满足所有进程的内存需求。因此,Linux内核引入了swap机制,用来将不频繁使用的页表项对应的物理内存的内容替换到磁盘的swap分区,从而释放物理内存和页表项,用来满足其他进程的需求。Linux内核线程kwapd负责实现swap机制。
嵌入式Linux系统中,往往不支持swap机制,因为嵌入式Linux的物理存储介质通常为Nor Flash或者Nand Flash,其大小一般都远远小于物理内存大小,而且读写速度慢,从各方面看都不能发挥出swap机制的优势。因此,这种情况下,就不得不依赖于Linux内核提供的另外一个杀手进程——Low Memory Killer,一旦系统可用内存低于阈值,则狠心杀掉部分进程,从而释放出物理内存。
Linux内核还提供另外一个工具——OOM (Out of Memory Killer),通过设置进程的内存阈值,一旦超过阈值则杀死进程以释放内存。即LMK是从整个系统的维度释放物理内存,OOM从进程的维度释放物理内存。
4.1、__do_page_fault()
Linux内核中,使用__do_page_fault()函数处理缺页中断。
5、进程虚拟地址空间布局
以32位系统为例,说明Linux进程虚拟地址空间的布局。Linux 32位系统中,虚拟地址空间寻址范围是4GB,分为内核空间和用户空间两部分,以CONFIG_PAGE_OFFSET为界,高1GB为内核空间,低3GB为用户空间。
5.1、用户空间
用户空间从低地址到高地址分别为:
1)代码段
2)数据段(存放全局初始化数据)
3)BSS段(存放全局未初始化数据)
4)堆(从低地址向高地址增长,通常用于动态内存分配,如malloc函数)
5)内存映射段(动态库、libc等基础库映射区)
6)栈(进程调用栈,存放局部变量和临时变量)
进程的栈大小默认为8MB,可以通过ulimit -s设置,一般Linux系统支持栈自动扩展,当栈大小不够时,产生缺页中断,扩展栈大小。
线程的栈位于进程的堆中,因为使用pthread_create()创建线程的时候,实际上是调用malloc()函数在进程的堆中分配一段指定大小(因此线程的栈不能动态增长)的内存用来作为线程的栈。
7)环境变量和命令行参数
5.2、内核空间
Linux 32位系统中,内核空间分为直接映射内存段(通常用来做DMA、内核代码段和数据段等),VMALLOC区、持久映射区和高端内存映射区等。但是Linux 64位系统中,这些概念就渐渐模糊了。
每一个进程的内核空间的内容是不一样的,Linux进程的进程描述符(task_struct结构体)存放在内核空间的低地址,这是基于安全考虑的,因为用户态可能被篡改。
6、地址空间范围
ARMv7架构最开始都是使用32位物理地址和32位虚拟地址,其寻址范围都是4GB。但是随着内存技术的发展和软件的不断膨胀,4GB物理内存已经完全不能满足需求了。因此,ARMv7架构后期的处理器引入了LPAE(Large Physical Address Extend)技术,可以通过寄存器配置将物理地址位宽扩展为最多40位,这样就可以最大支持1024GB物理内存,但是虚拟地址位宽仍然维持4GB,因为短时间内单个进程的内存需求还不会超过4GB。
ARMv8架构开始设计时,就开始考虑到了这方面的因素。因此,在LPAE的基础上,支持通过寄存器将物理地址位宽扩展到48位,这样最大支持512TB物理内存,而虚拟地址位宽也可以最大扩展到48位(通过内核配置选项CONFIG_ARM64_VA_BITS配置),极大地扩展了单个进程的寻址范围(像firefox这种包含N多线程的超大进程终于可以肆无忌惮地挥霍内存资源了)。
值得一提的是,如果在64位内核上运行32位应用程序,用户空间的虚拟地址位宽为32位,最大寻址4GB,但是总算是可以不用将高1GB分给内核空间了。
介绍几个相关的内核配置选项:
CONFIG_COMPAT 是否兼容32位应用程序
CONFIG_ARM64_64K_PAGES 是否支持64KB页
CONFIG_ARM64_VA_BITS 虚拟地址位宽
使用cat /proc/<pid>/maps或者pmap -x <pid>命令可以清晰地看到指定进程的虚拟内存地址空间布局。
鸿蒙伊始,天地初开。Linux内核启动阶段,还没有建立起来页表机制,kmalloc也没有实现,这个时候的内核和内核模块面对的是一片荒芜的物理内存,它们都是直接跑马圈地(物理内存)。这个时候,Linux内核使用memblock(前身是bootmem)进行简单的内存管理,记录物理内存的使用情况。
在进一步介绍memblock之前,有必要先了解系统物理内存的使用情况:
1)首先,内存中的某些部分是永久分配给内核的,如内核代码段、内核数据段、ramdisk和device tree占用的物理内存,他们是系统内存的一部分,但是不能被侵占,也不参与内存分配,称为静态内存;
2)其次,GPU、Camera等都需要预留大量连续物理内存,这部分内存称为保留内存(Reserved Memory);
3)最后,内存的其余部分称为动态内存,是由内核管理的物理内存资源;
memblock把物理内存划分为若干内存区,按使用类型分别放在memory和reserved两个集合(数组)中,memory即动态内存的集合,reserved集合包括静态内存和预留内存。
memory类型的内存集合指向memblock_memory_init_regions数组,最多可以记录128个内存区。内核使用mmeblock_add()函数添加memory类型内存区。
reserved类型的内存集合指向memblock_reserved_init_regions数组,最多可以记录128个内存区。内核使用memblock_reserve()函数添加reserved类型内存区。
内核使用memblock_remove()函数移除内存区。系统不会为移除的内存区建立内存映射,这部分内存后续应该由DMA或者CMA管理。
arm64_memblock_init()函数初始化系统预留内存。reserved类型的内存区通常由CMA(Contiguous Memory Allocator,连续内存分配器)管理。因为内存资源非常宝贵,如果reserved内存没有被使用时,CMA将reserved内存分配给用户使用;当驱动需要使用时,则腾出来给驱动使用。但是并不是所有的预留内存都由CMA管理,比如modem,TA等永久分配给其他核心使用的内存空间,内核并不为这部分空间建立内存映射,而是交给DMA(Direct Memory Allocator)管理。
memblock再怎么优化,效率都不高,在要分配内存时都需要进行遍历。buddy系统刚好能解决这个问题:在内部保存一些2的幂次方大小的空闲内存片段,如果要分配3 page,则去4 page列表中取,分配3个之后将剩下的一个放回去,内存释放的过程刚好相反。
buddy伙伴系统每次分配内存都是以页为单元,它维护了各个order的的页面数组。
它的优势是可以快速分配各种大小的内存,缺陷是会引入内存碎片。可以通过cat /proc/buddyinfo获取到各order种的空闲页面数。vmalloc()就是用来解决内存碎片的,它通过搜集这些零碎的内存页,拼凑成大段内存区后出售,因此vmalloc()分配的内存在物理上是不连续的,kmalloc()分配的内存在物理上是连续的。
系统运行时,使用的绝大多数数据结构都很小,为了一个小对象分配4KB显然不划算。因此,Linux引入了slab机制来解决小对象的分配问题。
slab分配器向buddy批发一些内存页,加工切块后散卖出去。比如内核进程管理维护一个进程描述符队列,其中的每一个成员都是一个task_struct结构体(大小约为1.7KB),内核进程管理单元向slab分配器申请一个task_struct结构体数组,由slab进行维护。当创建进程时,从数组中分配一个结构体成员给新进程,当进程注销时,回收task_struct结构体,但是并不释放内存,这样可以实现快速内存分配。
随着大规模多处理器系统和NUMA系统的广泛应用,slab终于暴露出不足:
1)复杂的队列管理
2)管理数据和队列存储开销较大
3)长时间运行partial队列可能会非常长
4)对NUMA支持非常复杂
为了解决这些问题,高手们开发了slub:改造page结构来削减slab管理结构的开销、每个CPU都维护一个本地活动的slab(kmem_cache_cpu)。对于嵌入式Linux系统,开发了slab的模拟层slob。
9、Linux内核态内存分配函数
9.1、kmalloc()
分配物理地址连续的内存区,kmalloc分配的内存单元大小最小为32或者64字节,最大为128KB,一般从低端内存开始分配。
9.2、vmalloc()
分配虚拟地址连续(物理地址不连续)的内存区,vmalloc一般用来分配大块内存,而且一般是高端内存,只有当内存不够时,才会分配低端内存。
9.3、ioremap()
通过ioremap()可以使用内核提供的I/O读写接口读写外设I/O资源,如寄存器组。
9.4、mmap()
通过mmap()可以像读写物理内存一样读写外设控制器的内存资源,如显卡内存。
10、Linux用户态内存分配函数
10.1、malloc()
我们经常调用的malloc()函数是glibc库提供的函数,而不是系统调用。glibc内部使用ptmalloc机制管理内存,ptmalloc运行在用户态,它会首先向内核申请一部分内存,然后在用户态自己进行维护。
ptmalloc 对于申请内存小于 128KB 时,分配是在堆段,使用系统调用 brk() 或者 sbrk()。如果大于 128 KB 的话,分配在映射区,使用系统调用 mmap()。
10.2、calloc()
calloc()分配内存并将申请到的内存清零。
10.3、realloc()
对malloc()申请的内存进行大小调整。
10.4、alloca()
在函数栈帧中分配内存,当函数退出执行时,自动释放内存。
10.5、free()
释放内存,分配内存和释放内存必须成对出现,否则容易产生内存泄漏或者野指针。
11、常用命令
11.1、cat /proc/meminfo
查看系统内存使用情况
# cat /proc/meminfo
MemTotal: kB
MemFree: kB
MemAvailable: kB
Buffers: kB
Cached: kB
SwapCached: kB
Active: kB
Inactive: kB
Active(anon): kB
Inactive(anon): kB
Active(file): kB
Inactive(file): kB
Unevictable: kB
Mlocked: kB
SwapTotal: kB
SwapFree: kB
Dirty: kB
Writeback: kB
AnonPages: kB
Mapped: kB
Shmem: kB
Slab: kB
SReclaimable: kB
SUnreclaim: kB
KernelStack: kB
PageTables: kB
NFS_Unstable: kB
Bounce: kB
WritebackTmp: kB
CommitLimit: kB
Committed_AS: kB
VmallocTotal: kB
VmallocUsed: kB
VmallocChunk: kB
HardwareCorrupted: kB
AnonHugePages: kB
CmaTotal: kB
CmaFree: kB
HugePages_Total:
HugePages_Free:
HugePages_Rsvd:
HugePages_Surp:
Hugepagesize: kB
DirectMap4k: kB
DirectMap2M: kB
11.2、cat /proc/iomem
查看物理内存分段,动态内存、静态内存和保留内存段
# cat /proc/iomem
-00000fff : reserved
-0009f3ff : System RAM
0009f400-0009ffff : reserved
000a0000-000bffff : PCI Bus :
000c0000-000c7fff : Video ROM
000ca000-000cbfff : reserved
000ca000-000cafff : Adapter ROM
000cc000-000cffff : PCI Bus :
000d0000-000d3fff : PCI Bus :
000d4000-000d7fff : PCI Bus :
000d8000-000dbfff : PCI Bus :
000dc000-000fffff : reserved
000f0000-000fffff : System ROM
-bfeeffff : System RAM
-0185d320 : Kernel code
0185d321-01f4887f : Kernel data
020d1000-02219fff : Kernel bss
bfef0000-bfefefff : ACPI Tables
bfeff000-bfefffff : ACPI Non-volatile Storage
bff00000-bfffffff : System RAM
11.3、cat /proc/<pid>/maps
查看指定进程的虚拟地址空间布局
11.4、pmap
pmap命令是通过解析/proc/<pid>/maps文件,分析指定进程的虚拟地址空间
11.4.1、pmap -d <pid>
# pmap -d
: bash
Address Kbytes Mode Offset Device Mapping
r-x-- : bash
00000000006f3000 r---- 00000000000f3000 : bash
00000000006f4000 rw--- 00000000000f4000 : bash
00000000006fd000 rw--- : [ anon ]
rw--- : [ anon ]
00007f071ff8b000 r-x-- : libnss_files-2.23.so
00007f071ff96000 ----- 000000000000b000 : libnss_files-2.23.so
00007f0720195000 r---- 000000000000a000 : libnss_files-2.23.so
00007f0720196000 rw--- 000000000000b000 : libnss_files-2.23.so
00007f0720197000 rw--- : [ anon ]
00007f072019d000 r-x-- : libnss_nis-2.23.so
00007f07201a8000 ----- 000000000000b000 : libnss_nis-2.23.so
00007f07203a7000 r---- 000000000000a000 : libnss_nis-2.23.so
00007f07203a8000 rw--- 000000000000b000 : libnss_nis-2.23.so
00007f07203a9000 r-x-- : libnsl-2.23.so
00007f07203bf000 ----- : libnsl-2.23.so
00007f07205be000 r---- : libnsl-2.23.so
00007f07205bf000 rw--- : libnsl-2.23.so
00007f07205c0000 rw--- : [ anon ]
00007f07205c2000 r-x-- : libnss_compat-2.23.so
00007f07205ca000 ----- : libnss_compat-2.23.so
00007f07207c9000 r---- : libnss_compat-2.23.so
00007f07207ca000 rw--- : libnss_compat-2.23.so
00007f07207cb000 r---- : locale-archive
00007f0720c55000 r-x-- : libc-2.23.so
00007f0720e15000 ----- 00000000001c0000 : libc-2.23.so
00007f0721015000 r---- 00000000001c0000 : libc-2.23.so
00007f0721019000 rw--- 00000000001c4000 : libc-2.23.so
00007f072101b000 rw--- : [ anon ]
00007f072101f000 r-x-- : libdl-2.23.so
00007f0721022000 ----- : libdl-2.23.so
00007f0721221000 r---- : libdl-2.23.so
00007f0721222000 rw--- : libdl-2.23.so
00007f0721223000 r-x-- : libtinfo.so.5.9
00007f0721248000 ----- : libtinfo.so.5.9
00007f0721447000 r---- : libtinfo.so.5.9
00007f072144b000 rw--- : libtinfo.so.5.9
00007f072144c000 r-x-- : ld-2.23.so
00007f0721652000 rw--- : [ anon ]
00007f072166a000 r--s- : gconv-modules.cache
00007f0721671000 r---- : ld-2.23.so
00007f0721672000 rw--- : ld-2.23.so
00007f0721673000 rw--- : [ anon ]
00007ffc5acca000 rw--- : [ stack ]
00007ffc5adf1000 r---- : [ anon ]
00007ffc5adf4000 r-x-- : [ anon ]
ffffffffff600000 r-x-- : [ anon ]
mapped: 23088K writeable/private: 728K shared: 28K
11.4.2、pmap -x <pid>
# pmap -x
: bash
Address Kbytes RSS Dirty Mode Mapping
r-x-- bash
r-x-- bash
00000000006f3000 r---- bash
00000000006f3000 r---- bash
00000000006f4000 rw--- bash
00000000006f4000 rw--- bash
00000000006fd000 rw--- [ anon ]
00000000006fd000 rw--- [ anon ]
rw--- [ anon ]
rw--- [ anon ]
00007f071ff8b000 r-x-- libnss_files-2.23.so
00007f071ff8b000 r-x-- libnss_files-2.23.so
00007f071ff96000 ----- libnss_files-2.23.so
00007f071ff96000 ----- libnss_files-2.23.so
00007f0720195000 r---- libnss_files-2.23.so
00007f0720195000 r---- libnss_files-2.23.so
00007f0720196000 rw--- libnss_files-2.23.so
00007f0720196000 rw--- libnss_files-2.23.so
00007f0720197000 rw--- [ anon ]
00007f0720197000 rw--- [ anon ]
00007f072019d000 r-x-- libnss_nis-2.23.so
00007f072019d000 r-x-- libnss_nis-2.23.so
00007f07201a8000 ----- libnss_nis-2.23.so
00007f07201a8000 ----- libnss_nis-2.23.so
00007f07203a7000 r---- libnss_nis-2.23.so
00007f07203a7000 r---- libnss_nis-2.23.so
00007f07203a8000 rw--- libnss_nis-2.23.so
00007f07203a8000 rw--- libnss_nis-2.23.so
00007f07203a9000 r-x-- libnsl-2.23.so
00007f07203a9000 r-x-- libnsl-2.23.so
00007f07203bf000 ----- libnsl-2.23.so
00007f07203bf000 ----- libnsl-2.23.so
00007f07205be000 r---- libnsl-2.23.so
00007f07205be000 r---- libnsl-2.23.so
00007f07205bf000 rw--- libnsl-2.23.so
00007f07205bf000 rw--- libnsl-2.23.so
00007f07205c0000 rw--- [ anon ]
00007f07205c0000 rw--- [ anon ]
00007f07205c2000 r-x-- libnss_compat-2.23.so
00007f07205c2000 r-x-- libnss_compat-2.23.so
00007f07205ca000 ----- libnss_compat-2.23.so
00007f07205ca000 ----- libnss_compat-2.23.so
00007f07207c9000 r---- libnss_compat-2.23.so
00007f07207c9000 r---- libnss_compat-2.23.so
00007f07207ca000 rw--- libnss_compat-2.23.so
00007f07207ca000 rw--- libnss_compat-2.23.so
00007f07207cb000 r---- locale-archive
00007f07207cb000 r---- locale-archive
00007f0720c55000 r-x-- libc-2.23.so
00007f0720c55000 r-x-- libc-2.23.so
00007f0720e15000 ----- libc-2.23.so
00007f0720e15000 ----- libc-2.23.so
00007f0721015000 r---- libc-2.23.so
00007f0721015000 r---- libc-2.23.so
00007f0721019000 rw--- libc-2.23.so
00007f0721019000 rw--- libc-2.23.so
00007f072101b000 rw--- [ anon ]
00007f072101b000 rw--- [ anon ]
00007f072101f000 r-x-- libdl-2.23.so
00007f072101f000 r-x-- libdl-2.23.so
00007f0721022000 ----- libdl-2.23.so
00007f0721022000 ----- libdl-2.23.so
00007f0721221000 r---- libdl-2.23.so
00007f0721221000 r---- libdl-2.23.so
00007f0721222000 rw--- libdl-2.23.so
00007f0721222000 rw--- libdl-2.23.so
00007f0721223000 r-x-- libtinfo.so.5.9
00007f0721223000 r-x-- libtinfo.so.5.9
00007f0721248000 ----- libtinfo.so.5.9
00007f0721248000 ----- libtinfo.so.5.9
00007f0721447000 r---- libtinfo.so.5.9
00007f0721447000 r---- libtinfo.so.5.9
00007f072144b000 rw--- libtinfo.so.5.9
00007f072144b000 rw--- libtinfo.so.5.9
00007f072144c000 r-x-- ld-2.23.so
00007f072144c000 r-x-- ld-2.23.so
00007f0721652000 rw--- [ anon ]
00007f0721652000 rw--- [ anon ]
00007f072166a000 r--s- gconv-modules.cache
00007f072166a000 r--s- gconv-modules.cache
00007f0721671000 r---- ld-2.23.so
00007f0721671000 r---- ld-2.23.so
00007f0721672000 rw--- ld-2.23.so
00007f0721672000 rw--- ld-2.23.so
00007f0721673000 rw--- [ anon ]
00007f0721673000 rw--- [ anon ]
00007ffc5acca000 rw--- [ stack ]
00007ffc5acca000 rw--- [ stack ]
00007ffc5adf1000 r---- [ anon ]
00007ffc5adf1000 r---- [ anon ]
00007ffc5adf4000 r-x-- [ anon ]
00007ffc5adf4000 r-x-- [ anon ]
ffffffffff600000 r-x-- [ anon ]
ffffffffff600000 r-x-- [ anon ]
---------------- ------- ------- -------
total kB
11.5、free
# free
total used free shared buff/cache available
Mem:
Swap:
11.6、size
查看可执行文件的代码段、数据段、BSS段大小。
# size tracee
text data bss dec hex filename
tracee
11.7、cat /proc/buddyinfo
查看buddy信息。
11.8、cat /proc/slabinfo
查看slab信息。
11.9、cat /proc/zoneinfo
11.10、cat /proc/vmallocinfo
11.11、cat /proc/vmstat
11.12、cat /proc/kpagecounts
11.13、cat /proc/kpageflags
11.14、cat /proc/pagetypeinfo
11.15、cat /proc/<pid>/smaps
11.16、echo m > /proc/sysrq-trigger
12、参考文献
[1] Understanding Linux Virtual Memory Manager
[2] Learn the Architecture Memory Management Unit
[3] Linux on AArch64