内核内存管理与DMA直接内存访问详解
1. 内核内存管理基础
1.1 内存映射相关操作
在进行内存映射时,需要调用
remap_pfn_range
函数,传入计算得到的物理页帧号(PFN)、大小和保护标志。示例代码如下:
if (remap_pfn_range(vma, vma->vm_start, pfn, size,
vma->vm_page_prot)) {
return -EAGAIN;
}
return 0;
同时,需要将自定义的
mmap
函数传递给
struct file_operations
结构体,示例如下:
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
[...]
.mmap = my_mmap,
[...]
};
1.2 Linux 缓存系统概述
缓存是将频繁访问或新写入的数据存储在一个小而快的内存(称为缓存)中的过程。其中,脏内存是指数据(如文件数据)内容已被修改(通常在缓存中)但尚未写回磁盘的内存。缓存中的数据版本比磁盘上的版本新,两者不同步。将缓存数据写回磁盘(后端存储)的机制称为写回。而干净内存是指内容与磁盘同步的文件支持内存。
Linux 会延迟写操作以加速读过程,并通过仅在必要时写入数据来减少磁盘磨损。例如,
dd
命令执行完成并不意味着数据已写入目标设备,因此大多数情况下
dd
会与
sync
命令一起使用。
1.3 缓存的定义与工作原理
缓存是一种临时、小而快的内存,用于保存来自更大且通常非常慢的内存(如硬盘、主内存)的数据副本。当首次读取数据时,例如进程从大而慢的磁盘请求数据,请求的数据会返回给进程,同时访问的数据副本会被跟踪并缓存。后续的读取操作将从缓存中获取数据。任何数据修改都将在缓存中进行,而不是在主磁盘上。当缓存区域的内容被修改且与磁盘版本不同(比磁盘版本新)时,该区域将被标记为脏。当缓存满时,新数据会驱逐最长时间未被访问且闲置的数据,若后续需要这些数据,则需再次从大/慢存储中获取。
1.4 CPU 缓存 - 内存缓存
现代 CPU 上有三种缓存内存,按大小和访问速度排序如下:
| 缓存级别 | 内存大小 | 访问速度 | 特点 |
| ---- | ---- | ---- | ---- |
| L1 缓存 | 通常在 1K 到 64K 之间 | 单个时钟周期内可被 CPU 直接访问,速度最快 | 频繁使用的数据存储在此,直到其他数据使用更频繁且 L1 空间不足时,数据会移动到 L2 缓存 |
| L2 缓存 | 可达几兆字节 | 可在少量时钟周期内访问 | 中间级别缓存,数据从 L2 移动到 L3 缓存遵循此特性 |
| L3 缓存 | - | 比 L1 和 L2 慢,但可能比主内存(RAM)快两倍 | 每个核心可能有自己的 L1 和 L2 缓存,所有核心共享 L3 缓存 |
CPU 缓存主要解决的问题是延迟,间接提高了吞吐量,因为访问未缓存的内存可能需要一些时间。可以用图书馆的例子来类比,图书馆会将最受欢迎的书籍副本放在展示架上以便快速访问,而大量的藏书则存放在大型档案库中,需要图书管理员去取。展示架就类似于缓存,档案库则是大而慢的内存。
1.5 Linux 页面缓存 - 磁盘缓存
页面缓存是 RAM 中页面的缓存,包含最近访问文件的块。RAM 作为磁盘上页面的缓存,即内核的文件内容缓存。缓存的数据可以是常规文件系统文件、块设备文件或内存映射文件。当调用
read()
操作时,内核首先检查数据是否存在于页面缓存中,如果存在则立即返回,否则从磁盘读取数据。
如果进程需要在不涉及缓存的情况下写入数据,需要使用
O_SYNC
标志,该标志保证
write()
命令在所有数据传输到磁盘后才返回;或者使用
O_DIRECT
标志,该标志仅保证数据传输不使用缓存,但实际上
O_DIRECT
依赖于所使用的文件系统,不建议使用。
1.6 专用缓存(用户空间缓存)
- Web 浏览器缓存 :将频繁访问的网页和图像存储在磁盘上,而不是从网络获取。首次访问在线数据可能需要数百毫秒以上,而第二次访问将在仅 10 毫秒内从缓存(磁盘)中获取数据。
- libc 或用户应用缓存 :内存和磁盘缓存实现会尝试猜测用户接下来需要使用的数据,而浏览器缓存则会保留本地副本以备再次使用。
1.7 延迟写入数据到磁盘的原因
延迟写入数据到磁盘主要有两个原因:
-
提高磁盘效率
:更好地利用磁盘特性,例如延迟磁盘访问并在数据达到一定大小时进行处理,可以提高磁盘性能并减少嵌入式系统中 eMMC 的磨损。每次小块写入会合并为一个连续的写入操作。
-
提升应用性能
:允许应用在写入后立即继续执行。写入的数据会被缓存,使得进程可以立即返回,后续的读取操作将从缓存中获取数据,从而使程序响应更迅速。存储设备更喜欢少量的大操作而不是多个小操作,通过延迟报告永久存储上的写入操作,可以避免磁盘引入的延迟问题。
1.8 写缓存策略
根据缓存策略,可以列举出以下几个好处:
- 减少数据访问延迟,从而提高应用性能
- 延长存储设备寿命
- 减少系统负载
- 降低数据丢失风险
缓存算法通常分为以下三种不同策略:
-
直写缓存
:任何写入操作都会自动更新内存缓存和永久存储。这种策略适用于不能容忍数据丢失的应用程序,以及写入后频繁重新读取数据的应用程序(因为数据存储在缓存中,读取延迟较低)。
-
绕写缓存
:与直写缓存类似,但每次写入操作会立即使缓存失效(这对系统来说成本也很高,因为每次写入都会自动使缓存失效)。主要后果是后续的读取操作将从磁盘获取数据,速度较慢,从而增加了延迟。它可以防止缓存被不会被后续读取的数据淹没。
-
回写缓存
:Linux 采用的策略,每次数据发生更改时将数据写入缓存,而不更新主内存中的相应位置。相反,页面缓存中的相应页面会被标记为脏(此任务由 MMU 使用 TLB 完成)并添加到内核维护的列表中。数据仅在指定的时间间隔或特定条件下写入永久存储的相应位置。当页面中的数据与页面缓存中的数据同步时,内核会将页面从列表中移除,不再标记为脏。在 Linux 系统中,可以通过以下命令查看脏页信息:
cat /proc/meminfo | grep Dirty
1.9 刷新线程
回写缓存会延迟页面缓存中的 I/O 数据操作,一组称为刷新线程的内核线程负责处理脏页写回。当满足以下任何一种情况时,会发生脏页写回:
- 当空闲内存低于指定阈值时,为了回收脏页占用的内存。
- 当脏数据持续到特定时间段时,将最旧的数据写回磁盘,以确保脏数据不会永远保持脏状态。
- 当用户进程调用
sync()
和
fsync()
系统调用时,这是一种按需写回操作。
1.10 设备管理资源 - Devres
Devres 是一种内核工具,帮助开发人员在驱动程序中自动释放分配的资源,简化了
init/probe/open
函数中的错误处理。每个资源分配器都有其管理版本,会负责资源的释放。资源分配器分配的内存与设备相关联,
devres
由与
struct device
关联的任意大小内存区域的链表组成。每个
devres
资源分配器会将分配的资源插入列表中,资源将一直可用,直到代码手动释放、设备从系统中分离或驱动程序卸载。每个
devres
条目都与一个释放函数关联,有不同的方式来释放
devres
,但无论如何,所有
devres
条目都会在驱动分离时释放,释放时会调用关联的释放函数,然后释放
devres
条目。
以下是驱动程序可用的资源列表:
- 用于私有数据结构的内存
- 中断(IRQs)
- 内存区域分配(
request_mem_region()
)
- 内存区域的 I/O 映射(
ioremap()
)
- 缓冲区内存(可能带有 DMA 映射)
- 不同框架的数据结构:时钟、GPIO、PWM、USB 物理层、调节器、DMA 等
几乎本章讨论的每个函数都有其管理版本,大多数情况下,管理版本的函数名是在原函数名前加上
devm
前缀。例如,
devm_kzalloc()
是
kzalloc()
的管理版本。参数保持不变,但会向右移动,因为第一个参数是为其分配资源的
struct device
。对于非管理版本的函数已经在参数中包含
struct device
的情况除外。例如:
void *kmalloc(size_t size, gfp_t flags)
void * devm_kmalloc(struct device *dev, size_t size, gfp_t gfp)
当设备从系统中分离或设备驱动程序卸载时,内存会自动释放。如果不再需要内存,也可以使用
devm_kfree()
释放。以下是旧的中断注册方式和使用
devres
的正确方式对比:
// 旧方式
ret = request_irq(irq, my_isr, 0, my_name, my_data);
if(ret) {
dev_err(dev, "Failed to register IRQ.\n");
ret = -ENODEV;
goto failed_register_irq; /* Unroll */
}
// 正确方式
ret = devm_request_irq(dev, irq, my_isr, 0, my_name, my_data);
if(ret) {
dev_err(dev, "Failed to register IRQ.\n");
return -ENODEV; /* Automatic unroll */
}
2. DMA - 直接内存访问
2.1 DMA 概述
DMA(直接内存访问)是计算机系统的一项功能,允许设备在无需 CPU 干预的情况下访问主系统内存 RAM,从而使 CPU 可以专注于其他任务。通常用于加速网络流量,但支持任何类型的复制操作。DMA 控制器是负责 DMA 管理的外设,常见于现代处理器和微控制器中。当需要传输一块数据时,处理器会将源地址、目标地址和总字节数提供给 DMA 控制器,DMA 控制器会自动将数据从源地址传输到目标地址,而不占用 CPU 周期。当剩余字节数达到零时,块传输结束。
2.2 设置 DMA 映射
对于任何类型的 DMA 传输,都需要提供源地址、目标地址以及要传输的字数。在外设 DMA 的情况下,外设的 FIFO 可以作为源或目标。当外设作为源时,内存位置(内部或外部)作为目标地址;当外设作为目标时,内存位置(内部或外部)作为源地址。根据传输方向,需要指定源或目标,即 DMA 传输需要合适的内存映射。
2.3 缓存一致性与 DMA
如前文所述,最近访问的内存区域副本会存储在缓存中,这也适用于 DMA 内存。实际上,两个独立设备共享的内存通常是缓存一致性问题的根源。缓存不一致是由于其他设备可能不知道写入设备的更新而导致的问题。而缓存一致性确保每个写入操作看起来是即时发生的,使得共享同一内存区域的所有设备看到完全相同的更改序列。
以下是一个缓存一致性问题的示例场景:假设一个配备缓存的 CPU 和一个可以通过 DMA 直接访问的外部内存。当 CPU 访问内存中的位置 X 时,当前值会存储在缓存中。后续对 X 的操作将更新缓存中的 X 副本,但不会更新外部内存中的 X 版本(假设是回写缓存)。如果在下一次设备尝试访问 X 之前缓存没有刷新到内存,设备将收到 X 的陈旧值。同样,如果设备向内存写入新值时缓存中的 X 副本没有失效,CPU 将对 X 的陈旧值进行操作。
解决这个问题有两种方法:
-
基于硬件的解决方案
:这样的系统是一致系统。
-
基于软件的解决方案
:操作系统负责确保缓存一致性,这样的系统称为非一致系统。
2.4 DMA 映射类型
任何合适的 DMA 传输都需要合适的内存映射。DMA 映射包括分配 DMA 缓冲区并为其生成总线地址,设备实际使用的是总线地址,总线地址是
dma_addr_t
类型的实例。DMA 映射分为两种类型:
-
连贯 DMA 映射
:可以在多次传输中使用,会自动解决缓存一致性问题,但成本较高。通常在驱动程序的生命周期内存在,适用于需要长期使用的缓冲区。
-
流式 DMA 映射
:有更多限制,不会自动解决一致性问题,但可以通过每次传输之间的几个函数调用来解决。通常在 DMA 传输完成后取消映射。
在代码中,处理 DMA 映射需要包含以下头文件:
#include <linux/dma-mapping.h>
2.5 连贯映射
使用以下函数设置连贯映射:
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag)
该函数负责分配和映射缓冲区,并返回一个内核虚拟地址,该缓冲区大小为
size
字节,可由 CPU 访问。
dev
是设备结构体,第三个参数是指向关联总线地址的输出参数。为映射分配的内存保证是物理连续的,
flag
决定了内存的分配方式,通常使用
GFP_KERNEL
或
GFP_ATOMIC
(如果处于原子上下文)。
这种映射具有以下特点:
- 一致性(连贯):为设备执行 DMA 分配无缓存、无缓冲的内存。
- 同步性:设备或 CPU 的写入操作可以立即被对方读取,无需担心缓存一致性问题。
使用以下函数释放映射:
void dma_free_coherent(struct device *dev, size_t size,
void *cpu_addr, dma_addr_t dma_handle);
其中,
cpu_addr
对应
dma_alloc_coherent()
返回的内核虚拟地址。这种映射成本较高,最小分配单位是一个页面,实际上只分配 2 的幂次方数量的页面。可以使用
int order = get_order(size)
获取页面的阶数。应将这种映射用于设备生命周期内持续使用的缓冲区。
2.6 流式 DMA 映射
流式映射与连贯映射不同,具有以下特点:
- 需要与已经分配的缓冲区一起工作。
- 可以接受多个非连续和分散的缓冲区。
- 映射的缓冲区属于设备而不是 CPU,在 CPU 使用缓冲区之前,需要先取消映射(在
dma_unmap_single()
或
dma_unmap_sg()
之后),这是为了缓存目的。
- 对于写事务(CPU 到设备),驱动程序应在映射之前将数据放入缓冲区。
- 需要指定数据移动的方向,并且数据只能根据该方向使用。
不能在缓冲区未取消映射时访问它的原因是 CPU 映射是可缓存的。用于流式映射的
dma_map_*()
系列函数会首先清理/使与缓冲区相关的缓存失效,并依赖 CPU 在相应的
dma_unmap_*()
之前不访问该缓冲区。之后,如果有任何推测性预取,会再次使缓存失效,然后 CPU 才能读取设备写入内存的数据。
流式映射有两种形式:
-
单缓冲区映射
:只允许一页映射。
-
分散/聚集映射
:允许传递多个(分散在内存中的)缓冲区。
综上所述,内核内存管理和 DMA 直接内存访问是计算机系统中非常重要的部分,它们相互配合,共同提高了系统的性能和效率。通过合理使用缓存策略、内存映射和 DMA 技术,可以优化系统资源的利用,减少 CPU 负载,提升应用程序的响应速度和整体性能。
2.7 DMA 映射操作流程总结
为了更清晰地理解 DMA 映射的操作流程,下面用 mermaid 流程图展示连贯映射和流式映射的主要操作步骤:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B{选择映射类型}:::decision
B -->|连贯映射| C(调用 dma_alloc_coherent):::process
C --> D(使用映射的缓冲区进行 DMA 操作):::process
D --> E(调用 dma_free_coherent 释放映射):::process
E --> F([结束]):::startend
B -->|流式映射| G(准备已分配的缓冲区):::process
G --> H{选择映射形式}:::decision
H -->|单缓冲区映射| I(调用 dma_map_single):::process
H -->|分散/聚集映射| J(调用 dma_map_sg):::process
I --> K(进行 DMA 操作):::process
J --> K
K --> L(调用 dma_unmap_single 或 dma_unmap_sg 取消映射):::process
L --> F
这个流程图展示了连贯映射和流式映射的基本操作流程。对于连贯映射,先分配映射,然后进行 DMA 操作,最后释放映射;对于流式映射,先准备缓冲区,选择映射形式,进行映射,完成 DMA 操作后取消映射。
2.8 DMA 映射的使用建议
在实际使用中,应根据具体需求选择合适的 DMA 映射类型:
-
选择连贯映射的情况
:当需要在多次 DMA 传输中使用同一缓冲区,并且对缓存一致性有严格要求,同时不介意较高的成本时,应选择连贯映射。例如,对于一些需要长期稳定运行的设备驱动程序,其缓冲区在设备的整个生命周期内都需要使用,此时连贯映射是一个不错的选择。
-
选择流式映射的情况
:当缓冲区已经分配好,并且可能需要处理多个非连续的缓冲区,对成本较为敏感,并且能够在每次传输之间进行额外的缓存管理操作时,应选择流式映射。例如,在一些数据处理场景中,数据可能分散在不同的内存区域,此时流式映射的分散/聚集功能就可以很好地发挥作用。
2.9 缓存策略与 DMA 的综合应用
在实际系统中,缓存策略和 DMA 技术通常需要综合应用,以达到最佳的性能和资源利用效果。以下是一些综合应用的建议:
-
结合回写缓存和 DMA
:Linux 采用的回写缓存策略可以与 DMA 技术相结合。在进行 DMA 传输时,利用回写缓存可以减少对磁盘的直接写入操作,提高系统的写入性能。例如,当设备通过 DMA 将数据写入内存时,数据可以先存储在页面缓存中,标记为脏页,然后由刷新线程在合适的时机将数据写回磁盘。
-
根据缓存一致性问题调整 DMA 映射
:在存在缓存一致性问题的系统中,应根据具体情况选择合适的 DMA 映射类型。对于一致系统,可以使用连贯映射来自动解决缓存一致性问题;对于非一致系统,则需要在软件层面进行额外的缓存管理操作,如在流式映射中使用
dma_map_*()
和
dma_unmap_*()
函数来清理和使缓存失效。
2.10 总结
本文详细介绍了内核内存管理和 DMA 直接内存访问的相关知识,包括内存映射、缓存系统、写缓存策略、Devres 机制以及 DMA 映射的类型和操作等内容。通过对这些知识的理解和应用,可以更好地优化计算机系统的性能和资源利用。
- 内核内存管理 :通过合理使用缓存策略,如直写缓存、绕写缓存和回写缓存,可以减少数据访问延迟,提高应用性能,延长存储设备寿命,降低数据丢失风险。同时,Devres 机制可以简化驱动程序中的资源管理和错误处理。
- DMA 直接内存访问 :DMA 技术允许设备在无需 CPU 干预的情况下访问主系统内存,提高了系统的吞吐量。通过选择合适的 DMA 映射类型,如连贯映射和流式映射,可以解决缓存一致性问题,并根据具体需求进行灵活的内存管理。
在实际应用中,应根据系统的具体需求和特点,综合考虑内核内存管理和 DMA 技术的应用,以达到最佳的性能和资源利用效果。例如,在嵌入式系统中,需要考虑设备的资源限制和性能要求,选择合适的缓存策略和 DMA 映射类型,以提高系统的稳定性和响应速度。
424

被折叠的 条评论
为什么被折叠?



