在汇编代码中将全局数据转储到磁盘

Dump global datas to disk in assembly code

实验是在 Linux、x86 32 位上进行的。

所以假设在我的汇编程序中,我需要定期(例如每次执行100000个基本块后)将.bss部分中的数组从内存转储到磁盘。数组的起始地址和大小是固定的。数组记录了执行的基本块的地址,目前大小为16M

我试着写了一些本机代码,把memcpy从.bss段放到栈上,然后再写回磁盘。但是在我看来这很繁琐,我担心性能和内存消耗,比如说,每次都在堆栈上分配一个非常大的内存...

所以这是我的问题,如何以有效的方式从全局数据部分转储内存?我够清楚吗?

首先,不要在 asm 中编写这部分代码,尤其是。一开始不是。编写一个 C 函数来处理这部分,并从 asm 中调用它。如果您需要在转储另一个 16MiB 时只对 运行s 的部分进行性能调整,那么您可以手动调整它。系统级编程就是检查来自系统调用(或 C stdio 函数)的错误 return,而在 asm 中这样做会很痛苦。

显然你可以在asm中写任何东西,因为与C相比进行系统调用没有什么特别的。而且与C相比,asm中没有任何部分更容易C,除了可能在锁定周围投入 MFENCE

无论如何,我已经解决了您希望缓冲区发生什么的三种变体:

  1. 覆盖相同的缓冲区 (mmap(2) / msync(2))
  2. 将缓冲区的快照附加到文件(使用 write(2) 或可能不起作用的零拷贝 vmsplice(2) + splice(2) 想法。)
  3. 在写入旧缓冲区后启动一个新的(清零的)缓冲区。 mmap(2) 输出文件的顺序块。

就地覆盖

如果您只是想每次都覆盖磁盘的同一区域,mmap(2) 一个文件并将其用作您的数组。 (定期调用 msync(2) 以强制将数据写入磁盘。)不过,mmapped 方法不能保证文件的状态一致。除非请求,否则写入可以刷新到磁盘。 IDK 如果有一种方法可以通过任何类型的保证来避免这种情况(即不仅仅是选择缓冲区刷新计时器等等,所以你的页面通常不会被写入,除非 msync(2)。)

附加快照

将缓冲区附加到文件的简单方法是在需要写入时调用write(2)write(2) 满足您的一切需求。如果您的程序是多线程的,您可能需要在系统调用之前锁定数据,然后释放锁定。我不确定写入系统调用的速度有多快 return。它可能只会在内核将您的数据复制到页面缓存后 return。

如果您只需要一个快照,但所有写入缓冲区的操作都是原子事务(即缓冲区始终处于一致状态,而不是需要彼此一致的值对),那么您不需要在调用 write(2) 之前需要锁定。在这种情况下会有少量偏差(假设内核按顺序复制,缓冲区末尾的数据将比开始的数据稍晚一些)。

IDK if write(2) return 使用直接 IO(零复制,绕过页面缓存)时速度变慢或变快。 open(2) 您的文件通常带有 O_DIRECTwrite(2)

如果你想写一个缓冲区的快照然后继续修改它,那么在这个过程中的某个地方必须有一个副本。否则 MMU 写时复制欺骗:

零拷贝追加快照

有一个 API 用于将用户页面零拷贝写入磁盘文件。 Linux 中的 vmsplice(2) and splice(2) 将让您告诉内核将您的页面映射到页面缓存中。如果没有 SPLICE_F_GIFT,我假设它将它们设置为写时复制。 (糟糕,实际上手册页说没有 SPLICE_F_GIFT,下面的 splice(2) 将不得不复制。所以 IDK 如果有一种机制来获得写时复制语义。)

假设有一种方法可以为您的页面获取写时复制语义,直到内核完成将它们写入磁盘并可以释放它们:

进一步的写入可能需要内核在数据到达磁盘之前 memcpy 一个或两个页面,但保存复制整个缓冲区。无论如何,软页面错误和 page-table 操作开销可能不值得,除非您的数据访问模式在短时间内在空间上非常本地化,直到写入命中磁盘和要写入的页面可以释放。 (我认为不存在以这种方式工作的 API,因为没有机制让页面在到达磁盘后立即释放。Linux 想要接管它们并将它们保存在页面缓存中.)

我从来没有使用过 vmsplice,所以我可能会弄错一些细节。

如果有办法创建同一内存的新写时复制映射,可能是 mmap 对临时文件进行新映射(在 tmpfs 文件系统上,概率 /dev/shm),这将使您无需长时间持有锁即可获得快照。然后您可以将快照传递给 write(2),并在发生太多写时复制页面错误之前尽快取消映射。

每个块的新缓冲区

如果每次写入后都可以从清零的缓冲区开始,您可以 mmap(2) 文件的连续块,因此您生成的数据总是已经在正确的位置。

  • (可选)fallocate(2) 一些 space 在您的输出文件中,以防止在您的写入模式不连续时出现碎片。
  • mmap(2) 您的缓冲区到输出文件的前 16MiB。
  • 运行正常
  • 当您想继续下一个 16MiB 时:
    1. 锁定以防止其他线程使用缓冲区
    2. munmap(2) 你的缓冲区
    3. mmap(2) 文件的下一个 16MiB 到同一地址 ,因此您无需将新地址传递给作者。根据 POSIX 的要求(不能让内核暴露内存),这些页面将被预置零。
    4. 解除锁定

可能 mmap(buf, 16MiB, ... MAP_FIXED, fd, new_offset) 可以替换 munmap / mmap 对。 MAP_FIXED 丢弃它重叠的旧 mmapings。我认为这并不意味着对文件/共享内存的修改被丢弃,而是实际映射发生变化,即使没有 munmap.

Peter 回答中对 Append snapshots 案例的两处澄清。

1.不附加 O_DIRECT

正如 Peter 所说,如果您不使用 O_DIRECTwrite() 将在数据复制到页面缓存后立即 return。如果页面缓存已满,它将阻塞,直到一些过时的页面被刷新到磁盘。

如果您只是追加数据而不读取数据(很快),您可以定期调用带有 POSIX_FADV_DONTNEED 标志的 sync_file_range(2) to schedule flush for previously written pages and posix_fadvise(2) 以从页面缓存中删除已刷新的页面。这可以显着降低 write() 阻塞的可能性。

2。附加 O_DIRECT

对于 O_DIRECTwrite() 通常会阻塞,直到数据发送到磁盘(尽管不能严格保证,请参阅 here)。由于这很慢,如果您需要非阻塞写入,请准备好实施您自己的 I/O 调度。

您可以存档的好处是:更可预测的行为(您可以控制何时阻塞)以及可能减少内存和 CPU 通过您的应用程序和内核的协作使用。