变长数组在Linux USB当中的应用

本文深入探讨了Linux-USB代码中如何使用变长数据,通过分析`usb_alloc_urb`函数,解释了kmalloc分配内存的过程,并详细介绍了urb结构体的末尾零数组如何支持非等长数据存储。

        偶然浏览Linux-USB的代码,发现了变长数据的妙用,总结一下,学起来。
所示的函数中

struct urb *usb_alloc_urb(int iso_packets, gfp_t mem_flags)
{
	struct urb *urb;

	urb = kmalloc(sizeof(struct urb) +
		iso_packets * sizeof(struct usb_iso_packet_descriptor),
		mem_flags);
	if (!urb) {
		printk(KERN_ERR "alloc_urb: kmalloc failed\n");
		return NULL;
	}
	usb_init_urb(urb);
	return urb;
}

其中,kmalloc分配新的urb时,多分配了一下内存,用于存储数据。

iso_packets * sizeof(struct usb_iso_packet_descriptor)

再来看urb结构体定义,结构体的末尾有个零数组,可以再分配urb的同时,在urb后的连续内存上,非配不等长的内存用来存储数据。

struct urb {
	/* private: usb core and host controller only fields in the urb */
	struct kref kref;		/* reference count of the URB */
	void *hcpriv;			/* private data for host controller */
	atomic_t use_count;		/* concurrent submissions counter */
	atomic_t reject;		/* submissions will fail */
	int unlinked;			/* unlink error code */

	/* public: documented fields in the urb that can be used by drivers */
	struct list_head urb_list;	/* list head for use by the urb's
					 * current owner */
	struct list_head anchor_list;	/* the URB may be anchored */
	struct usb_anchor *anchor;
	struct usb_device *dev;		/* (in) pointer to associated device */
	struct usb_host_endpoint *ep;	/* (internal) pointer to endpoint */
	unsigned int pipe;		/* (in) pipe information */
	unsigned int stream_id;		/* (in) stream ID */
	int status;			/* (return) non-ISO status */
	unsigned int transfer_flags;	/* (in) URB_SHORT_NOT_OK | ...*/
	void *transfer_buffer;		/* (in) associated data buffer */
	dma_addr_t transfer_dma;	/* (in) dma addr for transfer_buffer */
	struct scatterlist *sg;		/* (in) scatter gather buffer list */
	int num_mapped_sgs;		/* (internal) mapped sg entries */
	int num_sgs;			/* (in) number of entries in the sg list */
	u32 transfer_buffer_length;	/* (in) data buffer length */
	u32 actual_length;		/* (return) actual transfer length */
	unsigned char *setup_packet;	/* (in) setup packet (control only) */
	dma_addr_t setup_dma;		/* (in) dma addr for setup_packet */
	int start_frame;		/* (modify) start frame (ISO) */
	int number_of_packets;		/* (in) number of ISO packets */
	int interval;			/* (modify) transfer interval
					 * (INT/ISO) */
	int error_count;		/* (return) number of ISO errors */
	void *context;			/* (in) context for completion */
	usb_complete_t complete;	/* (in) completion routine */
	struct usb_iso_packet_descriptor iso_frame_desc[0];
					/* (in) ISO ONLY */
};



<think> 我们正在讨论Linux驱动中DMA缓冲区的正确分配方法。根据引用内容,DMA缓冲区分配涉及几个关键点:DMA映射、缓存一致性和分配函数的使用。下面将结合引用内容详细说明。 引用[1]提到:DMA映射是在设备与主存之间建立DMA数据传输通道时,在主存中为DMA通道分配内存空间,这个内存空间就是DMA缓冲区。由于现代处理器cache的存在,事情变得复杂(需要维护缓存一致性)。 引用[2]指出:使用DMA时,高速缓存与内存内容的一致性问题是一个主要问题,通常通过硬件机制维护一致性。Linux内核为ARM处理器提供了两种DMA映射操作数据结构。 引用[3]和[4]则提到了具体的分配函数和注意事项:如`dma_alloc_coherent`函数,并强调设备参数不能为NULL,否则会导致分配失败。 下面将详细说明Linux驱动中分配DMA缓冲区的正确方法: ## 一、DMA缓冲区的分配方式 ### 1. 一致性DMA缓冲区(Coherent DMA buffers) - **特点**:在驱动生命周期内长期存在,硬件和CPU访问时自动维护缓存一致性(通过非缓存或写回策略) - **适用场景**:频繁交换的小数据(如设备控制结构) #### 分配函数: ```c void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag); ``` - `dev`:设备结构体指针(**必须有效**,引用[4]强调不能为NULL) - `size`:请求的缓冲区大小(以字节为单位) - `dma_handle`:返回的DMA总线地址(物理地址的映射) - `flag`:内存分配标志(如`GFP_KERNEL`、`GFP_ATOMIC`) #### 释放函数: ```c void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t dma_handle); ``` #### 示例代码: ```c struct device *dev = &pdev->dev; // 假设从平台设备获取 dma_addr_t dma_addr; void *vaddr; vaddr = dma_alloc_coherent(dev, 4096, &dma_addr, GFP_KERNEL); if (!vaddr) { // 错误处理 } // 使用vaddr和dma_addr... dma_free_coherent(dev, 4096, vaddr, dma_addr); ``` > **关键点**:`dev`参数必须指向一个有效的设备结构,否则会导致分配失败(引用[4]) ### 2. 流式DMA缓冲区(Streaming DMA buffers) - **特点**:用于单次传输,需要手动维护缓存一致性(调用同步函数) - **适用场景**:大数据传输(如网络数据包、块设备I/O) #### 映射函数: ```c dma_addr_t dma_map_single(struct device *dev, void *ptr, size_t size, enum dma_data_direction dir); ``` - `ptr`:内核虚拟地址(由`kmalloc`等分配) - `dir`:数据传输方向(`DMA_TO_DEVICE`、`DMA_FROM_DEVICE`、`DMA_BIDIRECTIONAL`) #### 同步函数(缓存一致性维护): ```c void dma_sync_single_for_cpu(struct device *dev, dma_addr_t dma_handle, size_t size, enum dma_data_direction dir); void dma_sync_single_for_device(struct device *dev, dma_addr_t dma_handle, size_t size, enum dma_data_direction dir); ``` #### 示例代码: ```c void *buf = kmalloc(BUF_SIZE, GFP_KERNEL); dma_addr_t dma_addr; dma_addr = dma_map_single(dev, buf, BUF_SIZE, DMA_TO_DEVICE); // 启动DMA传输... // DMA传输完成后,从设备回读数据前需同步CPU缓存 dma_sync_single_for_cpu(dev, dma_addr, BUF_SIZE, DMA_FROM_DEVICE); // 处理数据后若需再次传输 dma_sync_single_for_device(dev, dma_addr, BUF_SIZE, DMA_TO_DEVICE); // 最后取消映射 dma_unmap_single(dev, dma_addr, BUF_SIZE, DMA_TO_DEVICE); kfree(buf); ``` ## 二、缓存一致性处理机制 ### 硬件自动维护(一致性DMA) - 通过非缓存(Uncached)或写合并(Write-combining)内存实现 - CPU和DMA控制器访问同一物理地址时数据自动同步 ### 软件手动维护(流式DMA) 1. **映射时**:`dma_map_single`会刷CPU缓存(确保设备看到最新数据) 2. **传输中**:设备直接访问内存 3. **传输后**: - 设备→内存:需`dma_sync_single_for_cpu`使CPU缓存失效 - 内存→设备:需`dma_sync_single_for_device`刷CPU缓存 ## 三、分散/聚集(Scatter/Gather)映射 当需要处理大容量或不连续数据时(引用[3]): ```c int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction dir); ``` - `sg`:预先初始化的scatterlist数组 - `nents`:sg数组项数 **使用流程**: 1. 分配多个内存块(如`kmalloc`) 2. 初始化`scatterlist`数组 3. 调用`dma_map_sg`映射所有缓冲区 4. 将生成的DMA地址数组传递给设备 ## 四、常见错误与规避方法 | 错误类型 | 解决方案 | |-------------------------|------------------------------| | 设备参数`dev`为NULL | 确保传入有效设备结构体指针[^4] | | 未对齐的缓冲区地址 | 使用`dma_get_cache_alignment`获取对齐要求 | | 未维护缓存一致性 | 流式DMA必须调用sync函数 | | 分配过大连续内存失败 | 改用SG映射(分散缓冲区)[^3] | ## 五、最佳实践总结 1. **设备参数验证**:确保`dma_alloc_coherent`的`dev`参数有效 2. **缓冲区大小限制**:避免单次分配过大内存(>4MB建议用SG) 3. **方向标识准确**:正确设置`DMA_TO_DEVICE`/`FROM_DEVICE` 4. **生命周期管理**: ```mermaid graph LR A[分配/映射] --> B[传输前同步] B --> C[设备DMA操作] C --> D[传输后同步] D --> E[释放/解映射] ``` > **性能提示**:频繁传输的小数据用一致性映射,大数据传输用流式映射[^2][^3] **总结**:Linux驱动中正确分配DMA缓冲区的关键在于: 1. 根据传输特性选择一致性或流式映射 2. 严格遵循API规范(特别是设备指针有效性) 3. 精准维护缓存一致性 4. 大内存分配优先使用分散/聚集模式 §§相关问题§§ 1. 如何诊断DMA映射导致的缓存一致性问题? 2. 在ARM架构中DMA映射有哪些特殊注意事项? 3. 分散/聚集映射(Scatter/Gather)的具体实现步骤是什么? </think>### Linux驱动中正确分配DMA缓冲区的方法 #### 一、核心原则与挑战 1. **缓存一致性问题** - 现代处理器存在缓存(Cache),DMA设备直接访问内存时可能导致缓存与内存数据不一致[^1][^2] - 解决方案:通过硬件机制(如Cache Coherent Interconnect)或软件同步维护一致性 2. **两种DMA映射类型** | 类型 | 特点 | 适用场景 | |--------------------|-------------------------------|------------------------| | **一致性映射** | 硬件自动维护缓存一致性 | 长期存在的小数据缓冲区 | | **流式映射** | 需手动同步缓存 | 单次大数据传输 | #### 二、分配方法详解 ##### 1. **一致性DMA缓冲区(Coherent DMA)** ```c // 分配函数 void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag); // 释放函数 void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t dma_handle); ``` - **参数说明**: - `dev`: **必须有效**的设备结构体指针(`NULL`会导致失败)[^4] - `size`: 缓冲区大小(字节) - `dma_handle`: 返回的DMA总线地址 - `flag`: 内存分配标志(如`GFP_KERNEL`) - **使用示例**: ```c struct device *dev = &pdev->dev; // 从平台设备获取 dma_addr_t dma_addr; void *vaddr = dma_alloc_coherent(dev, 4096, &dma_addr, GFP_KERNEL); if (!vaddr) { dev_err(dev, "DMA分配失败"); return -ENOMEM; } // 使用vaddr(CPU虚拟地址)和dma_addr(设备物理地址) ``` - **特点**: - 自动处理缓存一致性 - 分配的内存可能是非缓存的 - 适合存放设备控制结构等长期数据 ##### 2. **流式DMA缓冲区(Streaming DMA)** ```c // 映射函数 dma_addr_t dma_map_single(struct device *dev, void *ptr, size_t size, enum dma_data_direction dir); // 同步函数(缓存维护) void dma_sync_single_for_cpu(struct device *dev, dma_addr_t addr, size_t size, enum dma_data_direction dir); void dma_sync_single_for_device(...); // 解映射 void dma_unmap_single(...); ``` - **使用流程**: 1. 用`kmalloc()`或类似函数分配普通内存 2. 调用`dma_map_single()`获取DMA地址 3. 启动DMA传输 4. 传输完成后根据方向调用同步函数: - 设备→内存: `dma_sync_single_for_cpu()` - 内存→设备: `dma_sync_single_for_device()` 5. 调用`dma_unmap_single()`解映射 - **方向枚举**: ```c enum dma_data_direction { DMA_BIDIRECTIONAL = 0, // 双向 DMA_TO_DEVICE = 1, // 内存→设备 DMA_FROM_DEVICE = 2, // 设备→内存 DMA_NONE = 3, }; ``` #### 三、分散/聚集映射(Scatter/Gather) **适用场景**:大内存或不连续缓冲区(引用[3]) ```c // 映射函数 int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction dir); // 获取DMA地址和长度 dma_addr_t sg_dma_address(struct scatterlist *sg); unsigned int sg_dma_len(struct scatterlist *sg); ``` - **使用步骤**: 1. 准备`scatterlist`数组 2. 调用`dma_map_sg()`映射 3. 遍历scatterlist获取每个段的DMA地址 4. 传输后调用`dma_unmap_sg()`解映射 #### 四、关键注意事项 1. **设备指针有效性** - `dma_alloc_coherent()`的`dev`参数**不能为NULL**(否则返回未初始化指针)[^4] - 正确获取设备指针:`&pdev->dev`(平台设备)或`&udev->dev`(USB设备) 2. **内存对齐要求** - 使用`dma_get_cache_alignment()`获取缓存对齐值 - 示例:分配对齐内存 ```c size_t align = dma_get_cache_alignment(); void *buf = kmalloc(size + align, GFP_KERNEL); void *aligned_buf = PTR_ALIGN(buf, align); ``` 3. **大内存分配策略** - 大于4MB的分配优先使用`dma_alloc_noncoherent()`+手动同步 - 或采用分散/聚集映射(SG模式)避免连续物理内存不足[^3] 4. **ARM架构特殊处理** - 可能需要配置`dma_map_ops`操作集 - 对无IOMMU的设备需使用`__dma_alloc()`等底层函数 #### 五、完整流程示例(网络驱动) ```c // 1. 分配接收缓冲区 skb = netdev_alloc_skb(dev, len); if (!skb) return -ENOMEM; // 2. 映射DMA缓冲区 dma_addr = dma_map_single(dev->dev, skb->data, len, DMA_FROM_DEVICE); // 3. 配置网卡DMA描述符 desc->addr = dma_addr; desc->len = len; // 4. 启动DMA writel(DMA_START, reg_base + CONTROL_REG); // 5. 中断处理中同步缓存 dma_sync_single_for_cpu(dev->dev, dma_addr, len, DMA_FROM_DEVICE); // 6. 处理数据后解映射 dma_unmap_single(dev->dev, dma_addr, len, DMA_FROM_DEVICE); ``` #### 六、错误排查指南 | 现象 | 可能原因 | 解决方案 | |-----------------------|-------------------------|------------------------------| | 分配返回NULL | 连续物理内存不足 | 改用SG映射或减小缓冲区大小 | | 数据传输损坏 | 缓存未同步 | 检查`dma_sync_*`调用位置 | | 硬件报告DMA错误 | 地址未对齐 | 确保缓冲区按缓存行对齐 | | 仅首次传输成功 | 未在传输后解映射 | 确认每次映射后都有解映射操作 | > **最佳实践**:一致性映射用于控制结构,流式映射用于数据缓冲区,大内存分配优先使用SG模式[^1][^3]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值