在传统的数据传输模式中,无论是从硬盘读取数据到内存,还是将内存中的数据发送到外设,都需要 CPU 全程参与。当数据量较小时,CPU 还能应付自如,但一旦面对高清视频这种每秒需要处理大量数据的任务,或者大文件拷贝时的数据洪流,CPU 就会不堪重负。因为在数据传输过程中,CPU 需要频繁地在数据搬运和自身核心任务之间切换,大量的时间和算力都消耗在了数据传输上,导致其无法高效地处理其他关键事务,系统性能自然就会受到严重影响。
而直接内存访问(Direct Memory Access,简称 DMA)技术的出现,就像是为 CPU 这位忙碌的快递员配备了一辆自动化的配送车。它允许外部设备(如硬盘、网卡、声卡等)直接与系统内存进行数据传输,无需 CPU 频繁干预。在 DMA 技术的协助下,CPU 只需在数据传输开始前,向 DMA 控制器发送相关指令,告知它数据的源地址、目标地址以及传输长度等信息,之后 DMA 控制器就会独立完成数据的搬运工作。在这个过程中,CPU 可以腾出手来,专注于执行更复杂的算法计算、系统资源的调度等核心任务,大大提高了系统的整体运行效率。
接下来,将带你深入探索 Linux 系统中的 DMA 技术。
一、初识 Linux DMA
1.1 什么是 DMA?
DMA,即直接内存访问(Direct Memory Access),从硬件层面来看,它是一种允许外部设备(如硬盘、网卡、声卡等)绕过 CPU,直接与系统内存进行数据传输的硬件机制。在传统的程序直接输入输出(PIO,Programmed Input/Output)模式中,CPU 需要全程参与数据传输过程。例如,当我们从硬盘读取数据到内存时,CPU 需要逐字节地将数据从硬盘读取到自己的寄存器中,然后再将寄存器中的数据写入内存。这就好比一个人需要亲自将货物一箱一箱地从仓库搬到车上,效率低下且耗费精力。
而 DMA 技术的出现,改变了这种局面。它引入了 DMA 控制器(DMAC,DMA Controller)这一关键角色。当外设需要进行数据传输时,它会向 DMA 控制器发送请求。DMA 控制器在接收到请求后,会根据预先设置好的参数,如数据的源地址、目标地址和传输长度等,直接控制数据在内存和外设之间传输。在这个过程中,CPU 只需在传输开始前对 DMA 控制器进行初始化设置,然后就可以去处理其他任务,无需像 PIO 模式那样全程参与数据搬运。这就如同我们雇佣了一辆自动搬运车,只需告诉它货物的存放位置和目的地,它就能自动完成搬运工作,而我们则可以去做更重要的事情。
1.2 Linux 下 DMA 的核心角色
在 Linux 系统中,理解 DMA 涉及的三种核心地址概念是深入掌握其工作原理的关键,这三种地址分别是虚拟地址、物理地址和总线地址。
虚拟地址是 CPU 所看到的地址,它是一种逻辑地址。在现代操作系统中,为了实现内存的有效管理和保护,CPU 使用虚拟地址来访问内存。通过内存管理单元(MMU,Memory Management Unit),虚拟地址被映射为物理地址,从而实现对实际内存的访问。例如,在一个进程中,程序所使用的变量地址就是虚拟地址,它并不直接对应内存中的物理位置。
物理地址则是内存芯片上的实际地址,它是内存的真实位置标识。内存通过地址总线来接收物理地址信号,从而实现数据的读写操作。
总线地址是 DMA 控制器或外设用来访问主存的地址。这里需要注意的是,DMA 控制器不支持 MMU 的地址转换功能,它使用的是总线地址来直接访问内存。在不同的硬件架构下,总线地址与物理地址的关系有所不同。在没有输入输出内存管理单元(IOMMU,Input/Output Memory Management Unit)的情况下,总线地址和物理地址通常是相同的,或者彼此之间存在一个固定的偏移值。而在带有 IOMMU 的系统中,IOMMU 负责将总线地址映射为主存的物理地址,类似于 MMU 对虚拟地址和物理地址的映射关系。
Linux DMA 的核心任务之一就是在这三种地址之间搭建起沟通的桥梁,确保设备能够正确地访问内存中的数据。例如,当一个网络设备通过 DMA 接收数据时,它使用总线地址将数据直接写入内存。而 CPU 在处理这些数据时,使用的是虚拟地址。Linux 内核中的 DMA 子系统负责处理这些地址之间的转换和映射,使得整个数据传输过程能够无缝进行。

1.3 DMA 的三种传输模式
DMA 技术为了在数据传输效率和 CPU 的正常运行之间找到平衡,发展出了三种经典的传输模式,分别是 Block DMA、Burst mode 和 Transparent DMA。
Block DMA 模式,也称为块传输模式。在这种模式下,DMA 控制器会一次性传送所有要求的数据长度。在数据传输期间,DMA 控制器会完全占据数据总线,CPU 此时无法使用数据总线访问主存数据。这就好比一辆大型货车一次性装满货物后直接运输到目的地,运输过程中独占道路。这种模式适用于对数据传输效率要求极高,且 CPU 在这段时间内可以暂时等待的场景,例如大文件的快速拷贝。但是,由于数据总线被长时间占用,CPU 在传输期间无法进行内存访问等操作,可能会影响系统的整体响应速度。
Burst mode 模式,即突发模式。它一次只传送总数据的一小段,而不是全部。在传输间歇期间,CPU 可以访问数据总线。这就像是一辆小型货车分多次运输货物,每次运输之间留出时间让其他车辆通行。这种模式在一定程度上减少了数据总线被占用的时间,使得 CPU 能够在传输间隙执行其他任务,提高了系统的并发性能。例如,在一些实时性要求较高的多媒体应用中,音频和视频数据的传输可以采用 Burst mode 模式,既能保证数据的及时传输,又能让 CPU 有时间处理其他实时任务。
Transparent DMA 模式,也就是透明模式。它的工作方式更加巧妙,DMA 控制器会在 CPU 不访问数据总线期间偷偷地传输数据,这样就避免了和 CPU 争抢数据总线。这就好比一个隐形的搬运工,在主人不使用道路的时候悄悄地搬运货物。这种模式对 CPU 的影响最小,能够最大程度地保证 CPU 的正常运行。在一些对 CPU 利用率要求极高的系统中,如高性能服务器,Transparent DMA 模式可以有效地提高系统的整体性能。
二、Linux DMA 的核心工作机制
2.1 DMA 映射机制与缓存一致性解决方案
2.1.1 DMA 映射的本质
在 Linux 内核中,DMA 映射机制是实现设备与内存高效数据传输的关键基石,它为设备与内存之间搭建起了一座直接通信的桥梁。从本质上讲,DMA 映射机制是 Linux 内核提供的一套工具集,用于管理设备与内存之间的地址映射关系。在计算机系统中,设备和 CPU 对于内存地址的认知存在差异,设备通常使用物理地址来访问内存,而 CPU 则使用虚拟地址。DMA 映射机制的核心作用就是解决这一地址差异问题,使得设备能够正确地访问内存中的数据。
具体来说,当设备需要进行 DMA 传输时,首先要通过 DMA 映射机制分配一块符合 DMA 要求的物理连续内存缓冲区。这是因为 DMA 控制器在进行数据传输时,通常要求数据的物理地址是连续的,这样可以提高数据传输的效率。在分配好物理内存缓冲区后,DMA 映射机制会将该缓冲区的物理地址映射为设备可访问的总线地址,同时也会将其映射为 CPU 可访问的虚拟地址。通过这种映射关系,设备可以直接使用总线地址对内存缓冲区进行数据读写操作,而 CPU 则可以使用虚拟地址对该缓冲区进行数据处理。
以网络设备接收数据包为例,当网络设备接收到数据包后,它需要将数据包存储到内存中。此时,DMA 映射机制会为网络设备分配一块物理连续的内存缓冲区,并将该缓冲区的物理地址映射为网络设备可访问的总线地址。网络设备通过总线地址将数据包直接写入内存缓冲区,而 CPU 则可以通过虚拟地址对该缓冲区中的数据包进行处理,如解析数据包的内容、进行路由决策等。
2.1.2 两种映射模式
在 Linux 系统中,DMA 映射主要分为一致性映射(coherent DMA mapping)和流式映射(streaming DMA mapping)两种模式。
一致性映射,通常使用dma_alloc_coherent函数来实现。这种映射模式的特点是,在驱动的整个生命周期内,所分配的内存缓冲区始终保持有效,并且能够自动维护缓存一致性。这意味着,无论是 CPU 还是设备对该缓冲区进行数据访问,都能保证数据的一致性,不会出现数据不一致的问题。一致性映射适用于那些需要长期共享数据的场景,比如设备的配置信息、控制数据等,这些数据在设备的运行过程中需要被频繁地访问,并且要求数据始终保持一致。例如,在一个嵌入式系统中,设备的寄存器配置信息需要被 CPU 和设备频繁地访问和修改,使用一致性映射可以确保 CPU 和设备在访问这些信息时,始终能够获取到最新的数据,避免了数据不一致导致的设备故障。
流式映射,则是通过dma_map_single函数来创建。它主要适用于短期的、单次的数据传输场景。在这种映射模式下,DMA 映射会在每次数据传输前创建,传输完成后立即解除映射。由于流式映射的生命周期较短,因此在缓存管理方面,它需要手动进行缓存的同步操作,以确保数据的一致性。例如,在进行网络数据包的传输时,每个数据包的传输都是一个独立的过程,数据包之间没有长期的共享关系,此时使用流式映射可以提高内存的利用率,减少不必要的内存占用。同时,在每次数据包传输前,需要手动调用dma_sync_single_for_device函数将 CPU 缓存中的数据同步到内存中,以确保设备能够读取到最新的数据;在传输完成后,需要调用dma_sync_single_for_cpu函数将内存中的数据同步到 CPU 缓存中,以便 CPU 能够正确地处理接收到的数据。
下面是一致性映射和流式映射的代码调用范式:
// 一致性映射示例
#include <linux/dma-mapping.h>
// 分配一致性内存
void *dma_vaddr;
dma_addr_t dma_handle;
dma_vaddr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!dma_vaddr) {
// 错误处理
}
// 使用dma_vaddr和dma_handle进行DMA操作
//...
// 释放一致性内存
dma_free_coherent(dev, size, dma_vaddr, dma_handle);
// 流式映射示例
#include <linux/dma-mapping.h>
// 假设已经分配了内存buf
dma_addr_t dma_handle;
dma_handle = dma_map_single(dev, buf, size, direction);
if (dma_mapping_error(dev, dma_handle)) {
// 错误处理
}
// 使用dma_handle进行DMA传输
//...
// 解除映射
dma_unmap_single(dev, dma_handle, size, direction);
2.1.3 缓存一致性
为了提高 CPU 访问内存的速度,通常会在 CPU 和内存之间引入高速缓存(Cache)。CPU 在访问内存时,首先会检查缓存中是否存在所需的数据,如果存在,则直接从缓存中读取数据,这样可以大大提高数据的访问速度。然而,当设备通过 DMA 直接访问内存时,就可能会出现缓存一致性问题。
这是因为设备在进行 DMA 传输时,直接对物理内存进行读写操作,而不会经过 CPU 的缓存。如果此时 CPU 缓存中存在与 DMA 传输相关的数据,并且这些数据在 DMA 传输前后发生了变化,就会导致 CPU 缓存中的数据与物理内存中的数据不一致。例如,当 CPU 向内存中写入数据时,数据首先会被写入到缓存中,然后在某个时刻再被写回到物理内存中。如果在数据还未被写回到物理内存时,设备通过 DMA 读取了该内存地址的数据,那么设备读取到的数据就是旧的数据,而不是 CPU 刚刚写入的新数据;反之,如果设备通过 DMA 向内存中写入了数据,而 CPU 缓存中仍然保存着旧的数据,那么 CPU 在读取该内存地址的数据时,就会读取到错误的数据。
为了解决缓存一致性问题,Linux 系统提供了两种主要的解决方案:一种是禁用 DMA 内存的缓存,使得设备在进行 DMA 传输时,直接访问物理内存,而不经过缓存,这样就避免了缓存不一致的问题。但是,这种方法会降低 CPU 访问内存的速度,因为 CPU 无法利用缓存来加速数据访问。另一种方法是在 DMA 传输前后,主动对缓存进行刷新(flush)或失效(invalidate)操作。在 DMA 传输前,通过刷新缓存,将 CPU 缓存中的数据写回到物理内存中,确保设备读取到的是最新的数据;在 DMA 传输后,通过使缓存失效,让 CPU 重新从物理内存中读取数据,避免读取到旧的数据。
而对于一致性映射,它通过硬件机制或内核的特殊处理来保证缓存一致性。在一些硬件架构中,支持缓存一致性协议,硬件会自动维护缓存和内存之间的数据一致性,使得一致性映射能够在不进行额外软件操作的情况下,保证数据的一致性。在内核层面,一致性映射在分配内存时,会对内存的属性进行特殊设置,确保内存的访问能够正确处理缓存一致性问题。
2.2 Linux DMA 引擎与控制器驱动架构
2.2.1 DMA 引擎框架
Linux DMA 引擎框架是 Linux 内核中实现 DMA 功能的核心框架,它通过引入一个统一的抽象层,将各种不同硬件架构的 DMA 控制器进行了抽象和封装,为驱动开发者提供了一套标准化的 API 接口。在这个框架中,所有的 DMA 控制器都被抽象为struct dma_device结构体,这个结构体包含了 DMA 控制器的各种属性和操作函数指针,如 DMA 控制器的能力描述、数据传输函数、中断处理函数等。通过这种抽象方式,驱动开发者在编写设备驱动时,无需深入了解底层硬件的具体细节,只需要使用 DMA 引擎框架提供的 API 接口,就可以轻松地实现 DMA 数据传输功能。
例如,当驱动开发者需要为一个新的设备编写 DMA 驱动时,他们只需要根据设备所使用的 DMA 控制器,实现struct dma_device结构体中定义的操作函数,然后将这个结构体注册到 DMA 引擎框架中。之后,在设备驱动中,就可以通过调用 DMA 引擎框架提供的 API,如dma_request_channel获取 DMA 通道、dma_async_issue_pending提交 DMA 传输请求等,来实现数据的 DMA 传输。这样,即使硬件平台发生了变化,只要新的 DMA 控制器能够适配 DMA 引擎框架,驱动开发者只需要修改少量的代码,就可以使驱动在新的平台上正常工作,大大提高了驱动的可移植性和开发效率。
除了struct dma_device结构体,DMA 引擎框架还包含其他一些核心组件,如 DMA 通道(DMA channel)、DMA 事务(DMA transaction)等。DMA 通道是 DMA 传输的基本单元,每个 DMA 控制器可以包含多个 DMA 通道,每个通道可以独立地进行数据传输。DMA 事务则是对一次 DMA 传输操作的抽象,它包含了数据传输的源地址、目标地址、传输长度、传输方向等信息。这些组件相互协作,共同完成了 DMA 数据传输的各项任务。
2.2.2 DMA 传输的完整流程
在 Linux 系统中,DMA 传输可以大致分为三个主要步骤:初始化阶段、传输阶段和中断通知阶段。
在初始化阶段。当设备需要进行 DMA 数据传输时,CPU 首先要对 DMA 控制器进行初始化配置。这包括设置 DMA 控制器的各种寄存器,如源地址寄存器,用于指定数据传输的源地址,它可以是内存中的某个地址,也可以是设备的某个寄存器地址;目标地址寄存器,用于指定数据传输的目标地址,同样可以是内存地址或设备寄存器地址;传输长度寄存器,用于设置本次 DMA 传输的数据长度;以及传输方向寄存器,用于确定数据是从设备传输到内存(DMA_FROM_DEVICE),还是从内存传输到设备(DMA_TO_DEVICE)。此外,CPU 还需要设置 DMA 控制器的其他一些控制寄存器,如中断使能寄存器,用于开启或关闭 DMA 传输完成后的中断通知功能;传输模式寄存器,用于选择 DMA 传输的模式,如块传输模式、突发模式等。通过这些寄存器的设置,CPU 为 DMA 控制器提供了数据传输所需的基本信息和控制参数。
当 DMA 控制器完成初始化配置后,就进入了传输阶段。在这个阶段,DMA 控制器开始接管系统总线的控制权,直接在设备和内存之间进行数据传输。DMA 控制器根据初始化阶段设置的源地址、目标地址和传输长度等参数,从源地址读取数据,并将数据写入到目标地址。在数据传输过程中,DMA 控制器会自动更新源地址和目标地址,以及传输长度寄存器的值,以确保数据能够按照设定的要求准确无误地传输。由于 DMA 控制器独立于 CPU 进行数据传输,因此在这个过程中,CPU 可以并行执行其他任务,大大提高了系统的整体效率。
当 DMA 传输完成后,就进入了中断通知阶段。此时,DMA 控制器会向 CPU 发送一个中断信号,通知 CPU 数据传输已经完成。CPU 在接收到中断信号后,会暂停当前正在执行的任务,转而执行 DMA 中断处理程序。在中断处理程序中,CPU 会对 DMA 传输的结果进行检查,如检查传输是否成功、是否发生错误等。如果传输成功,CPU 会根据具体的应用场景,进行后续的处理工作,如处理接收到的数据、更新设备的状态信息等;如果传输发生错误,CPU 会采取相应的错误处理措施,如记录错误日志、重新启动 DMA 传输等。通过中断通知机制,CPU 能够及时了解 DMA 传输的完成情况,并做出相应的处理,确保系统的正常运行。

2.3 DMA-BUF 零拷贝共享机制
2.3.1 传统方案的瓶颈
数据往往需要在多个设备之间进行流转和处理,例如在音视频处理场景中,数据需要从摄像头采集,经过 GPU 处理,最后在显示屏上显示。在传统的数据传输方案中,数据在设备之间的流转通常需要经过多次内核态与用户态之间的拷贝,这不仅会导致内存的冗余,还会消耗大量的 CPU 资源,成为系统性能提升的瓶颈。
以摄像头采集图像数据,经过 GPU 处理后在显示屏上显示的过程为例。在传统方案中,首先摄像头通过 DMA 将采集到的图像数据写入摄像头驱动分配的 DMA 缓冲区,这是一次 DMA 传输操作。接着,摄像头驱动需要将 DMA 缓冲区中的数据拷贝到内核态临时缓冲区,这是第一次数据拷贝。然后,内核态临时缓冲区的数据又需要被拷贝到用户态缓冲区,以便应用程序对数据进行进一步的处理,这是第二次数据拷贝。当 GPU 需要处理这些数据时,GPU 驱动又要将用户态缓冲区的数据拷贝到 GPU 驱动分配的 DMA 缓冲区,这是第三次数据拷贝。最后,GPU 处理完数据后,再将数据拷贝到显示屏的 DMA 缓冲区,以便在显示屏上显示,这是第四次数据拷贝。
在这个过程中,数据经过了多次拷贝,每一次拷贝都需要消耗 CPU 资源和内存带宽。随着数据量的增大和处理需求的增加,这种多次拷贝的方式会导致 CPU 负载过高,内存带宽被大量占用,从而使得系统的整体性能下降。例如,在处理高清视频时,由于视频数据量巨大,多次拷贝操作会导致 CPU 忙于数据搬运,无法及时处理其他任务,从而出现视频卡顿、延迟等问题。此外,多次拷贝还会导致内存中存在多个相同数据的副本,造成内存资源的浪费。
2.3.2 DMA-BUF 的核心原理:一块内存多设备共享
为了解决传统方案中多次拷贝带来的性能损耗问题,Linux 内核引入了 DMA-BUF(Direct Memory Access - Buffer)机制,它的核心原理是实现一块内存多设备共享,通过 "零拷贝" 技术大大提高了数据在设备之间的流转效率。
DMA-BUF 本质上是内核统一管理的跨设备共享缓冲区,它通过一种 "生产者 - 消费者" 模型来实现多设备之间的数据共享。在这个模型中,由一个设备(通常称为生产者)负责分配 DMA-BUF 共享缓冲区。例如,在上述的音视频处理场景中,摄像头驱动可以作为生产者,分配一块 DMA-BUF 共享缓冲区。摄像头通过 DMA 将采集到的图像数据直接写入这个共享缓冲区,无需进行额外的数据拷贝操作。
其他需要使用这些数据的设备(后续称为消费者),如 GPU 驱动和显示屏驱动,可以通过文件描述符(fd)来映射并访问这个共享缓冲区。具体来说,生产者在分配完 DMA-BUF 共享缓冲区后,内核会为这个缓冲区创建一个对应的文件描述符。生产者将这个文件描述符传递给消费者,消费者通过调用dma_buf_get函数,根据文件描述符获取对应的struct dma_buf指针,从而获得对共享缓冲区的访问权。然后,消费者可以调用dma_buf_attach函数,将自己的设备与共享缓冲区进行关联,并通过dma_buf_map_attachment函数将共享缓冲区映射到自己的设备地址空间。这样,消费者就可以直接在自己的设备地址空间中访问共享缓冲区中的数据,实现了数据的 "零拷贝" 流转。
struct dma_buf是 DMA-BUF 机制中的核心数据结构,它包含了缓冲区的大小、关联的文件对象、所有消费者设备的附加信息链表、缓冲区操作函数集等重要信息。通过这个数据结构,内核可以有效地管理 DMA-BUF 共享缓冲区的生命周期,包括缓冲区的分配、映射、共享和释放等操作。例如,当所有消费者设备都完成对共享缓冲区的访问后,生产者可以调用dma_buf_release函数释放共享缓冲区,内核会根据struct dma_buf中的信息,回收相关的内存资源,完成整个生命周期的管理。
三、Linux DMA 的典型应用场景
3.1 磁盘 I/O:大文件传输的性能加速器

在 Linux 系统中,DMA(直接内存访问)是提升磁盘 I/O 性能的关键技术,尤其在处理大文件读写时表现突出。传统方式下,CPU 需逐字节搬运数据,效率低下;而借助 DMA,数据可直接在磁盘与内存之间传输,无需 CPU 持续干预。
当应用程序发起大文件读取请求时,内核的块设备驱动会调用 submit_bio() 函数,将 I/O 请求提交给磁盘控制器,并触发 DMA 传输。DMA 控制器随即接管数据搬运任务------从硬盘读取数据并写入指定内存区域,全程由硬件完成。这不仅显著提升了传输速度,还释放了 CPU 资源,使其能够专注于更高价值的任务,如调度、计算或响应其他系统事件。
在大数据分析、视频编辑等场景中,这种"零拷贝"式的数据通路尤为重要。若无 DMA,CPU 将被大量 I/O 操作拖累,导致系统响应迟滞、吞吐下降。DMA 的引入,使得海量数据能高效流入内存,为上层应用提供坚实支撑。
3.2 网络通信:网卡数据包的高速搬运工
现代网卡普遍支持 DMA 功能,可在接收到网络数据包后,直接将其写入内核预先分配的接收缓冲区,无需 CPU 参与每一步数据搬运。
这一机制极大降低了网络 I/O 对 CPU 的开销。在高并发服务器(如 Web 服务、数据库节点)中,每秒可能处理数万甚至数十万个数据包。若每个包都需 CPU 介入复制,系统将迅速过载,引发延迟、丢包甚至服务中断。而通过 DMA,网卡自主完成数据入内存的操作,CPU 仅需在适当时候轮询或中断处理这些已就绪的数据包,从而显著提升网络吞吐量与系统整体响应能力。
DMA 与 NAPI(New API)、多队列网卡等技术协同工作,构成了现代高性能网络栈的基石。
3.3 音视频处理:流畅播放的幕后功臣
音视频应用对数据传输的实时性与连续性要求极高。无论是高清视频渲染还是高质量音频播放,都需要稳定、低延迟的数据流从内存传送到 GPU 或声卡。DMA 正是保障这一流程顺畅的核心机制。
为满足硬件对物理连续内存的需求,驱动程序通常使用 dma_alloc_coherent() 等内核 API 分配一致性 DMA 缓冲区。这类内存既对 CPU 可见,也能被外设直接访问,避免了缓存一致性问题,确保数据传输可靠高效。
以视频播放为例:原始视频帧从存储设备经 DMA 加载至内存缓冲区,GPU 再通过 DMA 直接读取该缓冲区进行解码与渲染,最终输出至显示设备。音频路径亦类似。整个链路几乎无需 CPU 干预,有效避免了卡顿、花屏或音频断续等问题,为用户提供流畅、沉浸式的多媒体体验。
3.4 内存间传输:无需 CPU 参与的"数据搬家"
除了设备与内存之间的交互,Linux 系统在某些高性能或嵌入式场景中,还需实现内存到内存的高速数据迁移。此时,专用 DMA 控制器(DMAC)便派上用场,实现真正意义上的"CPU 无关"数据搬运。
例如,在图像预处理、信号处理或实时数据重组任务中,大量数据需在不同内存区域间移动。若由 CPU 执行 memcpy 类操作,不仅耗时,还会挤占宝贵的计算资源。而通过配置 DMAC 的寄存器(如源地址、目标地址、传输长度等),系统可启动一次自主的内存间 DMA 传输。
一旦启动,DMAC 独立完成整个拷贝过程,CPU 可继续执行其他任务。这种方式不仅提升了带宽利用率,还增强了系统的实时性与确定性。在嵌入式视觉系统、工业控制或 FPGA 协处理器协同场景中,此类 DMA 传输已成为优化性能的关键手段。
四、实战:Linux DMA 驱动开发与调试
4.1 驱动开发五步走:从缓冲区分配到传输完成
4.1.1 分配 DMA 缓冲区:一致性映射的代码实现
在 Linux DMA 驱动开发中,分配合适的 DMA 缓冲区是实现高效数据传输的首要任务。以dma_alloc_coherent函数为例,它在驱动中起着关键作用,用于分配物理连续的 DMA 缓冲区,确保 CPU 和外设看到的数据一致,避免缓存不一致问题。
该函数的原型如下:
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag);
其中,dev是指向设备结构体的指针,通过它可以获取设备相关的 DMA 配置,不同的设备对应不同的dev指针,它是连接驱动与硬件设备的关键纽带。size表示要分配的内存大小,单位为字节,根据实际的数据传输需求来确定,例如在网络通信中,可能需要根据数据包的大小来设置size。dma_handle是一个输出参数,用于存储分配到的内存的物理地址,这个物理地址将被设备用于 DMA 传输,它就像是设备访问内存的 "钥匙"。flag是分配内存时使用的标志,常见的有GFP_KERNEL和GFP_ATOMIC。GFP_KERNEL允许睡眠,适用于非原子操作的场景,当驱动在执行一些可以被中断的操作时,可以使用这个标志;而GFP_ATOMIC则用于原子分配,不可睡眠,当驱动处于中断上下文或者其他不允许睡眠的场景时,就需要使用GFP_ATOMIC标志。
在实际代码实现中,我们可以这样使用dma_alloc_coherent函数:
#include <linux/dma-mapping.h>
#include <linux/device.h>
struct device *dev = get_device(); // 获取设备指针,这里假设已经有获取设备指针的函数
size_t buffer_size = 4096; // 分配4KB的缓冲区,可根据实际需求调整
dma_addr_t dma_addr;
void *buffer = dma_alloc_coherent(dev, buffer_size, &dma_addr, GFP_KERNEL);
if (!buffer) {
// 缓冲区分配失败的错误处理
printk(KERN_ERR "Failed to allocate DMA buffer\n");
return -ENOMEM;
}
在这段代码中,首先获取设备指针dev,然后定义要分配的缓冲区大小buffer_size为 4KB。接着调用dma_alloc_coherent函数进行缓冲区分配,将返回的缓冲区指针存储在buffer中,将分配到的物理地址存储在dma_addr中。如果分配失败,buffer将为NULL,此时通过printk函数打印错误信息,并返回-ENOMEM错误码,表示内存分配失败。
4.1.2 请求 DMA 通道
在完成 DMA 缓冲区的分配后,接下来需要请求 DMA 通道,这是 DMA 传输的前提条件。dma_request_channel函数是实现这一功能的核心函数。
dma_request_channel函数的原型为:
struct dma_chan *dma_request_channel(dma_cap_mask_t mask, dma_filter_fn filter_fn, void *filter_param);
其中,mask是一个 DMA 能力掩码,用于指定所需的 DMA 通道能力,它可以筛选出满足特定能力要求的 DMA 通道。例如,如果我们需要一个支持特定传输模式的 DMA 通道,就可以通过设置mask来筛选。filter_fn是一个可选的过滤器函数,对于需要获取特定 DMA 通道的场景非常重要。它会对每个空闲的通道进行调用,当返回true时,表示找到了期望的通道。filter_param是传递给过滤器函数的参数。
我们通常根据设备树或硬件 ID 指定通道。以设备树为例,假设设备树中已经定义了 DMA 相关属性,我们可以这样请求 DMA 通道:
#include <linux/dmaengine.h>
dma_cap_mask_t mask;
dma_cap_zero(mask);
dma_cap_set(DMA_SLAVE, mask); // 设置掩码,这里假设需要一个支持从设备传输的通道
struct dma_chan *chan = dma_request_channel(mask, NULL, NULL);
if (!chan) {
// 通道申请失败的应对策略
printk(KERN_ERR "Failed to request DMA channel\n");
return -ENODEV;
}
在这段代码中,首先初始化一个 DMA 能力掩码mask,并设置为支持从设备传输(DMA_SLAVE)。然后调用dma_request_channel函数请求 DMA 通道,由于这里不需要特定的过滤器函数,所以filter_fn和filter_param都设置为NULL。如果通道申请失败,chan将为NULL,此时打印错误信息并返回-ENODEV错误码,表示设备不存在或无法找到合适的通道。
4.1.3 配置传输参数
当成功请求到 DMA 通道后,就需要配置传输参数,其中dmaengine_prep_sg函数是准备传输描述符的关键。该函数支持分散 - 聚集(Scatter-Gather)模式,这是一种强大的传输模式,允许将物理上不连续的内存块作为一个整体进行传输。
dmaengine_prep_sg函数的原型如下:
struct dma_async_tx_descriptor *dmaengine_prep_sg(struct dma_chan *chan, struct scatterlist *sgl, unsigned int sg_len, enum dma_data_direction direction, unsigned long flags);
chan是之前请求到的 DMA 通道。sgl是一个分散列表,用于存储多个内存块的信息,每个内存块可以是物理上不连续的。sg_len表示分散列表中元素的数量。direction指定数据传输的方向,例如DMA_TO_DEVICE表示从内存传输到设备,DMA_FROM_DEVICE表示从设备传输到内存。flags是一些传输标志,用于设置传输的特性,如是否使用中断通知等。
与单缓冲区传输相比,分散 - 聚集模式具有明显的优势。在单缓冲区传输中,要求数据的物理地址必须是连续的,这在实际应用中可能会受到内存碎片化等因素的限制。例如,当内存中没有足够大的连续空闲内存块时,单缓冲区传输就无法进行。而分散 - 聚集模式则可以突破这一限制,它可以将多个不连续的内存块组合起来进行传输。在处理大文件传输时,文件数据可能分散存储在磁盘的不同位置,通过分散 - 聚集模式,可以将这些分散的数据块一次性传输到内存中,而无需先将它们复制到一个连续的内存区域,大大提高了数据传输的效率。特别是在大吞吐量场景下,如网络通信中的大量数据包传输、视频处理中的大数据块传输等,分散 - 聚集模式能够充分利用内存资源,减少内存拷贝的次数,从而显著提升系统性能。
4.1.4 设置回调函数
为了及时处理 DMA 传输完成事件或错误事件,我们需要设置回调函数,dmaengine_set_callback函数就是用于实现这一功能的。
回调函数的编写范式通常如下:
static void my_dma_callback(void *param)
{
struct dma_async_tx_descriptor *desc = param;
enum dma_status status = dmaengine_tx_status(desc, NULL);
if (status == DMA_COMPLETE) {
printk(KERN_INFO "DMA transfer completed successfully\n");
} else {
printk(KERN_ERR "DMA transfer failed with status: %d\n", status);
}
}
在这个回调函数中,首先通过传入的参数param获取 DMA 传输描述符desc,然后使用dmaengine_tx_status函数获取传输状态status。如果传输状态为DMA_COMPLETE,表示 DMA 传输成功,通过printk函数打印成功信息;否则,表示传输失败,打印错误信息并显示具体的错误状态码。
在驱动中设置回调函数的代码如下:
struct dma_async_tx_descriptor *desc = dmaengine_prep_sg(chan, sgl, sg_len, direction, flags);
if (!desc) {
// 传输描述符准备失败的处理
printk(KERN_ERR "Failed to prepare DMA descriptor\n");
return -EINVAL;
}
dmaengine_set_callback(desc, my_dma_callback, desc);
首先调用dmaengine_prep_sg函数准备传输描述符,如果准备失败,打印错误信息并返回错误码。然后调用dmaengine_set_callback函数设置回调函数,将回调函数my_dma_callback和传输描述符desc作为参数传入,这样当 DMA 传输完成时,就会自动调用my_dma_callback函数来处理传输结果。
4.1.5 提交与启动传输
完成前面的步骤后,就可以提交和启动 DMA 传输了。这涉及到两个关键函数:dmaengine_submit和dma_async_issue_pending。
dmaengine_submit函数用于提交传输请求,它将传输描述符加入到 DMA 引擎的等待队列中,但不会立即启动 DMA 操作。其原型为:
dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc);
desc是之前准备好的传输描述符。函数返回一个dma_cookie_t类型的 cookie,这个 cookie 可以用于检查 DMA 引擎活动的状态过程。
dma_async_issue_pending函数则用于启动 DMA 引擎,它会激活等待队列中的传输描述符,开始真正的数据传输。其原型为:
void dma_async_issue_pending(struct dma_chan *chan);
chan是 DMA 通道。
在实际的执行流程中,我们首先调用dmaengine_submit提交传输请求:
dma_cookie_t cookie = dmaengine_submit(desc);
if (dma_submit_error(cookie)) {
// 提交失败的处理
printk(KERN_ERR "Failed to submit DMA transfer\n");
return -EIO;
}
这里调用dmaengine_submit函数提交传输请求,并检查返回的 cookie 是否表示提交成功。如果提交失败,打印错误信息并返回-EIO错误码,表示 I/O 错误。
然后调用dma_async_issue_pending启动传输:
dma_async_issue_pending(chan);
这一步将启动 DMA 引擎,开始数据传输。
结合用户空间write/read系统调用触发 DMA 传输的案例,当用户空间调用write系统调用向设备写入数据时,内核驱动会接收到这个请求。驱动首先会将用户数据复制到之前分配的 DMA 缓冲区中,然后按照上述步骤配置 DMA 传输参数,提交并启动 DMA 传输,将数据从 DMA 缓冲区传输到设备。当用户空间调用read系统调用从设备读取数据时,驱动会先通过 DMA 将数据从设备传输到 DMA 缓冲区,然后再将数据复制回用户空间。通过这样的方式,实现了内核态与用户态的交互链路,完成了高效的数据传输。
4.2 Linux DMA 问题排查
4.2.1 启用内核 DMA 调试功能:编译配置与启动参数
主要通过两种方式来启用 DMA 调试功能。
第一种方式是通过make menuconfig命令进行编译配置。在执行make menuconfig后,会进入内核配置界面。在这个界面中,我们需要找到 "Device Drivers" 选项,进入后再找到 "DMA Engine support" 选项。在 "DMA Engine support" 中,有一个 "DMA-API debugging" 选项,通过选中这个选项(通常按下空格键选择),可以启用 DMA 调试功能。这种方式适用于在编译内核时就明确需要开启调试功能的场景,它会将调试相关的代码编译进内核中,为后续的调试提供支持。
第二种方式是通过添加dma_debug=on内核启动参数来启用 DMA 调试功能。这可以在系统启动时,通过修改启动参数来实现。例如,在 GRUB 引导界面中,找到对应的内核启动项,按下 "e" 键进入编辑模式,在 "kernel" 行的末尾添加dma_debug=on参数,然后按下 "Ctrl + X" 组合键启动系统。这种方式更加灵活,即使内核在编译时没有开启 DMA 调试功能,也可以通过修改启动参数来临时启用,方便在运行时进行问题排查。
启用 DMA 调试功能后,可以通过dmesg命令来验证是否生效。dmesg命令用于查看内核环形缓冲区中的消息,其中包含了 DMA 调试相关的信息。在启用调试功能后,重新启动系统或加载相关驱动,然后执行dmesg | grep DMA命令,就可以查看与 DMA 相关的调试信息。如果看到大量与 DMA 操作相关的详细日志,如 DMA 缓冲区的分配、释放,DMA 通道的请求、使用等信息,说明调试功能已经成功启用,这些日志将为我们排查 DMA 问题提供重要线索。
4.2.2 利用 debugfs 分析 DMA 运行状态
debugfs是 Linux 内核提供的一个用于调试的虚拟文件系统,首先,我们需要挂载debugfs文件系统。在大多数 Linux 系统中,可以通过以下命令挂载debugfs:
mount -t debugfs nodev /sys/kernel/debug
这个命令将debugfs挂载到/sys/kernel/debug目录下。
在/sys/kernel/debug/dma-api目录下,有几个重要的文件,它们分别提供了不同方面的 DMA 运行状态信息。allocations文件记录了当前活跃的 DMA 映射信息,通过读取这个文件,可以获取到当前系统中所有活跃的 DMA 映射的详细情况,包括映射的设备、缓冲区大小、映射的起始地址等信息,这对于分析 DMA 缓冲区的使用情况非常有帮助。例如,如果发现某个设备的 DMA 映射过多或缓冲区大小不合理,就可以进一步排查是否存在资源浪费或内存泄漏的问题。last_fail文件则记录了最近一次 DMA 操作失败的详细信息,包括失败的原因、失败时的传输参数等,这对于定位 DMA 传输错误非常关键。当出现 DMA 传输失败的情况时,查看last_fail文件可以快速了解错误发生的原因,从而有针对性地进行修复。
通过读取这些文件,我们可以获取到关键信息来定位驱动中的问题。例如,假设我们发现某个设备的 DMA 传输经常失败,通过查看last_fail文件,发现是由于传输长度设置错误导致的。那么我们就可以在驱动中检查传输长度的设置逻辑,进行相应的修改,以解决 DMA 传输失败的问题。
4.2.3 检测内存泄漏
首先,需要启用CONFIG_DEBUG_KMEMLEAK编译选项。在执行make menuconfig命令进入内核配置界面后,找到 "Kernel hacking" 选项,进入后再找到 "Memory Debugging" 选项。在 "Memory Debugging" 中,有一个 "Kmemleak memory leak detector" 选项,通过选中这个选项,将启用kmemleak工具。启用这个编译选项后,内核将包含kmemleak相关的代码,用于检测内存泄漏。
在系统运行时,可以通过echo scan > /sys/kernel/debug/kmemleak命令来触发内存扫描。kmemleak工具会遍历系统中的所有内存对象,检查是否存在未释放的内存块。扫描完成后,可以通过查看/sys/kernel/debug/kmemleak文件来获取泄漏报告。这个报告中会详细列出所有检测到的内存泄漏信息,包括泄漏内存块的大小、分配位置(即分配该内存块的代码位置)、引用计数等。通过分析这些信息,我们可以快速定位到未释放的 DMA 缓冲区问题。例如,如果报告中显示某个 DMA 缓冲区在驱动的某个函数中分配后一直未释放,我们就可以在该函数中检查内存释放的逻辑,添加正确的释放代码,以解决内存泄漏问题。
五、 优化:Linux DMA 使用的关键要点
5.1 常见错误与解决方案
1)、缓冲区分配失败:
在使用dma_alloc_coherent等函数分配 DMA 缓冲区时,有时会出现分配失败的情况,返回NULL指针。
这可能是由于内存不足导致的,当系统中没有足够的连续物理内存来满足 DMA 缓冲区的分配需求时,就会出现这种情况。还有可能是分配标志使用不当,例如在不允许睡眠的场景中使用了GFP_KERNEL标志,或者在需要原子分配的场景中使用了不恰当的标志。 针对内存不足的问题,我们可以尝试调整系统的内存分配策略,例如增加系统的内存容量,或者优化内存管理,减少不必要的内存占用。在驱动中,可以通过检查系统的内存使用情况,提前释放一些不必要的内存资源,为 DMA 缓冲区的分配腾出空间。对于分配标志使用不当的问题,需要仔细分析代码的执行场景,确保使用正确的分配标志。如果是在中断上下文或者其他不允许睡眠的场景中,应该使用GFP_ATOMIC标志;如果是在可以睡眠的场景中,并且需要保证内存的一致性,可以使用GFP_KERNEL标志。同时,在分配失败时,需要进行适当的错误处理,例如打印错误信息,释放已分配的资源,避免内存泄漏。
2)、通道申请超时:
当调用dma_request_channel请求 DMA 通道时,可能会出现长时间等待甚至超时的情况。
这通常是因为系统中所有的 DMA 通道都被其他设备占用,没有可用的通道。另外,如果设备树配置错误,例如 DMA 通道的属性设置不正确,或者设备树中没有正确描述 DMA 控制器与设备的连接关系,也可能导致通道申请失败。 为了解决通道被占用的问题,可以优化设备的 DMA 通道使用策略,合理分配和释放 DMA 通道。在驱动中,可以通过记录 DMA 通道的使用情况,避免重复申请已经被占用的通道。当检测到通道被占用时,可以等待一段时间后重新申请,或者通知用户设备当前无法使用 DMA 功能。对于设备树配置错误的问题,需要仔细检查设备树文件,确保 DMA 通道的属性设置正确,并且设备树中正确描述了 DMA 控制器与设备的连接关系。可以参考硬件手册和相关的设备树示例,对设备树进行逐一检查和修正。同时,在设备树更新后,需要重新启动系统,使配置生效。
3)、传输状态码异常:
在 DMA 传输完成后,通过dmaengine_tx_status获取的传输状态码可能显示传输失败。
这可能是由于传输参数配置错误引起的,例如源地址或目标地址设置错误,导致 DMA 控制器无法正确访问数据;传输长度设置错误,可能导致数据传输不完整或溢出。还有可能是硬件故障导致的,例如 DMA 控制器损坏、内存故障等。 对于传输参数配置错误的问题,需要仔细检查传输参数的设置,确保源地址、目标地址和传输长度等参数正确无误。在驱动中,可以通过打印调试信息,输出传输参数的值,以便于检查和调试。如果是硬件故障导致的问题,需要使用硬件检测工具对硬件进行检测和诊断。可以使用示波器等工具检查硬件的信号传输是否正常,使用内存检测工具检查内存是否存在故障。如果确定是硬件故障,需要及时更换硬件设备,以确保系统的正常运行。同时,在驱动中,可以添加硬件故障检测和处理机制,当检测到硬件故障时,及时通知用户并采取相应的措施,例如关闭设备、记录错误日志等。
5.2 性能优化技巧
5.2.1 多通道 DMA 与批量传输
与单通道 DMA 相比,多通道 DMA 具有显著的优势。在单通道 DMA 中,所有的数据传输都依赖于一个通道,这就容易形成传输瓶颈。例如,当一个设备需要传输大量数据时,单通道 DMA 可能需要长时间占用通道资源,导致其他设备的传输请求被阻塞,从而降低了系统的整体性能。而多通道 DMA 则不同,它允许多个设备同时使用不同的 DMA 通道进行数据传输。在一个包含网络设备、存储设备和音频设备的系统中,网络设备可以使用一个 DMA 通道接收网络数据包,存储设备可以使用另一个 DMA 通道进行文件读写操作,音频设备则可以使用第三个 DMA 通道进行音频数据的传输。这样,各个设备的数据传输可以并行进行,大大提高了系统的数据传输能力,避免了单通道传输的瓶颈问题。
在分配 DMA 通道时,需要考虑设备的优先级和数据传输需求。对于优先级较高的设备,如实时性要求较高的音频设备或视频设备,应该优先分配 DMA 通道,以确保它们的数据能够及时传输,避免出现音频卡顿或视频掉帧的现象。对于数据传输需求较大的设备,如存储设备,也应该分配性能较好的 DMA 通道,以提高数据传输的速度。可以根据设备的类型、数据传输量和实时性要求等因素,制定合理的 DMA 通道分配策略。在一个服务器系统中,网络设备和存储设备的数据传输量较大,且对实时性要求较高,因此可以为它们分配高性能的 DMA 通道;而一些辅助设备,如打印机等,数据传输量较小,实时性要求也较低,可以为它们分配性能相对较低的 DMA 通道。
在传统的 DMA 传输中,每次传输完成后都会产生一个中断通知 CPU,这会导致 CPU 频繁地响应中断,从而增加 CPU 的负担。而批量传输策略则是将多个小的传输请求合并为一个大的传输请求,减少中断的次数。例如,在网络通信中,通常会有大量的小数据包需要传输。如果每个数据包都单独进行 DMA 传输并产生中断,那么 CPU 将忙于处理这些中断,无法高效地执行其他任务。而采用批量传输策略,将多个小数据包合并成一个大的数据包进行 DMA 传输,这样就可以减少中断的次数,使 CPU 能够有更多的时间执行其他任务,从而提升系统的整体性能。在实现批量传输时,需要根据实际情况合理设置批量大小。如果批量大小设置过小,可能无法充分发挥批量传输的优势;如果批量大小设置过大,可能会导致数据传输的延迟增加。因此,需要通过实验和测试,找到一个合适的批量大小,以达到最佳的性能优化效果。
5.2.2 结合 IOMMU:突破物理内存连续限制
在传统的 DMA 传输中,由于 DMA 控制器通常要求数据的物理地址是连续的,这就限制了内存的分配和使用效率。当系统中没有足够大的连续物理内存块时,即使有大量的零散物理内存可用,也无法满足 DMA 传输的需求,从而导致数据传输失败或效率低下。
IOMMU 的主要功能是将设备使用的总线地址映射为主存的物理地址,它就像是一个智能的地址翻译器,能够将设备发出的看似不连续的地址请求,准确地转换为实际物理内存中的地址。在开启 IOMMU 的场景下,DMA 缓冲区无需物理连续。例如,当设备需要进行 DMA 传输时,IOMMU 可以将多个零散的物理页映射为连续的设备地址,使得设备在进行 DMA 传输时,就像访问一块连续的物理内存一样。这极大地提升了内存分配的灵活性,系统可以更充分地利用内存资源,避免了因物理内存不连续而导致的 DMA 传输问题。
以视频处理场景为例,在处理高清视频时,需要传输大量的视频数据。这些数据可能分散存储在内存的不同位置,如果没有 IOMMU,可能很难找到一块足够大的连续物理内存来满足 DMA 传输的需求。而有了 IOMMU,它可以将这些分散的物理页映射为连续的设备地址,使得视频数据能够顺利地通过 DMA 传输到 GPU 进行处理,大大提高了视频处理的效率和流畅度。同时,IOMMU 还支持内存保护和虚拟化功能,在多用户或多虚拟机环境中,IOMMU 可以确保每个用户或虚拟机的 DMA 访问都被限制在其各自的内存空间内,提高了系统的安全性和稳定性。
5.3 关键注意事项:地址对齐与硬件适配
地址对齐对 DMA 传输的性能和稳定性有着直接的影响。DMA 缓冲区需按缓存行对齐,这是因为现代计算机系统中的缓存通常是以缓存行为单位进行管理的。缓存行是缓存与内存之间数据交换的最小单位,常见的缓存行大小为 64 字节或 128 字节。当 DMA 缓冲区按缓存行对齐时,数据的读取和写入可以更高效地利用缓存,减少缓存未命中的情况,从而提高数据传输的速度。
在代码中,我们可以使用 ALIGN 宏来实现地址对齐。以dma_alloc_coherent函数为例,假设我们需要分配一个大小为buffer_size的 DMA 缓冲区,并使其按 64 字节对齐,可以这样实现:
#include <linux/dma-mapping.h>
#include <linux/device.h>
struct device *dev = get_device(); // 获取设备指针
size_t buffer_size = 1024; // 假设需要分配1024字节的缓冲区
size_t aligned_size = (buffer_size + 63) & ~63; // 计算对齐后的大小
dma_addr_t dma_addr;
void *buffer = dma_alloc_coherent(dev, aligned_size, &dma_addr, GFP_KERNEL);
if (!buffer) {
// 缓冲区分配失败的错误处理
printk(KERN_ERR "Failed to allocate DMA buffer\n");
return -ENOMEM;
}
在这段代码中,首先通过(buffer_size + 63) & ~63计算出对齐后的大小aligned_size,然后使用dma_alloc_coherent函数分配对齐后的缓冲区。这样分配的缓冲区地址是 64 字节对齐的,能够满足 DMA 传输的要求。
除了地址对齐,硬件适配也是驱动开发中不可忽视的环节。不同的硬件平台具有不同的特性,其 DMA 控制器的寄存器设置、操作方式和传输能力等都可能存在差异。因此,在开发驱动时,需要根据外设寄存器特性调整代码,以适配不同硬件平台的 DMA 控制器差异。在一些硬件平台上,DMA 控制器的寄存器地址可能与其他平台不同,或者寄存器的位定义和操作方式也有所不同。在这种情况下,驱动开发者需要仔细阅读硬件手册,了解硬件平台的 DMA 控制器特性,然后相应地调整驱动代码中的寄存器访问和配置逻辑。如果不进行硬件适配,可能会导致驱动在某些硬件平台上无法正常工作,或者 DMA 传输出现错误。因此,在开发驱动时,要充分考虑硬件平台的多样性,编写具有良好兼容性和可移植性的代码,以确保驱动能够在不同的硬件平台上稳定运行。