DMA 与 scatterlist 技术简介

2018年6月26日 17.83k 次阅读 0 条评论 15 人点赞

目录

DMA 是一项很古老的技术,在 Wiki 中是这样定义的:直接内存访问(Direct Memory Access,DMA)是计算机科学中一种内存访问技术,它允许某些计算机内存的硬件子系统(外设)可以独立且直接的访问系统内存,不需要 CPU 的参与。在同等程度的处理器负担情况下,DMA 是一种快速的数据传递方式,所以很多的硬件都会使用到 DMA,以及操作系统也会对 DMA 进行足够的支持,比如我们常见到的硬盘、显卡、网卡声卡等。

DMA 的由来

我在文件系统设计与实现【1】 - I/O 设备中有提到过关于 DMA 作为更高效的处理数据方式存在,但是说的比较浅显,今天可以深入的了解什么是 DMA,以及如何通过使用 Linux 系统内核的 API 操作 DMA。在最初的 PC 体系结构中,CPU 是唯一的总线主控器,也就是说,为了提取和存储 RAM 存储单元的数据,CPU 是唯一一个可以驱动地址/数据总线的硬件设备,那么 CPU 就需要承担全部的数据迁移工作,干扰正常的程序执行。随着更多的比如 PCI 总线的出现,如果提供合适的电路基础,每一个外围设备都是可以充当总线主控器,因此每一个设备都可以自主的控制 RAM 和 IO 设备之间的数据传输。这样,CPU 仅仅只需要设置 DMA 需要操作的源地址,目的地址以及数据长度之后,激活 DMA,它就可以自行将数据搬移;在完成传输之后,DMA 再发出一个中断请求通知 CPU 数据迁移完成,剩下的程序逻辑交由 CPU 管理。从这里来看,DMA 还真实一个勤劳的小蜜蜂,自己干着老板交代的脏活累活,最后成绩都是老板的,的确是很有资本主义特色。还有一个小细节,CPU 这个老板并不是什么任务都交给 DMA 这个小蜜蜂,就好比老板伸手就能干完的事,何必让秘书来做呢?还得叫人,还得处理等等一个很长的流程,所以也不是什么时候都一定需要 DMA 的参与,在数据很小的情况下,CPU 自己完成数据迁移效率会比 DMA 更高,毕竟启动 DMA 和切换进程上下文都是需要时间的。

我们应该感谢 DMA 的存在,可以让在 CPU 不参与的情况下完成数据的传输,正是这样的一个小小的部件的存在让电脑不在变得更卡,可以愉快的 do anything you wana do.

DMA 的原理和实现

我相信,只要智商正常的人在读完前文 DMA 的由来之后就能很轻松的理解 DMA 的原理是什么,如果你不知道,那么我只能再告诉你一遍:DMA 的原理就是 CPU 将需要迁移的数据的位置告诉给 DMA,包括源地址,目的地址以及需要迁移的长度,然后启动 DMA 设备,DMA 设备收到命令之后,就去完成相应的操作,最后通过中断反馈给老板 CPU,结束。是不是很简单?是的没错。

那么原理很简单,实现起来呢?在实现 DMA 传输时,是 DMA 控制器掌控着总线,也就是说,这里会有一个控制权转让的问题,我们当然知道,计算机中最大的 BOSS 就是 CPU,这个 DMA 暂时掌管的总线控制权当前也是 CPU 赋予的,在 DMA 完成传输之后,会通过中断通知 CPU 收回总线控制权。还是计算机世界淳朴,DMA 会在完成自己的工作之后,毫不犹豫的交出已经转移到自己手里的总线控制权,在现实世界那可就不一定了,交出去容易,拿回来难。

一个完整的 DMA 传输过程必须经过 DMA 请求、DMA 响应、DMA 传输、DMA 结束这四个阶段。

DMA 请求:CPU 对 DMA 控制器初始化,并向 I/O 接口发出操作命令,I/O 接口提出 DMA 请求
DMA 响应:DMA 控制器对 DMA 请求判别优先级以及屏蔽位,向总线裁决逻辑提出总线请求,当 CPU 执行完成当前的总线周期之后即可释放总线控制权。此时,总线裁决逻辑输出总线应答,表示 DMA 已经就绪,通过 DMA 控制器通知 I/O 接口开始 DMA 传输。
DMA 传输:在 DMA 控制器的引导下,在存储器和外设之间进行数据传送,在传送过程中不需要 CPU 的参与
DMA 结束:当完成既定操作之后,DMA 控制器释放总线控制权,并向 I/O 接口发出结束信号,当 I/O 接口收到结束信号之后,一方面停止 I/O 设备的工作,另一方面向 CPU 提出中断请求,使 CPU 从不介入状态解脱,并执行一段检查本次 DMA 传输操作正确性的代码。最后带着本次操作的结果以及状态继续执行原来的程序。

由此可见,DMA 传输方式无需 CPU 直接控制传输,也没有中断一样的保留现场和现场恢复,通过硬件为 RAM 和 I/O 设备开辟了一条新的直接传输数据的通道,使 CPU 的效率大大提高。

DMA 控制器与 CPU 怎样分时使用内存

DMA 技术的出现,使得外围设备可以通过 DMA 控制器直接访问内存,与此同时,CPU 可以继续执行程序逻辑,通常采用以下三种方法实现 DMA 控制机与 CPU 分时使用内存:(1)停止 CPU 访问内存;(2)周期挪用;(3)DMA 与 CPU 交替访问内存。

(1)停止 CPU 访问内存

当外围设备要求传送一批数据时,由 DMA 控制器发一个停止信号给 CPU,要求 CPU 放弃对地址总线、数据总线和有关控制总线的使用权,DMA 控制器获得总线控制权之后,开始进行数据传输,在一批数据传输完毕之后,DMA 控制器通知 CPU 可以继续使用内存,并把总线控制权交还给 CPU。图(a)就是这样的一个传输图,很显然,在这种 DMA 传输过程中,CPU 基本处于不工作状态或者保持状态。

优点:控制简单,他是用于数据传输率很高的设备进行成组的传输。

缺点:在 DMA 控制器访问内存阶段,内存效能没有充分发挥,相当一部分的内存周期是空闲的,这是因为外围设备传送两个数据之间的间隔一般大于内存存储间隔,即使是再告诉的 I/O 存储设备也是如此(就是内存读写速率比 SSD 等都快)。

(2)周期挪用

当 I/O 设备没有 DMA 请求时,CPU 按照程序要求访问内存;一旦 I/O 设备有 DMA 请求,则由 I/O 设备挪用一个或者几个内存周期,这种传送方式如下图所示:

I/O 设备要求 DMA 传送时可能遇到两种情况:

第一种:此时 CPU 不需要访问内存,例如,CPU 正在执行乘法指令,由于乘法指令执行时间长,此时 I/O 访问与 CPU 访问之间没有冲突,则 I/O 设备挪用一两个内存周期是对 CPU 执行没有任何的影响。

第二种:I/O 设备要求访问内存时,CPU 也需要访问内存,这就产生了访问内存冲突,在这种情况下 I/O 设备优先访问,因为 I/O 访问有时间要求,前一个 I/O 数据必须在下一个访问请求来到之前存储完毕。显然,在这种情况下,I/O 设备挪用一两个内存周期,意味着 CPU 延缓了对指令的执行,或者更为明确的讲,在 CPU 执行访问指令的过程中插入了 DMA 请求。周期挪用内存的方式与停止 CPU 访问内存的方式对比,这种方式,既满足了 I/O 数据的传送,也发挥了 CPU 和内存的效率,是一种广泛采用的方法,但是 I/O 设备每一周期挪用都有申请总线控制权,建立总线控制权和归还总线控制权的过程,所以不适合传输小的数据。这种方式比较适用于 I/O 设备读写周期大于内存访问周期的情况。

(3)DMA 和 CPU 交替访问内存

如果 CPU 的工作周期比内存的存储周期长很多,此时采用交替访问内存的方法可以使 DMA 传送和 CPU 同时发挥最高效率。这种传送方式的时间图如下:

此图是 DMA 和 CPU 交替访问内存的消息时间图,假设 CPU 的工作周期为 1.2us,内存的存储周期小于 0.6us,那么一个 CPU 周期可以分为 C1 和 C2 两个分周期,其中 C1 专供 DMA 控制器访问内存,C2 专供 CPU 访问内存。这种方式不需要总线使用权的申请、建立和归还的过程,总线的使用权是通过 C1 和 C2 分时机制决定的。CPU 和 DMA 控制器各自有自己的访问地址寄存器、数据寄存器和读写信号控制寄存器。在 C1 周期中,如果 DMA 控制器有访问请求,那么可以将地址和数据信号发送到总线上。在 C2 周期内,如果 CPU 有访问内存的请求,同样将请求的地址和数据信号发送到总线上。事实上,这是用 C1 C2 控制的一个多路转换器,这种总线控制权的转移几乎不需要什么时间,所以对于 DMA 来讲是很高效率的。

这种传送方式又称为“透明 DMA”方式,其由来是由于 DMA 传送对于 CPU 来讲是透明的,没有任何感觉。在透明 DMA 方式下 CPU 不需要停止主程序的运行,也不进入等待状态,是一种很高效率的工作方式,当然,相对应的硬件设计就会更加复杂。

以上是传统意义的 DMA:是一种完全由硬件执行 I/O 交换的工作方式。这种方式中,DMA 控制器和 CPU 完全接管对总线的控制,数据交换不经过 CPU,而直接在内存和 I/O 设备之间进行。DMA 工作时,由 DMA 控制器向内存发起地址和控制信号,进行地址修改,对传输的数据进行统计,并以中断的形式通知 CPU 数据传输完成。

DMA 导致的问题

DMA 不仅仅只会带来效率的提升,同样,他也会带来一些问题,最明显的就是缓存一致性问题。想象一下,现代的 CPU 都是自带一级缓存、二级缓存甚至是三级缓存,当 CPU 访问内存某个地址时,暂时先将新的值写入到缓存中,但是没有更新外部内存的数据,假设此时发生了 DMA 请求且操作的就是这一块在缓存中更新了而外部内存没有更新的内存地址,这样 DMA 读到的就是非最新数据;相同的,如果外部设备通过 DMA 将新值写入到内存中,但是 CPU 访问得到的确实缓存中的数据,这样也会导致拿到的不是最新的数据。

为了能够正确进行 DMA 操作,必须进行必要的 Cache 操作,Cache 的操作主要分为 invalidate(作废)和 writeback(回写),有时候也会二者一同使用。如果 DMA 使用了 Cache,那么 Cache 一致性问题是必须要考虑的,解决的最简单的办法就是禁止 DMA 目标地址范围的 Cache 功能,但是这样会牺牲掉一定的性能。因此,在 DMA 是否使用 cache 的问题上,可以根据 DMA 缓冲区的期望保留时间长短来决策。DMA 被区分为了:一致性 DMA 映射和流式 DMA 映射。

一致性 DMA 映射:他申请的缓存区之后会被以非缓存的形式映射,一致性映射具有很长的生命周期,在这段时间内占用映射寄存器,即使不再使用也不会释放,一般情况下,一致性 DMA 的生命周期会被设计为驱动的生命周期(也就是在 init 里面注册,在 exit 里面释放)。

流式 DMA 映射:他的实现比较复杂,表现特征为使用周期很短,他的实现中会主动保持缓存的一致性。在使用方法上,流式 DMA 还需要指定内核数据的流向,不然会导致不可预期的后果。不过很多的现代处理能能够自己来保证 CPU 和 DMA 控制器之间的 cache 一致性问题,比如 ARM 的 ACP 功能,这样像dma_map_single函数只是返回物理地址,而dma_unmap_single则什么都不做,这样极大的提升了系统性能。

关于流式 DMA 和一致性 DMA 的区别

这一段是在我读完 DMA 整个流程之后新增的,之前一直不是特别的理解这二者的区别,在内核 Document 内也仅仅推荐什么场合该使用什么类型的映射,但是一直对其中的原理不甚理解,今天特地与其他的同事通过一番讨论之后对 DMA 这一块有了全新的理解。

  1. 在一致性 DMA 映射中,它采用的是系统预留的一段 DMA 内存用于 DMA 操作,这一段内核在系统启动阶段就已经预留完毕,比如 arm64 平台会在 dts 文件中写明系统预留的 DMA 内存段位于何处,并且会被标志为用于 dma 一致性内存申请,如果你有关注 DMA 的一致性映射操作 API 就会发现,一致性 DMA 不会去使用别的地方申请的内存,他都是通过dma_alloc_coherent自我申请内存,然后驱动自己填充数据最后被提交给 DMA 控制器。
  2. 流式 DMA 中,他可以是随意的内存交给 DMA 进行处理,你不需要从系统预留的 DMA 位置进行内存申请,任何普通的 kmalloc 申请的内存都能交给 DMA 控制器进行操作。

上面的两点是他们二者一个很重要的区别,我不知道为何在其他的文章中没有被写到过,那么以此衍生开的就是这二者是如何做到缓存一致性的:一致性 DMA 要做到缓存一致性很简单,在 DMA 内存申请的过程中,首先进行一个 ioremap_nocache 的映射,然后调用函数 dma_cache_wback_inv 保证缓存已经刷新到位之后,后面使用这一段内存时不存在一二级缓存;而流式 DMA 则比较复杂,他不能直接禁止缓存,因为流式 DMA 可以使用系统中的任意地址范围的地址,CPU 总不能将系统所有的地址空间都禁止缓存,这不科学,那么为了实现缓存一致性,流式 DMA 需要不断的对缓存进行失效操作,告诉 CPU 这一段缓存是不可信的,必须从内存中重新获取。说到这里,我想起之前的有篇文章提到过说他们二者的区别就是一个是禁止缓存,一个是刷缓存,但是他没有讲清楚,到底谁是刷缓存,谁是禁止缓存呢?现在从上面的理解来看,很明显,一致性 DMA 就是直接将缓存禁止,而流式 DMA 则是将缓存失效刷新。

Linux kernel scatterlist API 介绍

我们经常在那些需要和用户空间进行大量数据交互的子系统(例如 MMC、Video、Audio)中,随处可见 scatterlist 的影子,对于非英语国家的人来讲,可能很难以理解什么是散列表,其实这个也不难理解,他省略了一个主语,那就是物理内存,他的全称为物理内存散列表,作用就是将分散的物理内存用链表的形式组织起来。

假设如下图的一个系统中,有三个模块可以访问内存:CPU、DMA 和外设,CPU 通过 MMU 虚拟地址(VA)的形式访问内存;DMA 直接以物理地址的形式访问内存;设备通过自己的 IOMMU 以设备地址访问内存。然后某个软件分配了并使用了一片内存区域(如下图2),该内存空间在 CPU 的视角上(VA)是连续的,其实地址是 va1,实际上他是由三块物理地址组成(pa1,pa2,pa3)。

那么,如果该软件单纯以 CPU 视角访问这段内存,那么一切都是那么顺畅,不存在任何问题,就像运行在操作系统之上的进程,他们大部分都是运行在虚拟地址空间,尽管在物理地址上没有连续,但是 MMU 保证了连续的 VA 到非连续的 PA 的映射。

不过,如果软件经过一系列的操作之后,要把该段内存区域交给 DMA 控制器,最终由 DMA 控制器将其中的数据搬移到某个外设时,由于 DMA 控制器只能访问物理地址,但是此处的 VA 是由三块互不关联的物理地址组成,那么就需要分别发起三次 DMA 请求,既然如此,为何不把它做成一个链表,直接发给 DMA 控制器,告诉 DMA 有哪些区域是需要这一次 DMA 需要搬移的呢?

散列表由此诞生,为了方便对此类不连续的物理地址进行管理,我们将他们链接起来,形成一个数据结构,那么就是散列表,这样,我们就可以很容易的将 VA 描述的连续虚拟空间用一个散列表就将其的物理地址描述清晰了。本质上,scatterlist 是各种不同的地址映射空间(PA、VA、DA)之间的媒介(因为物理地址是真实的,他可以作为通用语言在各个地址之间传递)。

图1 三种地址空间

图1 三种地址空间

图2 va 与 pa 的一个映射关系

图2 va 与 pa 的一个映射关系

DMA 的内核编程 API

使用大块 DMA 一致性缓冲区

void *
dma_alloc_coherent(struct device *dev, size_t size,
                dma_addr_t *dma_handle, gfp_t flag)

一致性内存:设备对这一块内存进行读写操作而无需担心处理器一级二级等高速缓存的影响。此函数申请一段大小为 Size 字节的一致性内存,返回两个参数,一个是 dma_handle,他可以用作这段内存的物理地址,另外一个就是指向被分配的内存的指针(处理器的虚拟地址)。

注意:由于某些平台上一致性内存的代价很高,比如最小的分配长度为一个页,因此你应该尽可能的合并申请一段一致性内存,最简单的方法就是使用 dma_pool 系列函数。

参数 flag 与一般 kmalloc 的内存申请标志一致。

void
dma_zalloc_coherent(struct device *dev, size_t size,
                       dma_addr_t *dma_handle, gfp_t flag)

这个函数仅仅是对dma_alloc_coherent的一个封装,如果内存分配成功,则直接将内存清零。有时候,清零工作是必要的。

void
dma_free_coherent(struct device *dev, size_t size, void *cpu_addr,
                       dma_addr_t dma_handle)

释放之前申请的一致性内存,dev、size、dma_handle 都是与申请的时候一致的参数,cpu_addr 就是申请函数返回的 cpu 端的虚拟地址。

有个很重要需要注意的就是:和其他的内存分配函数不同,这些函数必须要在中断使能的情况下使用。

使用小块 DMA 一致性缓冲区

如果你需要用到大量的小块的 DMA 缓冲区,最佳的方式就是使用 DMA 内存池的方式申请而不是以页的单位申请内存,这种机制有点类似于 struct kmem_cache,只是他利用了 DMA 的一致性内存分配器,而不是调用 __get_free_pages() 函数。同样的,DMA 内存池还需要知道通用硬件的对齐限制,比如队列头需要 N 字节对齐等。

struct dma_pool *
dma_pool_create(const char *name, struct device *dev,
        size_t size, size_t align, size_t alloc);

dma_pool_create 函数为设备初始化 DMA 一致性内存的内存池,他必须要在可睡眠上下文调用。name 为内存池的名字(就像 struct kmem_cache .name 一样)。dev 及 size 就如 dma_alloc_coherent 函数的参数一样。align 为设备硬件需要的对齐大小(单位为字节,必须为 2 的幂次方)。如果设备没有边界限制,可以设置参数为 0,如果设置为 4096,则表示从内存池分配的内存不能超过 4k 字节的边界。

void *
dma_pool_free(struct dma_pool *pool, void *addr,
        dma_addr_t addr);

返回内存给内存池,参数 pool 为传递给 dma_pool_alloc 的 pool,参数 vaddr 及 addr 为 dma_pool_alloc 的返回值,也就是从内存池申请到的虚拟地址。

void
dma_pool_destroy(struct dma_pool *pool);

内存池析构函数用于释放整个内存池的资源,这个函数可以在睡眠上下文调用,请确认调用此函数时,所有从该内存池申请的内存都已归还内存池。

DMA 寻址限制

做 DMA 操作之前,我们都需要确认设备是否支持足够的寻址宽度的 DMA 类型。

int
dma_supported(struct device *dev, u64 mask);

用来检测该设备是否支持掩码 mask 定义的寻址宽度,比如 0xffffff,表示是否支持 24 位宽度的寻址,返回 1 表示支持,返回 0 表示不支持。

int
dma_set_mask(struct device *dev, u64 mask);

该函数用来验证掩码是否合法,如果合法则更新设备的寻址能力,当然,这个函数存在的主要目标就是更新 DMA 的寻址宽度能力。返回 0 表示成功,返回负值表示失败。

u64
dma_get_required_mask(struct device *dev);

该函数返回平台可以高效工作的掩码,通常这意味着返回掩码是可以寻址到所有内存的最小值,检查该值可以让 DMA 描述符的大小尽可能的小,获取到该值不会改变当前设备的掩码,你需要配合使用 dma_set_mask 来改变当前掩码。

流式 DMA 映射

dma_addr_t
dma_map_single(struct device *dev, void *cpu_addr, size_t size,
        enum dma_data_direction direction);

映射一块 CPU 的地址为 DMA 地址,流式 DMA 中一定需要指定数据流向。

枚举类型
功能
DMA_NONE 仅用于调试目的
DMA_TO_DEVICE 数据从内存传输到设备,可以认为是写操作
DMA_FROM_DEVICE 数据从设备传输到内存,可以认为是读操作
DMA_BIDIRECTIONAL 不清楚传输方向,这样做会导致性能问题

请注意,并不是一台机器上所有的内存区域都可以用这个 API 进行映射,对于内核中连续的虚拟地址空间并不一定在物理地址上连续(比如内核中的某些内存使用的是 vmalloc 申请的),因为这种函数并未提供任何的分散/聚集能力,因此用户在企图映射一块非物理连续的内存时,会返回失败,基于此原因,在使用被函数之前,请务必确认缓冲区的物理内存连续(比如使用 kmalloc)进行内存申请。还有另外一个原因就是,所申请的物理地址必须要在设备的 dma_mask 寻址范围以内,不然 DMA 是无法寻址到该内存。为了确保由 kmalloc 申请的内存在 dma_mask 上,驱动程序需要定义板级相关的标志位来限制分配的物理内存范围(比如 X86 上,GFP_DMA 用于保证申请的内存在可用物理内存的前 16MB 空间,可以由 ISA 设备使用)。

另外还有一个需要注意的一点就是,如果平台有 IOMMU (设备拥有 MMU 单元,可以进行 I/O 总线和设备的映射,即总线地址和内存物理地址的映射)单元,则上述物理地址的连续性以及外设寻址能力的限制就不存在,你可以任意为之,不过为了通用,你最好假定这样的 IOMMU 不存在。

DMA_TO_DEVICE: 软件对内存区域做最后一次修改后,在传输给设备之前,需要做一次同步。一旦使用该原语,内存区域可被视作设备的只读缓冲区。

DMA_FROM_DEVICE: 驱动在访问数据前必须做一次同步,因为数据可能被设备再次修改了。内存缓存区可被当做驱动的只读缓冲区。

DMA_BIDIRECTIONAL: 需要特别处理:这意味着驱动并不确定内存数据传输到设备前,是否已经被修改了,同时也不确定设备是否会修改内存,因此需要做两次双向同步内存操作:一次在内存数据传输到设备前(确保是所有缓冲区数据改变都从处理器的高速缓存刷新到内存中),另外一次就是设备可能访问该缓冲区前(确保所有处理器的高速缓存行都得到了更新,设备可能改变了缓冲区的数据)。即在处理器写操作时,需要做一次刷高速缓存的操作,以确保数据都同步到了内存缓冲区。在处理器读之前,需要更新一次高速缓冲区行,以确保设备对内存缓冲区的改变都同步到了高速缓冲区中。

void
dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size,
        enum dma_data_direction direction)

取消先前的内存映射,传入该函数的输入和dma_map_single相同,dma_addr 就是用 map 函数映射出来的地址。

dma_addr_t
dma_map_page(struct device *dev, struct page *page,
        unsigned long offset, size_t size,
        enum dma_data_direction direction)

void
dma_unmap_page(struct device *dev, dma_addr_t dma_address, size_t size,
        enum dma_data_direction direction)

上面的两个函数对单页进行映射/取消映射,里面的 offset 及 size 可用于对部分页进行映射,如果你对高速缓存行的宽度不清楚的话,不要使用这些参数。

int
dma_mapping_error(struct device *dev, dma_addr_t dma_addr)

某些场景下,通过 dma_map_single 及 dma_map_page 创建映射可能会失败,驱动程序可以通过此函数来检测这些错误。一个非 0 的返回值表示未成功创建映射关系,驱动程序需要采用使用的措施来解决该问题(比如降低当前 DMA 的使用率或者等待一段时间再继续尝试)

int
dma_map_sg(struct device *dev, struct scatterlist *sg,
        int nents, enum dma_data_direction direction)

返回值:被映射的物理内存块的数量(如果再 scatterlist 表中一些元素是物理地址或者虚拟地址相邻的,且 IOMMU 可以将他们映射成单个内存块,则返回值可能比输入值 nents 小)。

需要注意的是,如果 sg 已经被映射过了,那么不能再次被映射,再次映射会销毁掉 sg 中的信息。如果返回 0,则表示 dma_map_sg 失败,驱动程序需要采取适当的措施,驱动程序在此时做一些事情显得格外重要,一个阻塞驱动中断请求或者 oopsing 都总比什么都不做导致文件系统瘫痪强许多。

下面就是一个 scatterlist 的例子

int i, count = dma_map_sg(dev, sglist, nents, direction);
struct scatterlist *sg;

for_each_sg(sglist, sg, count, i) {
        hw_address[i] = sg_dma_address(sg);
        hw_len[i] = sg_dma_len(sg);
}

其中 nents 是 sglist 的条目数量,这种实现可以很方便的将几个连续的 sglist 条目合并为一个(比如 IOMMU 中,或者一些页正好是物理连续的),然后你就可以循环多次(可能小于 nents 次)使用 sg_dma_address 及 sg_dma_len 来获取 sg 的物理地址以及长度。

void
dma_unmap_sg(struct device *dev, struct scatterlist *sg,
        int nhwentries, enum dma_data_direction direction)

取消先前分散/聚集链表的映射,所有参数和分散/聚集映射 API 的参数相同。

本文 DMA 部分素材来自 SSD 社区网,有兴趣的同学可以自行前往学习。
本文 DMA API 素材部分来自 CSDN 博客