【问题标题】:Writing out DMA buffers into memory mapped file将 DMA 缓冲区写入内存映射文件
【发布时间】:2025-12-25 14:20:11
【问题描述】:

我需要在嵌入式 Linux(2.6.37) 中尽可能快地将传入的 DMA 缓冲区写入 HD 分区作为原始设备 /dev/sda1。缓冲区按要求对齐,长度相等,为 512KB。该过程可能会持续很长时间并填充多达 256GB 的数据。 我需要使用内存映射文件技术(O_DIRECT 不适用),但无法理解如何执行此操作的确切方法。 所以,用伪代码“正常”写法:

fd=open(/dev/sda1",O_WRONLY);
while(1) {
    p = GetVirtualPointerToNewBuffer();
    if (InputStopped())
        break;
    write(fd, p, BLOCK512KB);
}

现在,我将非常感谢类似的伪/真实代码示例,说明如何在本文中使用内存映射技术。

更新2: 感谢 kestasx,最新的工作测试代码如下所示:

#define TSIZE   (64*KB)
void* TBuf;
int main(int argc, char **argv) {
    int fdi=open("input.dat", O_RDONLY);
    //int fdo=open("/dev/sdb2", O_RDWR);
    int fdo=open("output.dat", O_RDWR);
    int i, offs=0;
    void* addr;
        i = posix_memalign(&TBuf, TSIZE, TSIZE);
        if ((fdo < 1) || (fdi < 1)) {
            printf("Error in files\n");
            return -1; }
        while(1) {
            addr = mmap((void*)TBuf, TSIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fdo, offs);
            if ((unsigned int)addr == 0xFFFFFFFFUL) {
                printf("Error MMAP=%d, %s\n", errno, strerror(errno));
                return -1; }
            i = read(fdi, TBuf, TSIZE);
            if (i != TSIZE) {
                printf("End of data\n");
                return 0; }
            i = munmap(addr, TSIZE);
            offs += TSIZE;
            sleep(1);
        };
}

更新3: 1. 为了精确模拟 DMA 工作,我需要将 read() 调用移到 mmp() 之前,因为当 DMA 完成时,它会为我提供它放置数据的地址。所以,在伪代码中: while(1) { read(fdi, TBuf, TSIZE); addr = mmap((void*)TBuf, TSIZE, PROT_READ|PROT_WRITE, MAP_FIXED|MAP_SHARED, fdo, offs); munmap(addr, TSIZE); offs += TSIZE; }

此变体在(!)第一个循环后失败 - read() 在 TBuf 上显示 BAD ADDRESS。 在不完全理解我所做的事情的情况下,我将 munmap() 替换为 msync()。这非常有效。
那么,这里的问题——为什么取消映射地址会影响 TBuf?

2.通过前面的示例,我使用 DMA 进入了真实系统。与 read() 调用不同的是,相同的循环是等待 DMA 缓冲区准备好并提供其虚拟地址的调用。
没有错误,代码运行,但没有记录(!)。 我的想法是 Linux 没有看到该区域已更新,因此没有同步()一件事。
为了测试这一点,我在工作示例中消除了 read() 调用 - 是的,也没有记录任何内容。

所以,这里的问题 - 我如何告诉 Linux 映射区域包含新数据,请刷新它!

非常感谢!!!

【问题讨论】:

  • 在更新的程序中我认为你不需要posix_memalign(不需要分配内存)。调用mmap() 后是否检查过addr 是否与TBuf 相同?我已将fdo 更改为打开output.dat(预分配的64KB 零填充文件)并在mmap() 之后分配TBuf=addr。有了这些修改程序对我来说工作正常。
  • @kestasx 是的,这个变量有效——这里的关键是 TBuf = addr;但这正是我做不到的,因为 TBuf 必须驻留在 DMA 传输的特殊内存中。在这个测试示例中它只被 read() 模仿...当我将 MAP_FIXED 添加到 mmap() 调用时,read(0 失败...
  • @kestasx 可能是我写的荒谬吗? TBuf 和 addr 都应该指向同一个物理内存,不是吗?
  • 是的,在最终设置中,addr 和 TBuf 应该相同。至少这是一个想法。现在下一步是找出什么是障碍。一些技术细节可能会有所帮助(正在使用的硬件和操作系统)。
  • UPDATE3 有点不正确:如果您在 mmap 之前读取(进入相同的内存区域),您将失去您已阅读的内容。 msync() 只是将 mmap() 内存页面推送到磁盘,但相同的文件区域保持映射(您需要在文件中前进)。你确定你没有 DMA 的例子真的可以正常工作(我怀疑它还不是:))?

标签: linux mmap memory-mapped-files dma


【解决方案1】:

如果我理解正确的话,如果你 mmap() 文件(不确定你是否可以 mmap() 原始分区/块设备)并且通过 DMA 将数据直接写入此内存区域是有意义的。

为此,您需要能够控制p(放置新缓冲区的位置)或映射文件的地址。如果你不这样做 - 你将不得不复制内存内容(并且会失去一些 mmap 的好处)。

所以伪代码将是:

truncate("data.bin", 256GB);
fd = open( "data.bin", O_RDWR );
p = GetVirtualPointerToNewBuffer();
adr = mmap( p, 1GB, PROT_READ | PROT_WRITE, MAP_SHARED, fd, offset_in_file );
startDMA();
waitDMAfinish();
munmap( adr, 1GB );

这只是第一步,我不完全确定它是否适用于 DMA(没有这样的经验)。

我假设它是 32 位系统,但即便如此,1GB 映射文件大小也可能太大(如果您的 RAM 较小,您将进行交换)。

如果此设置可行,下一步将是循环以映射不同偏移量的文件区域并取消映射已填充的区域。

您很可能需要将addr 与 4KB 边界对齐。

当您取消映射区域时,它的数据将同步到磁盘。因此,您需要进行一些测试来选择适当的映射区域大小(当下一个区域由 DMA 填充时,必须有足够的时间来取消映射/写入前一个区域)。

更新:

当您通过 DMA 填充 mmap'ed 区域时究竟会发生什么我根本不知道(不确定如何准确检测 脏页:硬件做了什么,必须做什么软件)。

更新 2:据我所知:

DMA 的工作方式如下:

  • CPU 安排 DMA 传输(将传输数据写入 RAM 的地址);
  • DMA 控制器完成实际工作,而 CPU 可以并行完成自己的工作;
  • 一旦 DMA 传输完成 - DMA 控制器通过 IRQ 线(中断)向 CPU 发出信号,因此 CPU 可以处理结果。

这似乎很简单,但不涉及虚拟内存:DMA 应该独立于正在运行的进程(CPU 使用的实际 VM 表)工作。然而,它应该是某种机制使 CPU 缓存无效,以供 DMA 物理 RAM 页面修改(不知道 CPU 是否需要做某事,或者它是由硬件自动完成的)。

mmap() 分叉如下:

  • 在成功调用mmap() 后,附加磁盘上的文件以处理内存范围(很可能在 OS 内核中填充了一些数据结构来保存此信息);
  • 来自 mmaped 范围的 I/O(读取或写入)触发 pagefault,由内核从附加文件加载适当的块来处理;
  • 对映射范围的写入由硬件处理(不知道具体如何:可能写入以前未修改的页面会触发一些错误,这是由内核标记这些页面处理的em>; 或者这个标记完全是在硬件中完成的,当内核需要将修改的页面刷新到磁盘时,这个信息可供内核使用。
  • 修改的(dirty)页面由操作系统写入磁盘(如果它认为合适),或者可以通过msync()munmap() 强制

理论上应该可以将 DMA 传输到 mmaped 范围,但是您需要找出页面究竟是如何标记的 dirty (如果您需要做一些事情来通知内核哪些页面需要写入磁盘)。

更新3:

即使被 DMA 修改的页面没有被标记,您应该能够通过重写(读取 ant 然后写入相同)在每个页面中至少一个值(很可能是每个 4KB)来触发标记) 转移。只需确保编译器不会删除(优化)这种重写。

更新4:

似乎无法对打开的文件 O_WRONLY 进行映射(请参阅问题 cmets,我的实验也证实了这一点)。这是上述mmap() 工作的合乎逻辑的结论。确认here 也是如此(参考 POSIX 标准要求,以确保文件可读,而不管映射保护标志如何)。

除非有什么办法,否则这实际上意味着通过使用mmap(),您无法避免读取结果文件(在您的情况下是不必要的步骤)。

关于 DMA 传输到映射范围,我认为需要确保映射页面在 DMA 启动之前预先分配(因此有 真实 内存分配给 DMA 和映射区域)。在 Linux 上,有 MAP_POPULATE mmap 标志,但从手册来看,它只适用于 MAP_PRIVATE 映射(更改不会写入磁盘),所以很可能它是可用的。您可能必须通过访问每个映射页面来手动触发页面错误。这应该会触发结果文件的读取。

如果您仍希望同时使用 mmap 和 DMA,但避免读取结果文件,则必须修改内核内部以允许 mmap 使用 O_WRONLY 文件(例如通过零填充触发页面,而不是读取它们从磁盘)。

【讨论】:

  • 感谢您的回复。请参阅问题正文中的更新。再次感谢您!
  • 请参阅正文中的 UPDATE2。非常感谢你,你真的帮助我前进。
  • 我有一个接口函数,它返回充满数据的缓冲区的虚拟地址。我所需要的只是将它非常快地写入磁盘,而不是更多。所以,mmap 应该在这个函数调用之后,它将返回给我放置映射的地址。
  • mmap() attaches 文件到 RAM,所以在 mmap 调用之后,任何从内存范围读取将导致从文件读取,任何写入将被刷新到磁盘(由操作系统, msync, munmap)。如果您无法在 DMA 完成之前获得 DMA 缓冲区地址之前,恐怕mmap() 与简单的write() 相比不会提供速度优势。可能您需要更深入地研究此 DMA 函数如何选择要写入的内存区域,以及您是否可以预测或更改它以与 mmaped 范围重叠。