DMA是什么以及DMA的用法
DMA(Direct Memory Access,直接内存访问)是计算机体系中一种核心的硬件加速技术,其核心作用是在不依赖 CPU 干预的情况下,实现外设(如硬盘、网卡、显卡)与内存之间直接的数据传输,从而解放 CPU 资源,提升系统整体数据处理效率。在高性能计算、实时系统、嵌入式设备等场景中,DMA 是实现高吞吐量、低延迟数据传输的关键技术。
一、DMA 核心概念与工作原理
要理解 DMA,首先需要对比传统数据传输与 DMA 传输的差异,明确其技术价值。
1. 传统数据传输 vs DMA 传输
在没有 DMA 的场景中,外设与内存的数据传输必须经过 CPU 中转,流程如下:
- 外设向 CPU 发送“数据就绪”中断请求;
- CPU 暂停当前任务,保存上下文(如寄存器值);
- CPU 从外设寄存器读取数据,写入内存指定地址;
- 重复步骤 3 直到传输完成,CPU 恢复上下文,继续执行原任务。
这种方式的问题在于:CPU 被“绑定”为数据搬运工,无法执行其他计算任务,尤其在传输大量数据(如视频流、文件拷贝)时,会导致 CPU 占用率飙升,系统响应变慢。
而 DMA 传输通过独立的硬件控制器(DMA 控制器)绕过 CPU,直接完成数据搬运,流程如下:
- 初始化阶段:CPU 向 DMA 控制器下发“传输指令”,包含 4 个关键参数: 源地址:数据的起始位置(如外设缓冲区地址);目标地址:数据的写入位置(如内存地址);传输长度:需要传输的字节数;传输方向:外设→内存(如读硬盘)或内存→外设(如写网卡)。
- 传输阶段:DMA 控制器向系统总线(地址总线、数据总线)发出请求,获得总线控制权后,直接在源地址与目标地址之间传输数据,全程无需 CPU 参与;
- 完成阶段:数据传输结束后,DMA 控制器向 CPU 发送“传输完成”中断;
- CPU 响应:CPU 仅需处理中断(如标记数据可用),无需参与数据搬运,可继续执行其他任务。
2. DMA 控制器的核心组件
DMA 功能由硬件层面的 DMA 控制器(DMAC) 实现,其核心组件包括:
- 通道寄存器组:每个 DMA 通道对应一组寄存器,存储该通道的源地址、目标地址、传输长度、传输模式(如单次传输/块传输);
- 优先级仲裁器:当多个外设同时请求 DMA 时,按预设优先级(如固定优先级、轮询优先级)分配总线资源,避免冲突;
- 中断控制器:传输完成/出错时,向 CPU 发送中断信号,触发后续处理(如数据校验、任务切换);
- 总线接口单元:与系统总线(如 PCIe、AHB、AXI)对接,实现地址解码、数据读写等总线操作。
3. DMA 的关键特性
- 无 CPU 干预:仅在初始化和传输完成阶段需要 CPU 参与,核心传输过程由硬件独立完成,降低 CPU 负载;
- 高传输效率:避免 CPU 上下文切换和指令执行的开销,尤其适合 GB 级、TB 级大吞吐量数据传输(如 SSD 读写、网络数据包转发);
- 多通道并行:主流 DMA 控制器支持多个独立通道(如 8 通道、16 通道),可同时为多个外设(如网卡+硬盘+显卡)提供传输服务;
- 灵活传输模式:支持多种传输方式,满足不同场景需求: 块传输(Block Transfer):一次性传输固定长度的数据(如 4KB、1MB),传输期间独占总线,适合连续数据;分散-聚集传输(Scatter-Gather):将内存中多个不连续的缓冲区地址整合为“描述符表”,DMA 按表依次传输,无需 CPU 拆分数据,常用于网络数据包(如 TCP 分段)、文件碎片读写;循环传输(Circular Transfer):传输完成后自动重置地址,重复传输(如音频/视频流播放),避免频繁初始化;握手传输(Handshake Transfer):与外设通过握手信号(如就绪信号、应答信号)同步,确保数据传输的正确性(如低速外设 UART、I2C)。
二、DMA 的应用场景
DMA 广泛存在于计算机、嵌入式设备、服务器等各类硬件中,核心场景均围绕“高吞吐量、低 CPU 占用”的需求展开:
1. 存储设备(硬盘、SSD、U盘)
- 场景:文件拷贝、系统启动、数据库读写等;
- 作用:SSD 与内存之间的读写速度可达 GB/s 级,若依赖 CPU 中转,会导致 CPU 占用率接近 100%(尤其大文件拷贝);通过 DMA,CPU 仅需下发“读/写指令”,数据传输由 SSD 控制器与 DMA 直接完成,CPU 可同时处理其他任务(如浏览器浏览、文档编辑)。
- 典型协议:SATA、NVMe(基于 PCIe)协议中均内置 DMA 功能,NVMe SSD 甚至支持多队列 DMA(Multi-Queue DMA),进一步提升并行性。
2. 网络设备(网卡、无线模块)
- 场景:网络数据包接收/发送(如下载文件、视频通话、服务器转发);
- 作用:千兆网卡每秒需处理数百万个数据包(每个数据包约 1.5KB),若 CPU 逐个接收,会因“中断风暴”(频繁处理数据包中断)导致性能瓶颈;通过 DMA 的“分散-聚集传输”,网卡可直接将多个数据包写入内存的不连续缓冲区,CPU 仅需批量处理已传输完成的数据包,大幅提升网络吞吐量(如服务器可同时处理万级并发连接)。
- 典型技术:网卡的 DMA 功能常与“中断 coalescing(中断聚合)”结合,减少中断次数(如每接收 100 个数据包触发一次中断),进一步降低 CPU 负载。
3. 多媒体设备(显卡、声卡、摄像头)
- 显卡(GPU):GPU 与内存(显存)之间的数据传输(如 3D 模型加载、视频渲染)依赖 DMA,部分高端显卡支持“PCIe DMA 直通”,可直接访问系统内存,避免数据二次拷贝;
- 声卡:音频流(如音乐播放)需持续传输到扬声器,通过 DMA 循环传输模式,可实现“无卡顿”播放,CPU 无需反复写入音频数据;
- 摄像头:实时视频采集(如直播、监控)需将摄像头的图像数据(如 YUV 格式)快速写入内存,DMA 可避免 CPU 因“逐帧处理”导致的延迟。
4. 嵌入式与实时系统(MCU、工业控制)
- 场景:传感器数据采集(如温度、加速度)、电机控制、工业总线(如 CAN、EtherCAT)通信;
- 作用:嵌入式设备(如 MCU)的 CPU 性能有限,若需同时处理传感器数据、控制逻辑、通信任务,易出现实时性不足;通过 DMA 可自动采集传感器数据(如 ADC 采样值)并写入内存,CPU 专注于控制算法,确保任务在毫秒/微秒级完成。
三、DMA 的使用方式(软件视角)
DMA 的使用需结合硬件平台(如 x86、ARM、RISC-V)和软件层次(驱动、操作系统、应用),核心是通过配置 DMA 控制器寄存器或调用操作系统 API 实现数据传输。以下分“驱动开发”和“应用开发”两个层面说明:
1. 底层驱动开发(直接操作硬件)
在无操作系统(裸机)或需要深度定制驱动的场景(如嵌入式 Linux 内核驱动),需直接操作 DMA 控制器的寄存器,步骤如下:
步骤 1:使能 DMA 时钟与外设时钟
硬件层面,DMA 控制器和目标外设(如 UART、ADC)需先上电使能时钟(如 ARM 芯片的 RCC 寄存器、x86 的 PCIe 配置空间),否则无法操作。
// 示例(ARM Cortex-M 裸机):使能 DMA1 和 UART2 时钟 RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN; // 使能 DMA1 时钟 RCC->APB1ENR |= RCC_APB1ENR_USART2EN;// 使能 UART2 时钟
步骤 2:配置 DMA 通道参数
根据传输需求,设置 DMA 通道的源地址、目标地址、传输长度、传输方向等,需注意地址对齐(如 32 位传输需地址按 4 字节对齐,避免总线错误)。
// 示例:配置 DMA1 通道4,实现 UART2 接收数据(外设→内存) DMA1_Channel4->CPAR = (uint32_t)&USART2->DR; // 源地址:UART2 数据寄存器 DMA1_Channel4->CMAR = (uint32_t)rx_buffer; // 目标地址:内存接收缓冲区 DMA1_Channel4->CNDTR = 1024; // 传输长度:1024 字节 DMA1_Channel4->CCR |= DMA_CCR_DIR; // 传输方向:外设→内存(DIR=1) DMA1_Channel4->CCR |= DMA_CCR_MINC; // 内存地址自增(MINC=1) DMA1_Channel4->CCR |= DMA_CCR_PSIZE_0; // 外设数据宽度:16位 DMA1_Channel4->CCR |= DMA_CCR_MSIZE_0; // 内存数据宽度:16位
步骤 3:使能 DMA 与外设中断
配置 DMA 传输完成/出错中断,确保传输结束后能通知 CPU 处理。
// 使能 DMA 传输完成中断和全局中断 DMA1_Channel4->CCR |= DMA_CCR_TCIE; // 传输完成中断使能(TCIE=1) DMA1_Channel4->CCR |= DMA_CCR_HTIE; // 半传输中断使能(可选,用于进度监控) NVIC_EnableIRQ(DMA1_Channel4_IRQn); // 使能 DMA 中断向量 // 使能外设(UART2)的 DMA 请求 USART2->CR3 |= USART_CR3_DMAR; // UART2 接收 DMA 请求使能
步骤 4:启动 DMA 传输并处理中断
启动 DMA 后,等待传输完成中断,在中断服务函数中处理数据(如校验、转发)。
// 启动 DMA 传输
DMA1_Channel4->CCR |= DMA_CCR_EN; // 使能 DMA 通道
// DMA 传输完成中断服务函数
void DMA1_Channel4_IRQHandler(void) {
if (DMA1->ISR & DMA_ISR_TCIF4) { // 检查传输完成标志
DMA1->IFCR |= DMA_IFCR_CTCIF4; // 清除中断标志
process_rx_data(rx_buffer, 1024); // 处理接收的数据
DMA1_Channel4->CNDTR = 1024; // 重置传输长度,准备下一次传输
}
}
2. 操作系统层应用开发(调用 API)
在 Windows、Linux、macOS 等操作系统中,用户无需直接操作硬件寄存器,而是通过操作系统提供的 API 或库函数使用 DMA(本质是操作系统内核驱动已封装好 DMA 逻辑)。以下以 Linux 为例说明:
场景:用户程序通过 DMA 与外设(如自定义 PCIe 设备)传输数据
Linux 中,用户空间无法直接访问 DMA 控制器,需通过“内核驱动 + 系统调用”或“内存映射(mmap)”实现数据传输,核心流程如下:
- 内核驱动初始化 DMA:申请 DMA 通道(dma_request_channel);分配 DMA 可访问的内存(dma_alloc_coherent,确保内存物理地址连续,避免 DMA 地址不连续问题);配置 DMA 传输参数(源/目标地址、长度),启动传输。
- 用户程序与内核驱动通信:通过 ioctl 或 mmap 将内核中的 DMA 缓冲区映射到用户空间,用户程序可直接读写该缓冲区,无需数据拷贝;传输完成后,内核通过信号或中断通知用户程序。
示例代码片段(Linux 内核驱动):
#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>
struct dma_dev {
struct dma_chan *dma_chan; // DMA 通道
dma_addr_t dma_addr; // DMA 物理地址
void *virt_addr; // 虚拟地址(内核空间)
size_t buf_len; // 缓冲区长度
};
// 初始化 DMA
static int dma_dev_probe(struct platform_device *pdev) {
struct dma_dev *dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
dev->buf_len = 4096; // 缓冲区长度 4KB
// 1. 申请 DMA 通道(匹配外设类型,如 "memory-to-memory")
dev->dma_chan = dma_request_channel(DMA_MEM_TO_MEM, NULL, NULL);
if (!dev->dma_chan) return -ENODEV;
// 2. 分配 DMA 可访问的连续内存
dev->virt_addr = dma_alloc_coherent(&pdev->dev, dev->buf_len,
&dev->dma_addr, GFP_KERNEL);
if (!dev->virt_addr) return -ENOMEM;
// 3. 配置 DMA 传输(内存→内存示例,实际可改为外设地址)
struct dma_async_tx_descriptor *desc;
dma_cap_mask_t mask;
dma_cap_zero(mask);
dma_cap_set(DMA_MEMCPY, mask); // 传输类型:内存拷贝
desc = dmaengine_prep_dma_memcpy(dev->dma_chan, dev->dma_addr, // 目标地址
(dma_addr_t)src_buffer, // 源地址(如外设地址)
dev->buf_len, DMA_PREP_INTERRUPT); // 传输长度+中断
if (!desc) return -EINVAL;
// 4. 绑定传输完成回调函数
desc->callback = dma_transfer_complete;
desc->callback_param = dev;
// 5. 提交 DMA 传输任务
dmaengine_submit(desc);
dma_async_issue_pending(dev->dma_chan); // 启动传输
return 0;
}
// DMA 传输完成回调函数
static void dma_transfer_complete(void *param) {
struct dma_dev *dev = param;
dev_info(&dev->dev, "DMA transfer completed\n");
// 通知用户程序(如发送信号、唤醒等待队列)
}
用户程序调用(Linux):
通过 mmap 将内核 DMA 缓冲区映射到用户空间,直接读写数据:
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#define DMA_DEV_PATH "/dev/dma_device"
#define BUF_LEN 4096
int main() {
int fd = open(DMA_DEV_PATH, O_RDWR);
if (fd < 0) { perror("open"); return -1; }
// 将内核 DMA 缓冲区映射到用户空间
void *user_buf = mmap(NULL, BUF_LEN, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (user_buf == MAP_FAILED) { perror("mmap"); return -1; }
// 1. 写入数据到用户缓冲区(内核会通过 DMA 传输到外设)
memcpy(user_buf, "Hello DMA", 10);
// 触发 DMA 传输(通过 ioctl 通知内核)
ioctl(fd, 0x12345678); // 自定义命令,内核收到后启动 DMA
// 2. 等待传输完成(如通过信号或轮询)
sleep(1);
// 3. 读取 DMA 传输到缓冲区的数据(外设→内存)
printf("Received data: %s\n", (char *)user_buf);
munmap(user_buf, BUF_LEN);
close(fd);
return 0;
}
四、DMA 的优缺点与注意事项
1. 优点
- 提升系统性能:解放 CPU 资源,让 CPU 专注于计算任务,而非数据搬运;
- 降低延迟:避免 CPU 上下文切换和指令开销,尤其适合实时性要求高的场景(如工业控制、音频视频);
- 支持高吞吐量:硬件直接传输数据,可充分利用总线带宽(如 PCIe 4.0 带宽达 8GB/s)。
2. 缺点与挑战
- 硬件复杂度增加:需额外的 DMA 控制器硬件,增加芯片成本和设计难度(嵌入式设备需权衡成本与性能);
- 地址对齐要求:多数 DMA 控制器要求传输地址按数据宽度对齐(如 32 位传输需地址能被 4 整除),否则会触发总线错误;
- 内存一致性问题:部分 CPU 存在缓存(Cache),若 DMA 直接修改内存数据,CPU 缓存中的数据可能与内存不一致(需通过“缓存刷新/无效化”操作解决,如 ARM 的
dmb指令、x86 的sfence指令); - 优先级冲突:多个外设同时请求 DMA 时,低优先级外设可能被“饿死”,需合理设计优先级策略(如实时系统采用“抢占式优先级”)。
3. 使用注意事项
- 地址正确性:确保 DMA 源/目标地址为物理地址(而非虚拟地址),操作系统中需通过
dma_map_single等函数将虚拟地址转换为物理地址; - 缓存处理:若传输涉及 CPU 缓存,需在 DMA 传输前刷新缓存(确保内存数据最新),传输后无效化缓存(确保 CPU 读取最新数据);
- 错误处理:配置 DMA 错误中断(如传输超时、总线错误),避免因硬件故障导致数据丢失或系统崩溃;
- 带宽分配:根据外设需求合理分配 DMA 总线带宽,避免高带宽外设(如 NVMe SSD)占用过多资源,影响其他设备(如网卡)。
五、总结
DMA 是解决“CPU 数据搬运瓶颈”的核心技术,通过硬件独立完成外设与内存的直接传输,在存储、网络、多媒体等场景中不可或缺。其使用需结合硬件平台(配置 DMA 控制器)和软件层次(驱动/API 调用),同时注意地址对齐、缓存一致性、优先级管理等细节。理解 DMA 的原理与用法,是掌握计算机硬件体系、嵌入式开发、高性能系统优化的关键基础。
查看11道真题和解析