@@ -4,7 +4,7 @@ categories:
44 - Linux 学习
55abbrlink: 484892ff
66date: 2024-10-24 15:00:00
7- updated: 2024-12-09 17:40 :00
7+ updated: 2024-12-10 20:15 :00
88---
99
1010<meta name="referrer" content="no-referrer"/>
@@ -4984,3 +4984,293 @@ Cache 和 DMA 本身似乎是两个毫不相关的事物。Cache 被用作 CPU
49844984
49854985**内存中用于与外设交互数据的区域称为 DMA 缓冲区。**在设备不支持 scatter/gather(分散/聚集,简称 SG)操作的情况下,DMA 缓冲区在物理上必须是连续的。
49864986
4987+ #### DMA 区域
4988+
4989+ 对于 x86 架构的 ISA 设备而言,其 DMA 操作只能在 16 MB 以下的内存中进行,因此,在使用 kmalloc()、`__get_free_pages()` 及其类似函数申请 DMA 缓冲区时应使用 GFP_DMA 标志,这样能保证获得的内存位于 DMA 区域中,并具备 DMA 能力。
4990+
4991+ 关于 ISA、PCI、PCIE 等总线协议的了解,参考:[https://blog.csdn.net/yinqiusheng/article/details/140387774](https://blog.csdn.net/yinqiusheng/article/details/140387774)
4992+
4993+ 内核中定义了使用 GFP_DMA 标志的申请 DMA 缓冲区的快捷函数 `__get_dma_pages()`,定义如下:
4994+
4995+ ```c
4996+ #define __get_dma_pages(gfp_mask, order) \
4997+ __get_free_pages((gfp_mask) | GFP_DMA, (order))
4998+ ```
4999+
5000+ 如果不想使用参数 order 申请 DMA 内存,可使用另一个函数 dma_mem_alloc(),定义如下:
5001+
5002+ ```c
5003+ static unsigned long dma_mem_alloc(unsigned long size)
5004+ {
5005+ // get_order():order = log2(size)
5006+ return __get_dma_pages(GFP_KERNEL, get_order(size));
5007+ }
5008+ ```
5009+
5010+ #### 虚拟地址、物理地址和总线地址
5011+
5012+ 基于 DMA 的硬件使用的是总线地址而不是物理地址,总线地址是从设备角度上看到的内存地址,物理地址则是从 CPU MMU 控制器外围角度上看到的内存地址(从 CPU 核角度看到的是虚拟地址)。虽然在 PC 上,对于 ISA 和 PCI 而言,总线地址即为物理地址,但并不是每个平台都是如此。因为有时候接口总线通过桥接电路连接,桥接电路会将 I/O 地址映射为不同的物理地址。
5013+
5014+ 内核提供了如下函数进行简单的虚拟地址/总线地址转换:
5015+
5016+ ```c
5017+ unsigned long virt_to_bus(void *address)
5018+ {
5019+ return (unsigned long)address;
5020+ }
5021+
5022+ void *bus_to_virt(unsigned long address)
5023+ {
5024+ return (void *)address;
5025+ }
5026+ ```
5027+
5028+ #### DMA 地址掩码
5029+
5030+ 设备不一定能在所有的内存地址上执行 DMA 操作,在这种情况下应该通过下列函数执行 DMA 地址掩码:
5031+
5032+ ```c
5033+ int dma_set_mask(struct device *dev, u64 mask);
5034+ ```
5035+
5036+ 这个函数的本质是修改 device 结构体中的 dma_mask 成员。在 device 结构体中,除了 dma_mask 以外,还有 coherent_dma_mask 成员。dma_mask 是设备 DMA 可寻址的范围,coherent_dma_mask 用作申请一致性 DMA 缓冲区。
5037+
5038+ ```c
5039+ struct device {
5040+
5041+ ...
5042+
5043+ u64 *dma_mask; /* dma mask (if dma'able device) */
5044+ u64 coherent_dma_mask;/* Like dma_mask, but for
5045+ alloc_coherent mappings as
5046+ not all hardware supports
5047+ 64 bit addresses for consistent
5048+ allocations such descriptors. */
5049+
5050+ ...
5051+ };
5052+ ```
5053+
5054+ #### 一致性 DMA 缓冲区
5055+
5056+ **DMA 映射包括两部分工作:分配一片 DMA 缓冲区;为这片缓冲区产生设备可访问的地址。**同时 DMA 映射也必须考虑 Cache 一致性问题。内核中提供了如下函数以分配一个 DMA 一致性的内存区域:
5057+
5058+ ```c
5059+ // 返回申请到的 DMA 缓冲区的虚拟地址。
5060+ // 通过参数 dma_handle 返回 DMA 缓冲区的总线地址。
5061+ void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t gfp)
5062+ {
5063+ return dma_alloc_attrs(dev, size, dma_handle, gfp,
5064+ (gfp & __GFP_NOWARN) ? DMA_ATTR_NO_WARN : 0);
5065+ }
5066+ ```
5067+
5068+ dma_alloc_coherent() 申请一片 DMA 缓冲区,进行地址映射并保证该缓冲区的 Cache 一致性。对应的释放函数为 dma_free_coherent()。
5069+
5070+ ```c
5071+ void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t dma_handle)
5072+ {
5073+ return dma_free_attrs(dev, size, cpu_addr, dma_handle, 0);
5074+ }
5075+ ```
5076+
5077+ 使用函数 dma_alloc_writecombine() 分配一个写合并(Writecombining)的 DMA 缓冲区,释放函数是 dma_free_coherent()。这两个接口在 Linux 5.15 以后已移除。
5078+
5079+ Linux 内核还提供了 PCI 设备申请 DMA 缓冲区的函数 pci_alloc_consistent(),释放函数是 pci_free_consistent()。
5080+
5081+ ```c
5082+ void *pci_alloc_consistent(struct pci_dev *hwdev, size_t size, dma_addr_t *dma_handle)
5083+ {
5084+ return dma_alloc_coherent(&hwdev->dev, size, dma_handle, GFP_ATOMIC);
5085+ }
5086+
5087+ void pci_free_consistent(struct pci_dev *hwdev, size_t size, void *vaddr, dma_addr_t dma_handle)
5088+ {
5089+ dma_free_coherent(&hwdev->dev, size, vaddr, dma_handle);
5090+ }
5091+ ```
5092+
5093+ > dma_alloc_xxx() 函数虽然以 `dma_alloc_` 开头,但是申请的区域不一定在 DMA 区域里。以 32 位 ARM 处理器为例,当 coherent_dma_mask 小于 0xffffffff 时,才会设置 GFP_DMA 标记,并从 DMA 区域申请内存。
5094+
5095+ #### 流式 DMA 映射
5096+
5097+ **并不是所有的 DMA 缓冲区都是驱动申请的,如果是驱动申请的,用一致性 DMA 缓冲区自然最方便,这直接考虑了 Cache 一致性问题。**但在许多情况下,缓冲区来自内核的较上层(如网卡驱动中的网络报文、块设备驱动中要写入设备的数据等),上层很可能用普通的 kmalloc()、`__get_free_pages()` 等方法申请,这时就要使用流式 DMA 映射。使用步骤一般如下:
5098+
5099+ 1. 进行流式 DMA 映射。
5100+ 2. 执行 DMA 操作。
5101+ 3. 进行流式 DMA 去映射(去掉映射)。
5102+
5103+ **流式 DMA 映射操作本质上大多是进行 Cache 的使无效或清除操作,以解决 Cache 一致性问题。**
5104+
5105+ ##### 单一缓冲区下的流式 DMA 映射
5106+
5107+ 对于单个已经分配的缓冲区而言,使用 dma_map_single() 可实现流式 DMA 映射。
5108+
5109+ ```c
5110+ #define dma_map_single(d, a, s, r) dma_map_single_attrs(d, a, s, r, 0)
5111+
5112+ // 映射成功返回总线地址,失败返回 NULL。
5113+ // dir:DMA 的方向,包括 DMA_TO_DEVICE、DMA_FROM_DEVICE、DMA_BIDIRECTIONAL 和 DMA_NONE 等。
5114+ dma_addr_t dma_map_single_attrs(struct device *dev, void *ptr, size_t size, enum dma_data_direction dir, unsigned long attrs)
5115+ {
5116+ /* DMA must never operate on areas that might be remapped. */
5117+ if (dev_WARN_ONCE(dev, is_vmalloc_addr(ptr),
5118+ "rejecting DMA map of vmalloc memory\n"))
5119+ return DMA_MAPPING_ERROR;
5120+ debug_dma_map_single(dev, ptr, size);
5121+ return dma_map_page_attrs(dev, virt_to_page(ptr), offset_in_page(ptr),
5122+ size, dir, attrs);
5123+ }
5124+ ```
5125+
5126+ dma_map_single() 对应的去映射函数是 dma_unmap_single()。
5127+
5128+ ```c
5129+ #define dma_unmap_single(d, a, s, r) dma_unmap_single_attrs(d, a, s, r, 0)
5130+
5131+ void dma_unmap_single_attrs(struct device *dev, dma_addr_t addr,
5132+ size_t size, enum dma_data_direction dir, unsigned long attrs)
5133+ {
5134+ return dma_unmap_page_attrs(dev, addr, size, dir, attrs);
5135+ }
5136+ ```
5137+
5138+ 通常情况下,设备驱动不应访问未映射的流式 DMA 缓冲区。如果一定要这么做,可使用如下函数获得 DMA 缓冲区的拥有权。
5139+
5140+ ```c
5141+ void dma_sync_single_for_cpu(struct device *dev, dma_addr_t addr, size_t size, enum dma_data_direction dir);
5142+ ```
5143+
5144+ 在驱动访问完 DMA 缓冲区后,使用如下函数将其所有权返还给设备。
5145+
5146+ ```c
5147+ void dma_sync_single_for_device(struct device *dev, dma_addr_t addr, size_t size, enum dma_data_direction dir);
5148+ ```
5149+
5150+ ##### SG 映射
5151+
5152+ 如果设备要求较大的 DMA 缓冲区,在其支持 SG 模式的情况下,申请多个相对较小不连续的 DMA 缓冲区通常是防止申请太大的连续物理空间的方法。使用函数 dma_map_sg() 申请,对应的释放函数是 dma_unmap_sg()。
5153+
5154+ ```c
5155+ #define dma_map_sg(d, s, n, r) dma_map_sg_attrs(d, s, n, r, 0)
5156+
5157+ // 函数返回 DMA 缓冲区的数量,可能小于 nents。
5158+ // nents:散列表(scatterlist)入口的数量。
5159+ // 对于 scatterlist 的每个项目,dma_map_sg() 为设备产生恰当的总线地址,它会合并物理上临近的内存区域。
5160+ unsigned int dma_map_sg_attrs(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction dir, unsigned long attrs)
5161+ {
5162+ int ret;
5163+
5164+ ret = __dma_map_sg_attrs(dev, sg, nents, dir, attrs);
5165+ if (ret < 0)
5166+ return 0;
5167+ return ret;
5168+ }
5169+ EXPORT_SYMBOL(dma_map_sg_attrs);
5170+
5171+ #define dma_unmap_sg(d, s, n, r) dma_unmap_sg_attrs(d, s, n, r, 0)
5172+
5173+ void dma_unmap_sg_attrs(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction dir, unsigned long attrs)
5174+ {
5175+ const struct dma_map_ops *ops = get_dma_ops(dev);
5176+
5177+ BUG_ON(!valid_dma_direction(dir));
5178+ debug_dma_unmap_sg(dev, sg, nents, dir);
5179+ if (dma_map_direct(dev, ops) ||
5180+ arch_dma_unmap_sg_direct(dev, sg, nents))
5181+ dma_direct_unmap_sg(dev, sg, nents, dir, attrs);
5182+ else if (ops->unmap_sg)
5183+ ops->unmap_sg(dev, sg, nents, dir, attrs);
5184+ }
5185+ EXPORT_SYMBOL(dma_unmap_sg_attrs);
5186+ ```
5187+
5188+ scatterlist 结构体定义如下,包含与 scatterlist 对应的页结构体指针、缓冲区在页中的偏移 offset、缓冲区长度 length 以及总线地址 dma_address。
5189+
5190+ ```c
5191+ struct scatterlist {
5192+ unsigned long page_link;
5193+ unsigned int offset;
5194+ unsigned int length;
5195+ dma_addr_t dma_address;
5196+ #ifdef CONFIG_NEED_SG_DMA_LENGTH
5197+ unsigned int dma_length;
5198+ #endif
5199+ };
5200+ ```
5201+
5202+ 执行 dma_map_sg() 后,通过 sg_dma_address() 返回 scatterlist 对应缓冲区的总线地址,sg_dma_len() 返回 scatterlist 对应缓冲区的长度。
5203+
5204+ ```c
5205+ #define sg_dma_address(sg) ((sg)->dma_address)
5206+
5207+ #define sg_dma_len(sg) ((sg)->dma_length)
5208+ ```
5209+
5210+ 同单一缓冲区的情况,如果设备驱动一定要访问未映射的 SG 缓冲区,应先调用函数 dma_sync_sg_for_cpu(),归还所有权的函数是 dma_sync_sg_for_device()。
5211+
5212+ ```c
5213+ void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg, int nelems, enum dma_data_direction dir);
5214+
5215+ void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg, int nelems, enum dma_data_direction dir);
5216+ ```
5217+
5218+ #### dmaengine 标准 API
5219+
5220+ 推荐使用 dmaengine 的驱动架构来编写 DMA 控制器的驱动,外设的驱动使用标准的 dmaengine API 进行 DMA 的准备、发起和完成时的回调工作。
5221+
5222+ 和中断一样,在使用 DMA 前,设备驱动程序需首先向 dmaengine 系统申请 DMA 通道,申请 DMA 通道的函数如下:
5223+
5224+ ```c
5225+ /* Deprecated, please use dma_request_chan() directly */
5226+ struct dma_chan * __deprecated dma_request_slave_channel(struct device *dev, const char *name)
5227+ {
5228+ struct dma_chan *ch = dma_request_chan(dev, name);
5229+
5230+ return IS_ERR(ch) ? NULL : ch;
5231+ }
5232+
5233+ struct dma_chan *dma_request_chan(struct device *dev, const char *name);
5234+ ```
5235+
5236+ 对应的释放通道的函数是 dma_release_channel()。
5237+
5238+ ```c
5239+ void dma_release_channel(struct dma_chan *chan);
5240+ ```
5241+
5242+ 下面是利用 dmaengine API 发起一次 DMA 操作的示例:
5243+
5244+ ```c
5245+ static void xxx_dma_fini_callback(void *data)
5246+ {
5247+ struct completion *dma_complete = data;
5248+
5249+ complete(dma_complete);
5250+ }
5251+
5252+ issue_xxx_dma(...)
5253+ {
5254+ // 通过 dmaengine_prep_slave_single() 准备好一些 DMA 描述符。
5255+ rx_desc = dmaengine_prep_slave_single(xxx->rx_chan, xxx->dst_start, t->len, DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
5256+
5257+ // 填充完成回调为 xxx_dma_fini_callback()。
5258+ rx_desc->callback = xxx_dma_fini_callback;
5259+ rx_desc->callback_param = &xxx->rx_done;
5260+
5261+ // 通过 dmaengine_submit() 把这个描述符插入队列。
5262+ dmaengine_submit(rx_desc);
5263+
5264+ // 通过 dma_async_issue_pending() 发起这次 DMA 动作。完成后 xxx_dma_fini_callback() 函数会被 dmaengine 驱动自动调用。
5265+ dma_async_issue_pending(xxx->rx_chan);
5266+ }
5267+ ```
5268+
5269+ ## 小结
5270+
5271+ 外设可处于 CPU 的内存空间和 I/O 空间。除 x86 外,嵌入式处理器一般只存在内存空间。Linux 为 I/O 内存和 I/O 端口的访问提高了一套统一的方法,访问流程一般为**申请资源->映射->访问->去映射->释放资源**。
5272+
5273+ 对于有 MMU 的处理器而言,Linux 的内部布局比较复杂,可直接映射的物理内存称为常规内存,超出部分为高端内存。kmalloc() 和 `__get_free_pages()` 申请的内存在物理上连续,vmalloc() 申请的内存在物理上不连续。
5274+
5275+ DMA 操作可能导致 Cache 不一致性的问题,故对于 DMA 缓冲,应使用 dma_alloc_coherent() 等方法申请。在 DMA 操作中涉及总线地址、物理地址和虚拟地址等概念,区分这 3 类地址非常重要。
5276+
0 commit comments