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

ARM64平台PCIe驱动mmap用户态数据访问延迟问题排查

PCIe驱动FPGA与ARM64数据传输延迟问题排查

背景

正在开发一款用于FPGA与ARM64架构CPU间数据传输的PCIe驱动,采用内存映射(mmap)的void指针数组作为接收缓冲区。FPGA发送的数据包写入缓冲区后,通知用户态读取数据。

用户态mmap代码

do
{
    m_bufs[m_numBufs] = mmap(0, PKT_MAX_SIZE,
                                 PROT_READ | PROT_WRITE, MAP_SHARED_VALIDATE | MAP_LOCKED,
                                 fd, 0);

    // if we got an error back from the driver, it has no more buffers to map
    if (m_bufs[m_numBufs] == (void *)-1)
    {
        printf("mmap errno: %d\n", errno);
        perror("mmap");
        done = true;
    }
    else
    {
        m_numBufs++;
    }
// keep going until the driver runs out of buffers, or we run out of pointers for them
} while (!done && (m_numBufs < NUM_PKT_BUFS));

内核态映射代码

vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);

ret = remap_pfn_range(vma, vma->vm_start,
                      virt_to_phys(lowspeed_dat.rx_pkt_queue.buffers[lowspeed_dat.rx_pkt_queue.mapping_idx]) >> PAGE_SHIFT,
                      vma->vm_end - vma->vm_start, vma->vm_page_prot);
if (ret)
{
    printk("error - remapping pages failed\n");
    ret = -EAGAIN;
}
else
{
    printk("(%d, %d) CHAN A start => 0x%08lx, end => 0x%08lx\n",
           queue_dat.mapping_mode, lowspeed_dat.rx_pkt_queue.mapping_idx, vma->vm_start, vma->vm_end);
   lowspeed_dat.rx_pkt_queue.mapping_idx++;
}

当前问题

用户态收到数据就绪的阻塞通知后,读取缓冲区时约90%的缓冲区为空,无规律;延迟1-2秒后重读,所有数据均存在。添加1.5ms延迟后,除首个数据包外其余均可正常读取,重读首个包也能得到正确数据。测试数据为带头部的计数器,无乱序。

已尝试的解决方法

  • 调用msync(调整页对齐后无报错,但无效果):
void *aligned_addr = (void *)((uintptr_t)m_bufs[rd_idx] & ~(page_size - 1));
   
   
if(msync(aligned_addr, PKT_MAX_SIZE, MS_SYNC) == -1)
{
    printf("errno: %d", errno);
    perror("msync");
}
  • 尝试添加MAP_SYNC标志到mmap调用,因文件系统未启用DAX失败。

此前采用DMA相同映射与阻塞调用方式无此延迟问题,请问该延迟是否正常?是否有遗漏的关键处理步骤?

补充代码

缓冲区分配

for (i = 0; i < PCI_PKT_BUFS; i++)
{
    lowspeed_dat.rx_pkt_queue.buffers[i] = kzalloc(PCI_PKT_MAX_SIZE, GFP_KERNEL);
}

用户态读取逻辑

while (running)
{
    if (false == gotIndex)
    {
        if (ioctl(fd, PCI_GET_RD_IDX, &index) != -1)
        {
            printf("Got Chan index: %d\n", index);
            gotIndex = true;
        } else{
            printf("Failed to get index\n");
        }
    }

    if (gotIndex == true)
    {
        if (ioctl(fd, PCI_READ_CHAN_A, &val) != -1)
        {
            // printf("Read IOCTL succesful val = %d\n", val);
            gotIndex = true;
        }


        if (val > 0)
        {
            gotIndex = false;
            // usleep(1500);
            writeLowSpeedDataToFile(index, file);
        } else 
        {
            printf("Read Chan A failed\n");
            running = false;
        }
    }
}
}

内核态ioctl实现

long readChanA(unsigned long arg, void *dev_id)
{
    u32 size;
    int ret = -1;   // return value
    int result = 0; // result of the wait
    unsigned int old_idx = 0;                        // current slot in the rx queue
    unsigned int new_idx = 0;                        // next slot in the tx queue
    pci_ls_pkt_queue *queue = &lowspeed_dat.rx_pkt_queue[CHANNEL_A];

    printk("Chan A: wr_idx = %d rd_idx = %d\n", queue->wr_idx, queue->rd_idx);
    
    old_idx = queue->rd_idx;
    new_idx = (queue->rd_idx + 1) % PCI_PKT_BUFS;
    printk("1\n");


    // wait until a packet is available or we timeout while waiting
    result = wait_event_interruptible_timeout(chan_a_wait_queue,
                                            ((queue->wr_idx + 1) % PCI_PKT_BUFS) != new_idx, HZ);

    printk("4\n");
    
    // if a packet is available, return a passing value
    if (result > 0)
    {
        ret = 0;
        printk("Chan A: wr_idx = %d new rd_idx = %d\n", queue->wr_idx, new_idx);
        queue->rd_idx = new_idx;
        printk("5\n");
        return result;
    }
    else
    {
        printk("Timed out \n");
        return -1;
    }
    return ret;
}

FPGA写入缓冲区示例

uint32_t *buf = (uint32_t*)queue->buffers[queue->wr_idx];

*buf = readReg(queue->fifo);
buf++;

//once buffer has full packet
queue->wr_idx = (queue->wr_idx + 1) % PCI_PKT_BUFS;
buf = (uint32_t*)queue->buffers[queue->wr_idx];

wake_up_interruptible(&chan_a_wait_queue);

问题分析与解决

这种延迟绝对不正常,核心问题出在内存一致性、同步机制和队列逻辑上,结合ARM64弱内存序架构特性,遗漏了以下关键处理步骤:

1. 内存屏障缺失

ARM64是弱内存序架构,FPGA写入缓冲区后,CPU可能无法立即看到最新数据,需在关键节点插入内存屏障:

  • FPGA端:写入完缓冲区并更新wr_idx后,添加dmb sy(全系统数据内存屏障)指令,确保所有写操作完成后再唤醒CPU等待队列。
  • 内核态:在wake_up_interruptible前加入smp_rmb()读屏障,保证CPU能看到FPGA写入的最新数据。
  • 用户态:读取缓冲区前加入__sync_synchronize(),强制同步缓存数据。

2. 缓冲区缓存一致性问题

当前用kzalloc分配内存,搭配pgprot_noncached的方式存在缺陷:

  • 替换kzalloc为dma_alloc_coherent,该API会直接分配DMA兼容、非缓存的连续物理内存,自动处理缓存一致性(这也是此前DMA方式正常的核心原因):
for (i = 0; i < PCI_PKT_BUFS; i++)
{
    lowspeed_dat.rx_pkt_queue.buffers[i] = dma_alloc_coherent(dev, PCI_PKT_MAX_SIZE, &lowspeed_dat.rx_pkt_queue.dma_addrs[i], GFP_KERNEL);
}
  • 内核态映射时使用DMA物理地址而非virt_to_phys:
ret = remap_pfn_range(vma, vma->vm_start,
                      lowspeed_dat.rx_pkt_queue.dma_addrs[lowspeed_dat.rx_pkt_queue.mapping_idx] >> PAGE_SHIFT,
                      vma->vm_end - vma->vm_start, vma->vm_page_prot);

3. 队列等待条件逻辑错误

当前readChanA中的等待条件逻辑混乱,导致用户态被提前唤醒:

// 错误条件
((queue->wr_idx + 1) % PCI_PKT_BUFS) != new_idx

修正为环形队列标准的非空判断:

result = wait_event_interruptible_timeout(chan_a_wait_queue,
                                        queue->wr_idx != queue->rd_idx, HZ);

同时,更新wr_idx和rd_idx时需加入内存屏障,确保索引更新对CPU可见。

4. 用户态读取时机同步

用户态收到PCI_READ_CHAN_A成功通知后,需确保缓冲区数据已同步,可在调用writeLowSpeedDataToFile前加入内存屏障,避免读取到旧缓存数据。


内容的提问来源于stack exchange,提问作者Justin McKenzie

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 17:52:33