You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 03:29:55