在汇编代码中将全局数据转储到磁盘
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
。
无论如何,我已经解决了您希望缓冲区发生什么的三种变体:
- 覆盖相同的缓冲区 (
mmap(2)
/ msync(2)
)
- 将缓冲区的快照附加到文件(使用
write(2)
或可能不起作用的零拷贝 vmsplice(2)
+ splice(2)
想法。)
- 在写入旧缓冲区后启动一个新的(清零的)缓冲区。
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_DIRECT
、write(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 时:
- 锁定以防止其他线程使用缓冲区
munmap(2)
你的缓冲区
mmap(2)
文件的下一个 16MiB 到同一地址 ,因此您无需将新地址传递给作者。根据 POSIX 的要求(不能让内核暴露内存),这些页面将被预置零。
- 解除锁定
可能 mmap(buf, 16MiB, ... MAP_FIXED, fd, new_offset)
可以替换 munmap
/ mmap
对。 MAP_FIXED
丢弃它重叠的旧 mmap
ings。我认为这并不意味着对文件/共享内存的修改被丢弃,而是实际映射发生变化,即使没有 munmap
.
Peter 回答中对 Append snapshots 案例的两处澄清。
1.不附加 O_DIRECT
正如 Peter 所说,如果您不使用 O_DIRECT
,write()
将在数据复制到页面缓存后立即 return。如果页面缓存已满,它将阻塞,直到一些过时的页面被刷新到磁盘。
如果您只是追加数据而不读取数据(很快),您可以定期调用带有 POSIX_FADV_DONTNEED
标志的 sync_file_range(2)
to schedule flush for previously written pages and posix_fadvise(2)
以从页面缓存中删除已刷新的页面。这可以显着降低 write()
阻塞的可能性。
2。附加 O_DIRECT
对于 O_DIRECT
,write()
通常会阻塞,直到数据发送到磁盘(尽管不能严格保证,请参阅 here)。由于这很慢,如果您需要非阻塞写入,请准备好实施您自己的 I/O 调度。
您可以存档的好处是:更可预测的行为(您可以控制何时阻塞)以及可能减少内存和 CPU 通过您的应用程序和内核的协作使用。
实验是在 Linux、x86 32 位上进行的。
所以假设在我的汇编程序中,我需要定期(例如每次执行100000个基本块后)将.bss部分中的数组从内存转储到磁盘。数组的起始地址和大小是固定的。数组记录了执行的基本块的地址,目前大小为16M
我试着写了一些本机代码,把memcpy
从.bss段放到栈上,然后再写回磁盘。但是在我看来这很繁琐,我担心性能和内存消耗,比如说,每次都在堆栈上分配一个非常大的内存...
所以这是我的问题,如何以有效的方式从全局数据部分转储内存?我够清楚吗?
首先,不要在 asm 中编写这部分代码,尤其是。一开始不是。编写一个 C 函数来处理这部分,并从 asm 中调用它。如果您需要在转储另一个 16MiB 时只对 运行s 的部分进行性能调整,那么您可以手动调整它。系统级编程就是检查来自系统调用(或 C stdio 函数)的错误 return,而在 asm 中这样做会很痛苦。
显然你可以在asm中写任何东西,因为与C相比进行系统调用没有什么特别的。而且与C相比,asm中没有任何部分更容易C,除了可能在锁定周围投入 MFENCE
。
无论如何,我已经解决了您希望缓冲区发生什么的三种变体:
- 覆盖相同的缓冲区 (
mmap(2)
/msync(2)
) - 将缓冲区的快照附加到文件(使用
write(2)
或可能不起作用的零拷贝vmsplice(2)
+splice(2)
想法。) - 在写入旧缓冲区后启动一个新的(清零的)缓冲区。
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_DIRECT
、write(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 时:
- 锁定以防止其他线程使用缓冲区
munmap(2)
你的缓冲区mmap(2)
文件的下一个 16MiB 到同一地址 ,因此您无需将新地址传递给作者。根据 POSIX 的要求(不能让内核暴露内存),这些页面将被预置零。- 解除锁定
可能 mmap(buf, 16MiB, ... MAP_FIXED, fd, new_offset)
可以替换 munmap
/ mmap
对。 MAP_FIXED
丢弃它重叠的旧 mmap
ings。我认为这并不意味着对文件/共享内存的修改被丢弃,而是实际映射发生变化,即使没有 munmap
.
Peter 回答中对 Append snapshots 案例的两处澄清。
1.不附加 O_DIRECT
正如 Peter 所说,如果您不使用 O_DIRECT
,write()
将在数据复制到页面缓存后立即 return。如果页面缓存已满,它将阻塞,直到一些过时的页面被刷新到磁盘。
如果您只是追加数据而不读取数据(很快),您可以定期调用带有 POSIX_FADV_DONTNEED
标志的 sync_file_range(2)
to schedule flush for previously written pages and posix_fadvise(2)
以从页面缓存中删除已刷新的页面。这可以显着降低 write()
阻塞的可能性。
2。附加 O_DIRECT
对于 O_DIRECT
,write()
通常会阻塞,直到数据发送到磁盘(尽管不能严格保证,请参阅 here)。由于这很慢,如果您需要非阻塞写入,请准备好实施您自己的 I/O 调度。
您可以存档的好处是:更可预测的行为(您可以控制何时阻塞)以及可能减少内存和 CPU 通过您的应用程序和内核的协作使用。