内存功能
内存 |
---|
内存访问 |
虚拟内存 |
内存映射 |
按需分页和交换 |
逻辑内存 |
页面分配器 |
页面 |
内核可以完全访问系统的内存,并允许进程安全地访问这些内存,以满足它们的需要。通常,第一步是使用虚拟寻址,通常通过分页和/或分段实现。虚拟寻址允许内核使给定的物理地址看起来像另一个地址,即虚拟地址。对于不同的进程,虚拟地址空间可能不同;一个进程在特定(虚拟)地址访问的内存可能与另一个进程在同一地址访问的内存不同。这允许每个程序都表现得像它唯一运行的程序一样(除了内核),从而防止应用程序相互崩溃。
在许多系统上,程序的虚拟地址可能引用当前不在内存中的数据。虚拟寻址提供的间接层允许操作系统使用其他数据存储(如硬盘驱动器)来存储那些原本必须保留在主随机存取内存(RAM)中的数据。因此,操作系统可以允许程序使用比系统实际可用的内存更多的内存。当程序需要当前不在 RAM 中的数据时,MMU会向内核发出信号,指示发生了这种情况,内核会相应地将非活动内存块的内容写入磁盘(如有必要),并用程序请求的数据替换它。然后,程序可以从它停止的位置恢复执行。这种方案通常称为按需分页。
虚拟寻址还允许在两个不相交区域创建虚拟内存分区,一个区域保留给内核(内核空间),另一个区域保留给应用程序(用户空间)。处理器不允许应用程序访问内核内存,从而防止应用程序损坏正在运行的内核。这种基本的内存空间分区极大地促进了实际通用内核的当前设计,并且在这些系统中几乎是通用的,Linux 就是其中之一。
⚲ Shell 接口
- cat /proc/meminfo
- man 1 free
- man 8 vmstat
- ⚲ man 2 brk ↪ sys_brk id, do_brk_flags id 动态更改调用进程的数据段大小。
更改是通过重置进程的程序断点来完成的,程序断点决定了可以分配的最大空间。程序断点是数据区域当前末尾之后的第一个位置的地址,它决定了进程可以分配的最大空间。随着断点值增加,可用空间量也会增加。添加的可用空间被初始化为零值。
- ⚲ man 2 mmap ↪ ksys_mmap_pgoff id 将文件或设备映射到内存。
它是内存映射文件 I/O 的一种方法。它自然地实现了按需分页,因为文件内容最初没有从磁盘读取,并且根本不使用物理 RAM。实际的磁盘读取是在访问特定位置后以“延迟”方式执行的。在不再需要内存后,重要的是要man 2 unmmap 指向它的指针。可以使用man 2 mprotect ↪ do_mprotect_pkey id 管理保护信息,并且可以使用man 2 madvise ↪ do_madvise id 强制执行特殊处理。在 Linux 中,man 2 mmap 可以创建几种类型的映射,例如匿名映射、共享映射和私有映射。使用MAP_ANONYMOUS
标志mmap()可以映射进程虚拟内存中的特定区域,该区域不受任何文件的支持,其内容被初始化为零。
这些函数通常从更高层的内存管理库函数调用,例如 C 标准库man 3 malloc 或C++ new 运算符。
💾 历史:Linux 从 Unix 继承了两个与内存管理系统调用相关的基本调用:brk 和 mmap。
BTW:在 Linux 中,man 2 sbrk 不是单独的系统调用,而是一个 C 库函数,它也调用sys_brk id 并保持一些内部状态以返回之前的断点值。
📚 参考文献
⚙️ 内部机制
🔧 TODO
🗝️ 首字母缩略词
- VPFN - 虚拟页面帧编号
- PFN - 物理页面帧编号
- pgd - 页面目录
- pmd - 页面中间目录
- pud - 页面上层目录
- pte - 页表条目
- TLB - 转换后备缓冲器
- MMU - 内存管理单元
⚲ API
⚙️ 内部机制
📚 参考文献
⚲ API
- linux/types.hinc
- linux/kref.hinc
- list_head id - 通用双向链表
- linux/list.h inc - 基本 list_head id 操作
- linux/klist.h inc - 一些 klist_node id->kref id 辅助函数
- klist_add_tailid ...
- linux/kobject.hinc
- linux/circ_buf.hinc
- linux/kfifo.h inc - 通用内核 FIFO
- kfifo_inid ...
- linux/rbtree.h inc - 红黑树
- linux/scatterlist.hinc
- linux/idr.h inc - ID 分配
- linux/bitmap.hinc
📚 参考文献
内存映射
[edit | edit source]🔧 TODO
关键项目
man 2 mmap man 2 mprotect man 2 mmap2 man 2 mincore man 2 ksys_mmap_pgoff
do_mmap id mm_struct id vm_area_struct id vm_struct id remap_pfn_range id SetPageReserved id ClearPageReserved id free_mmap_pages alloc_mmap_pages free_mmap_pages id
⚲ API
⚙️ 内部机制
📚 参考文献
交换
[edit | edit source]🔧 TODO
⚲ API
- cat /proc/sys/vm/swappiness ↪ vm_swappiness id
- linux/swap.hinc
- man 2 swapon ↪ enable_swap_slots_cache id
- man 2 swapoff
- man 2 mlock ↪ do_mlock id
- man 2 shmctl ↪ shmctl_do_lock id
⚙️ 内部机制
VM_LOCKED id swap_info_struct id si_swapinfo id swap_info id handle_pte_fault id do_swap_page id wakeup_kswapd id kswapd id
📚 参考文献
逻辑内存
[edit | edit source]⚲ kmalloc id 是内核中为小于页面大小的对象分配内存的常用方法。它在 linux/slab.h inc 中定义。第一个参数 size 是要分配的内存块的大小(以字节为单位)。第二个参数 flags 是分配标志或 GFP 标志,它是一组宏,调用者通过它们来控制所请求内存的类型。最常用的 flags 值是 GFP_KERNEL 和 GFP_ATOMIC,但还有更多需要考虑的因素。
内核中的内存分配请求始终由一组 GFP 标志(“GFP” 最初来自“get free page”)限定,这些标志描述了为了满足请求可以和不可以做什么。最常用的标志是 GFP_ATOMIC 和 GFP_KERNEL,尽管它们实际上是由更底层的标志构建的。完整的标志集非常庞大;它们可以在 linux/gfp.h inc 头文件中找到。
⚲ API
- ↯ RAII 分配函数层次结构来自 linux/device.h inc
- devm_kcalloc id - 清零数组
- devm_kzalloc id - 清零分配
- devm_kmalloc id - 通用分配
- 经典直接 API
Slab 分配
[edit | edit source]Slab 分配 是一种内存管理算法,旨在有效地为内核对象分配内存。它消除了由分配和释放导致的碎片。该技术用于保留分配的内存,其中包含特定类型的数据对象,以便在随后分配相同类型对象的分配中重新使用。
基础知识
本节介绍 SLAB 和 SLUB 分配器实现
可以将 slab 想象成一个跨越一个或多个连续内存页的、包含特定类型或大小对象的数组;例如,名为“task_struct”的 slab 包含 struct task_struct
类型的对象,供调度子系统使用。其他 slab 存储其他子系统使用的对象,还有一些 slab 用于内核内部的动态分配,例如“kmalloc-64” slab,它保存通过 kmalloc() 调用请求的最多 64 字节的块。在一个 slab 中,每个对象可以单独分配和释放。
slab 分配的主要动机是,内核数据对象的初始化和销毁实际上可能超过为它们分配内存的成本。由于对象创建和删除被内核广泛使用,初始化的开销可能会导致性能大幅下降。因此引入了对象缓存的概念,以避免调用用于初始化对象状态的函数。
使用 slab 分配,会预先分配适合容纳特定类型或大小的数据对象的内存块。slab 分配器会跟踪这些块,这些块被称为缓存 kmalloc_caches id,因此当收到为特定类型的数据对象分配内存的请求时,它可以使用已经分配的插槽 slab_alloc id 立即满足请求。
使用 kfree id 释放对象不会释放内存,而只是打开一个插槽,该插槽由 slab 分配器放入空闲插槽列表 kmem_cache_cpu id 中。下次调用分配相同大小的内存时,将返回现在未使用的内存插槽。参见 slab_alloc id//___slab_alloc id/get_freelist id。此过程消除了搜索合适内存空间的需要,并极大地缓解了内存碎片。在这种情况下,slab 是内存中包含预先分配的内存块的一个或多个连续页。
slab 分配为内核中那些需要比标准 4KB 页大小更灵活的内存分配的部分提供了一种面向 zoned buddy 分配器的前端。
⚲ 接口
- sudo cat /proc/slabinfo
- linux/slab.hinc
- kmem_cache_alloc id, kmem_cache_free id
- man 1 slabtop
⚙️ 内部机制
SLUB 分配器 – 默认的非排队分配器
SLUB 是原始 SLAB 分配器的迭代版本,它取代了原始 SLAB 分配器,并从 2.6.23 版本起成为 Linux 的默认分配器。
⚙️ 内部实现: mm/slub.c src
📚 参考文献
SLOB 分配器 – 用于 🤖 嵌入式设备的简单块列表
不幸的是,SLAB 和 SLUB 分配器会消耗大量内存来分配它们的 slab,这对内存受限的小型系统(如嵌入式系统)来说是一个严重的缺点。为了克服这个问题,Matt Mackall 在 2006 年 1 月设计了 SLOB(简单块列表)分配器,它是一种更简单的分配内核对象的方法。
SLOB 分配器使用首次适应算法,它选择第一个可用的空间作为内存。此算法减少了内存消耗,但此方法的一个主要限制是它非常容易受到内部碎片的影响。
当没有定义 slab 分配器时(当 CONFIG_SLAB id 标志被禁用时),内核构建系统也会使用 SLOB 分配器作为后备。
⚙️ 内部实现: mm/slob.c src, slob_alloc id
SLAB 分配器
💾 历史:SLAB 分配器是内核中第一个 slab 分配实现的名称,用于将其与使用相同接口的后续分配器区分开来。它很大程度上基于 Jeff Bonwick 的论文“The Slab Allocator: An Object-Caching Kernel Memory Allocator”(1994),该论文描述了在 Solaris 5.4 内核中实现的第一个 slab 分配器。
SLAB 是赋予内核中第一个 slab 分配实现的名称,用于将其与之后使用相同接口的分配器区分开来。它很大程度上基于 Jeff Bonwick 的论文“The Slab Allocator: An Object-Caching Kernel Memory Allocator”(1994 年),该论文描述了在 Solaris 5.4 内核中实现的第一个 slab 分配器。
⚙️ 内部实现: mm/slab.c src
📚 Slab 分配的参考资料
- KASAN - KernelAddressSANitizer doc - 用于查找越界和使用后释放错误的动态内存安全错误检测器
- 视频“SL[AUO]B:内核内存分配器设计和理念” Christopher Lameter (Linux.conf.au 2015 大会) 幻灯片
页面分配器
[edit | edit source]页分配器(或“zoned buddy 分配器”)是一个处理物理内存的底层分配器。它将物理页(通常大小为 4096 字节)的空闲内存交付给高级内存使用者,例如 slab 分配器和 kmalloc()
。作为系统中内存的最终来源,页分配器必须确保始终有可用内存,因为无法为关键内核子系统提供内存可能会导致系统整体故障或内核崩溃。
页分配器将物理内存划分为“区域”,每个区域对应于 zone_type id,具有特定的特征。ZONE_DMA 包含地址范围底部的内存,供严重受限的设备使用,例如,而 ZONE_NORMAL id 可能包含系统中的大部分内存。32 位系统有一个 ZONE_HIGHMEM 用于未直接映射到内核地址空间的内存。根据任何给定分配请求的特性,页分配器将按照特定优先级顺序搜索可用的区域。对于好奇的人来说,/proc/zoneinfo提供了有关任何给定系统上正在使用的区域的大量信息。
在一个区域内,内存被分组为页块,每个块可以使用一个迁移类型进行标记 - migratetype id 描述了块的分配方式。
⚲ API
- cat /proc/buddyinfo
- linux/gfp.hinc
- linux/mmzone.hinc
- alloc_pageid
- devm_get_free_pages id - RAII 函数,↯ 其层次结构
- __get_free_pagesid
- alloc_pagesid
- alloc_pages_nodeid
- __alloc_pages id - zoned buddy 分配器的“核心”
- alloc_pages_nodeid
- alloc_pagesid
- __get_free_pagesid
⚙️ 内部机制
- build_all_zonelists id 从 start_kernel id 中调用,↯ 调用层次结构
- __alloc_pages id - zoned buddy 分配器的“核心”
- struct zone id
- mm/mmzone.csrc
- mm/page_alloc.csrc
📚 参考文献
📚 逻辑内存的参考资料
物理内存
[edit | edit source]内存布局
[edit | edit source]32 位处理器最多可以寻址 4GB 内存。Linux 内核将 4GB 地址空间划分为用户进程和内核;在最常见的配置下,32 位范围的前 3GB 划归用户空间,内核从 0xc0000000 开始获取最后的 1GB。共享地址空间提供了许多性能优势;特别是,硬件的地址转换缓冲区可以在内核和用户空间之间共享。
在 x86-64 架构下 - CONFIG_X86_64 id,使用 4 级页表 (CONFIG_X86_5LEVEL id=n) 时,虚拟内存地址的低 48 位将用于地址转换(页表查找)。虚拟地址的第 48 到 63 位必须是第 47 位的副本,否则处理器将引发异常。符合此规则的地址被称为“规范形式”。规范形式地址范围从 0 到 00007FFF'FFFFFFFF,以及 FFFF8000'00000000 到 FFFFFFFF'FFFFFFFF,总共可以使用 256 TB 的 虚拟地址空间。这仍然大约是 32 位机器上虚拟地址空间的 64,000 倍。
Linux 将地址空间的较高部分用于内核空间,并将较低部分留给用户空间。“规范地址”设计实际上有两个内存部分:较低部分从 00000000'00000000 开始并向上扩展,随着更多虚拟地址位可用而扩展,而较高部分则停靠在地址空间顶部并向下扩展。
起始地址 | 偏移量 | 结束地址 | 大小 | VM 区域描述 |
---|---|---|---|---|
0000'8000'0000'0000
|
+128 TB | ffff'7fff'ffff'ffff
|
... 大量接近 64 位宽的非规范虚拟内存地址孔洞,一直延伸到内核映射的 -128 TB 起始偏移量。 | |
0000'0000'0000'0000
|
0 | 0000'7fff'ffff'ffff
|
128 TB=247 | 用户空间虚拟内存,每个 mm 不同 |
ffff'ffff'ffe0'0000
|
-2 MB | ffff'ffff'ffff'ffff
|
2 MB=221 | ... 未使用的孔洞 |
ffff'ffff'ff60'0000
|
-10 MB | ffff'ffff'ff60'0fff
|
4 kB=212 | VSYSCALL_ADDR id - 遗留 vsyscall ABI |
ffff'ffff'8000'0000
|
-2 GB | ffff'ffff'9fff'ffff
|
512 MB=219 | 内核文本映射,映射到物理地址 0 |
ffff'8880'0000'0000
|
-119.5 TB | ffff'c87f'ffff'ffff
|
64 TB | page_offset_base id = __PAGE_OFFSET_BASE_L4 id - 所有物理内存的直接映射 |
ffff'8000'0000'0000
|
-128 TB | ffff'87ff'ffff'ffff
|
8 TB | ... 保护孔洞,也保留给虚拟机管理程序 |
⚲ API
- man 8 setarch --addr-no-randomize cat /proc/self/maps
⚙️ 内部机制
📚 参考文献
页面
[edit | edit source]在 Linux 中,不同的架构具有不同的页面大小。x86 架构的原始页面大小(也是最常用的页面大小)为 4096 字节 (4 KB)。当前架构的页面大小(以字节为单位)由 PAGE_SIZE
宏定义,该宏包含在 arch/x86/include/asm/page_types.h src 头文件中。用户空间程序可以使用 man 2 getpagesize 库函数获取此值。另一个相关的宏是 PAGE_SHIFT
,它包含将地址左移以获取其页号所需的位数 - 对于 4K 页面,为 12 位。
与内存管理相关的最基本内核数据结构之一是 struct page
。内核使用这种类型的变量来跟踪系统中存在的每个物理内存页面的状态。现代系统中存在数百万个页面,因此内存中也存在数百万个此类结构。
struct page
的完整定义可以在 linux/mm_types.h inc 中找到。
DMA
[edit | edit source]⚲ API
- dma_addr_t id - 总线地址
- linux/dma-mapping.hinc
- dma_alloc_coherentid
- dma_alloc_pages id pin_user_pages id
- dma_map_single id dma_data_direction id
- dma_map_sg id scatterlist id
- dma_set_mask id dma_set_coherent_mask id dma_set_mask_and_coherent id
- dma_sync_single_for_cpu id dma_sync_single_for_device id
- linux/gfp.hinc
- linux/dmapool.hinc
- dma_pool_createid
- 可 DMA 的内存:__get_free_page id kmalloc id kmem_cache_alloc id
- get_user_pages id 将用户页面固定到内存中。
👁 示例
⚙️ 内部机制
📚 参考文献
💾 历史记录:
SAC 单地址周期
DMAEngine
[edit | edit source]- linux/dmaengine.hinc
- drivers/dmasrc
- 驱动程序 API/dmaengine doc
- https://bootlin.com/pub/conferences/2015/elc/ripard-dmaengine/
...
[edit | edit source]📚 文章参考资料