Linux内核中PCIe总线主DMA散列聚集列表的内存分配问题
Linux PCIe总线主DMA驱动:散列-聚集列表内存分配方案
需求与设备限制
需为一款支持总线主DMA(scatter-gather)的PCIe设备开发Linux驱动,该驱动已在Windows平台实现。设备总线主引擎存在以下限制:
- 仅支持32位地址,无法寻址4GB以上内存;
- scatter-gather列表(PRDT)必须按64KB边界对齐;
- scatter-gather列表在物理内存中必须连续。
当前瓶颈
在Linux内核中分配符合要求的PRDT内存时遇到问题:已知kmalloc可分配低896MB的连续内存(满足4GB地址限制),但不清楚如何实现64KB边界对齐,也不确定是否需要将内存标记为非缓存/非分页。
Windows驱动参考实现
以下是Windows平台中实现连续、对齐、非缓存PRDT分配的代码片段(已翻译注释):
// 散列-聚集条目,称为PRD(物理区域描述符) typedef struct _PRD { ULONG BaseAddress; // 缓冲区物理地址 USHORT ByteCount; // 缓冲区长度 USHORT EoT; // 表结束标记 } PRD, *PPRD; typedef struct _DEVICE_EXTENSION { ... PPRD PRDT; // 指向物理区域描述符表的指针 ... } DEVICE_EXTENSION, *PDEVICE_EXTENSION; NTSTATUS StartDevice(_In_ PIRP Irp, _In_ PIO_STACK_LOCATION Stack, _In_ PDEVICE_EXTENSION DeviceExtension) { ... PHYSICAL_ADDRESS LowestAcceptableAddress = { 0x0,0x0 }; PHYSICAL_ADDRESS HighestAcceptableAddress = { 0xFFFFFFFF, 0x0 }; // 4GB内存上限 PHYSICAL_ADDRESS BoundaryAddressMultiple = { 0x00010000, 0x0 }; // 64KB边界 ULONG Length = 4096 * sizeof(PRD); // 4096个PRD条目 // PRDT内存分配:1) 连续 2) 边界对齐 3) 非缓存 DeviceExtension->PRDT = MmAllocateContiguousMemorySpecifyCache(Length, LowestAcceptableAddress, HighestAcceptableAddress, BoundaryAddressMultiple, MmNonCached); // 将PRDT物理地址写入总线主控制端口 PHYSICAL_ADDRESS a = MmGetPhysicalAddress(DeviceExtension->PRDT); for (UCHAR j = 0; j < 4; ++j) WRITE_PORT_UCHAR(BusMasterPrdtOffsetReg + j, (a.LowPart >> (j * 8)) & 0xFF); ... }
Linux平台解决方案
1. 64KB对齐的连续内存分配
Linux内核中推荐使用__get_free_pages或alloc_pages分配连续物理内存,结合GFP_DMA32标志确保地址落在32位范围内(0~4GB):
- 计算页阶数:64KB对齐要求起始地址为2^16字节对齐,若系统页大小为4KB(
PAGE_SHIFT=12),需分配order=4的页(16个页,总计64KB);若PRDT总大小超过64KB,需提升order满足连续分配需求。 - 手动对齐:分配后用
ALIGN宏将虚拟地址对齐到64KB边界,对应的物理地址会自动对齐(因分配的页本身连续)。
2. 非缓存与非分页设置
- 非分页:
__get_free_pages分配的内存属于内核直接映射区,本身就是非分页的,无需额外处理。 - 非缓存:由于设备是总线主DMA,直接访问物理内存,需避免缓存一致性问题,可使用
set_memory_nocache将PRDT内存标记为非缓存,确保设备读取最新数据。
3. 代码实现示例
#include <linux/mm.h> #include <linux/io.h> #include <linux/pci.h> #include <linux/byteorder/generic.h> // 散列-聚集条目,称为PRD(物理区域描述符) struct prd { __le32 base_address; // 缓冲区物理地址(小端格式) __le16 byte_count; // 缓冲区长度(小端格式) __le16 eot; // 表结束标记(小端格式) }; struct device_extension { struct pci_dev *pdev; struct prd *prdt; // PRDT虚拟地址 dma_addr_t prdt_phys; // PRDT物理地址 size_t prdt_size; // PRDT总大小 unsigned long prdt_page;// 原始分配的页起始地址 unsigned int prdt_order;// 分配的页阶数 // 其他设备相关成员... }; // 分配PRDT内存 int prdt_alloc(struct device_extension *dev_ext) { dev_ext->prdt_size = 4096 * sizeof(struct prd); // 计算满足大小需求的最小页阶数 dev_ext->prdt_order = get_order(dev_ext->prdt_size); // 确保分配的内存支持64KB对齐 while ((PAGE_SIZE << dev_ext->prdt_order) < (64 * 1024)) { dev_ext->prdt_order++; } // 分配连续的32位可访问内存 dev_ext->prdt_page = __get_free_pages(GFP_DMA32 | GFP_KERNEL, dev_ext->prdt_order); if (!dev_ext->prdt_page) { return -ENOMEM; } // 将虚拟地址对齐到64KB边界 dev_ext->prdt = (struct prd *)ALIGN((void *)dev_ext->prdt_page, 64 * 1024); // 转换为物理地址 dev_ext->prdt_phys = virt_to_phys(dev_ext->prdt); // 验证物理地址是否在32位范围内 if (dev_ext->prdt_phys > 0xFFFFFFFF) { free_pages(dev_ext->prdt_page, dev_ext->prdt_order); return -EINVAL; } // 设置内存为非缓存 unsigned long num_pages = (dev_ext->prdt_size + PAGE_SIZE - 1) / PAGE_SIZE; if (set_memory_nocache((unsigned long)dev_ext->prdt, num_pages)) { free_pages(dev_ext->prdt_page, dev_ext->prdt_order); return -EFAULT; } // 将PRDT物理地址写入设备寄存器 void __iomem *prdt_reg = dev_ext->pdev->resource[0].start + BusMasterPrdtOffsetReg; for (int j = 0; j < 4; j++) { writeb((dev_ext->prdt_phys >> (j * 8)) & 0xFF, prdt_reg + j); } return 0; } // 释放PRDT内存 void prdt_free(struct device_extension *dev_ext) { if (dev_ext->prdt_page) { // 恢复缓存属性(可选,根据内核版本要求) unsigned long num_pages = (dev_ext->prdt_size + PAGE_SIZE - 1) / PAGE_SIZE; set_memory_wb((unsigned long)dev_ext->prdt, num_pages); // 释放原始分配的页 free_pages(dev_ext->prdt_page, dev_ext->prdt_order); dev_ext->prdt_page = 0; dev_ext->prdt = NULL; } }
注意事项
GFP_DMA32是关键标志,确保分配的物理地址在0~4GB范围内,符合设备的32位地址限制;- 释放内存时必须使用原始分配的
prdt_page指针,而非对齐后的prdt指针; - 若使用
dma_alloc_coherent分配一致内存,需手动检查物理地址是否64KB对齐,未对齐则需调整分配大小并重新计算对齐地址,灵活性较低; - 小端格式:需将PRD结构成员声明为小端类型(
__le32/__le16),确保与设备字节序兼容。
内容的提问来源于stack exchange,提问作者KzQHLnsb
相关产品推荐
相关产品推荐

