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
相关产品推荐
相关产品推荐

