我需要清除Linux中的大地址范围(每次200页的订单)。我试过两种方法-
memset -清除地址范围的最简单方法。执行速度比方法2慢一点。munmap/mmap --我在地址范围内调用了munmap,然后使用相同的权限再次调用了相同的地址。由于MAP_ANONYMOUS被传递,页面将被清除。第二种方法使基准测试运行速度提高5-10%。当然,基准不仅仅是清除页面。如果我正确理解,这是因为操作系统有一个零d页面池,它将其映射到地址范围。
但我不喜欢这样,因为munmap和mmap不是原子的。如果同时执行另一个mmap (以NULL作为第一个参数),则会使我的地址范围不可用。
那么,我的问题是,Linux是否提供了一个系统调用,可以将物理页面替换为具有零页的地址范围?
我试图查看glibc (特别是memset)的来源,看看它们是否使用了任何技术来有效地做到这一点。但我什么也找不到。
发布于 2018-04-18 21:20:34
memset()似乎比mmap()快一个数量级来获得一个新的零填充页面,至少在我现在可以访问的Solaris 11服务器上。我强烈怀疑Linux会产生类似的结果。
我编写了一个小的基准程序:
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <strings.h>
#include <sys/time.h>
#define NUM_BLOCKS ( 512 * 1024 )
#define BLOCKSIZE ( 4 * 1024 )
int main( int argc, char **argv )
{
int ii;
char *blocks[ NUM_BLOCKS ];
hrtime_t start = gethrtime();
for ( ii = 0; ii < NUM_BLOCKS; ii++ )
{
blocks[ ii ] = mmap( NULL, BLOCKSIZE,
PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0 );
// force the creation of the mapping
blocks[ ii ][ ii % BLOCKSIZE ] = ii;
}
printf( "setup time: %lf sec\n",
( gethrtime() - start ) / 1000000000.0 );
for ( int jj = 0; jj < 4; jj++ )
{
start = gethrtime();
for ( ii = 0; ii < NUM_BLOCKS; ii++ )
{
blocks[ ii ] = mmap( blocks[ ii ],
BLOCKSIZE, PROT_READ | PROT_WRITE,
MAP_FIXED | MAP_ANONYMOUS | MAP_PRIVATE, -1, 0 );
blocks[ ii ][ ii % BLOCKSIZE ] = 0;
}
printf( "mmap() time: %lf sec\n",
( gethrtime() - start ) / 1000000000.0 );
start = gethrtime();
for ( ii = 0; ii < NUM_BLOCKS; ii++ )
{
memset( blocks[ ii ], 0, BLOCKSIZE );
}
printf( "memset() time: %lf sec\n",
( gethrtime() - start ) / 1000000000.0 );
}
return( 0 );
}请注意,在页面的任何位置写入单个字节都是强制创建物理页面所需的。
我在我的Solaris 11文件服务器上运行了它(这是我现在运行的唯一POSIX风格的系统)。我没有在我的Solaris系统上测试madvise(),因为Solaris与Linux不同,它并不保证映射将被零填充页面重新填充,只是“系统开始释放资源”。
结果:
setup time: 11.144852 sec
mmap() time: 15.159650 sec
memset() time: 1.817739 sec
mmap() time: 15.029283 sec
memset() time: 1.788925 sec
mmap() time: 15.083473 sec
memset() time: 1.780283 sec
mmap() time: 15.201085 sec
memset() time: 1.771827 secmemset()几乎快一个数量级。当我有机会的时候,我会在Linux上重新运行这个基准,但是它可能必须在VM (AWS等)上。
这并不奇怪-- mmap()很昂贵,而且内核在某个时候仍然需要对页面进行零调整。
有趣的是,注释掉一行
for ( ii = 0; ii < NUM_BLOCKS; ii++ )
{
blocks[ ii ] = mmap( blocks[ ii ],
BLOCKSIZE, PROT_READ | PROT_WRITE,
MAP_FIXED | MAP_ANONYMOUS | MAP_PRIVATE, -1, 0 );
//blocks[ ii ][ ii % BLOCKSIZE ] = 0;
}产生的结果如下:
setup time: 10.962788 sec
mmap() time: 7.524939 sec
memset() time: 10.418480 sec
mmap() time: 7.512086 sec
memset() time: 10.406675 sec
mmap() time: 7.457512 sec
memset() time: 10.296231 sec
mmap() time: 7.420942 sec
memset() time: 10.414861 sec强制创建物理映射的负担已经转移到memset()调用,只在测试循环中留下隐式munmap(),当MAP_FIXED mmap()调用替换它们时,这些映射就会被破坏。请注意,munmap()所花费的时间大约是将页面保存在地址空间和memset()‘将其设置为零的3-4倍。
mmap()的成本并不是mmap()/munmap()系统调用本身,而是新页面需要大量的幕后CPU周期来创建实际的物理映射,而这并不发生在mmap()系统调用本身之后,当进程访问内存页时。
如果您怀疑结果,请注意这个来自Linus Torvalds自己的LMKL帖子
..。 然而,玩游戏与虚拟内存映射本身是非常昂贵的。它有很多真正的缺点,人们往往会忽略它,因为内存复制被看作是非常缓慢的事情,有时优化拷贝被认为是一种明显的改进。 mmap的缺点:
使用Solaris工作室收藏和分析器工具分析代码产生了以下输出:
Source File: mm.c
Inclusive Inclusive Inclusive
Total CPU Time Sync Wait Time Sync Wait Count Name
sec. sec.
1. #include <stdio.h>
2. #include <sys/mman.h>
3. #include <string.h>
4. #include <strings.h>
5.
6. #include <sys/time.h>
7.
8. #define NUM_BLOCKS ( 512 * 1024 )
9. #define BLOCKSIZE ( 4 * 1024 )
10.
11. int main( int argc, char **argv )
<Function: main>
0.011 0. 0 12. {
13. int ii;
14.
15. char *blocks[ NUM_BLOCKS ];
16.
0. 0. 0 17. hrtime_t start = gethrtime();
18.
0.129 0. 0 19. for ( ii = 0; ii < NUM_BLOCKS; ii++ )
20. {
21. blocks[ ii ] = mmap( NULL, BLOCKSIZE,
22. PROT_READ | PROT_WRITE,
3.874 0. 0 23. MAP_ANONYMOUS | MAP_PRIVATE, -1, 0 );
24. // force the creation of the mapping
7.928 0. 0 25. blocks[ ii ][ ii % BLOCKSIZE ] = ii;
26. }
27.
28. printf( "setup time: %lf sec\n",
0. 0. 0 29. ( gethrtime() - start ) / 1000000000.0 );
30.
0. 0. 0 31. for ( int jj = 0; jj < 4; jj++ )
32. {
0. 0. 0 33. start = gethrtime();
34.
0.560 0. 0 35. for ( ii = 0; ii < NUM_BLOCKS; ii++ )
36. {
37. blocks[ ii ] = mmap( blocks[ ii ],
38. BLOCKSIZE, PROT_READ | PROT_WRITE,
33.432 0. 0 39. MAP_FIXED | MAP_ANONYMOUS | MAP_PRIVATE, -1, 0 );
29.535 0. 0 40. blocks[ ii ][ ii % BLOCKSIZE ] = 0;
41. }
42.
43. printf( "mmap() time: %lf sec\n",
0. 0. 0 44. ( gethrtime() - start ) / 1000000000.0 );
0. 0. 0 45. start = gethrtime();
46.
0.101 0. 0 47. for ( ii = 0; ii < NUM_BLOCKS; ii++ )
48. {
7.362 0. 0 49. memset( blocks[ ii ], 0, BLOCKSIZE );
50. }
51.
52. printf( "memset() time: %lf sec\n",
0. 0. 0 53. ( gethrtime() - start ) / 1000000000.0 );
54. }
55.
0. 0. 0 56. return( 0 );
0. 0. 0 57. }
Compile flags: /opt/SUNWspro/bin/cc -g -m64 mm.c -W0,-xp.XAAjaAFbs71a00k.请注意在mmap()中花费的大量时间,以及在每个新映射的页面中设置一个字节所花费的大量时间。
这是analyzer工具的概述。注意大量的系统时间:

系统消耗的大量时间是映射和取消映射物理页面所需的时间。
这个时间线显示了当所有这些时间被消耗时:

浅绿色是系统时间--所有这些都在mmap()循环中。当memset()循环运行时,您可以看到切换到深绿色用户时间。我强调了其中一个例子,这样你就可以看到当时发生了什么。
更新了Linux:的结果
setup time: 2.567396 sec
mmap() time: 2.971756 sec
memset() time: 0.654947 sec
mmap() time: 3.149629 sec
memset() time: 0.658858 sec
mmap() time: 2.800389 sec
memset() time: 0.647367 sec
mmap() time: 2.915774 sec
memset() time: 0.646539 sec这与我昨天在评论中所说的完全一致: FWIW,我运行的一个快速测试显示,对memset()的简单单线程调用比重做mmap()快5到10倍。
我只是不理解对mmap()的这种迷恋。mmap()是一个非常昂贵的调用,它是一个强制的单线程操作--机器上只有一组物理内存。mmap()不仅仅是 system ,它还会影响整个进程地址空间和整个主机上的VM系统。
使用任何形式的mmap()只是为了清除内存页是适得其反的。首先,页面不会被免费归零--有些东西必须用memset()来清除它们。添加、拆卸和重新创建到该memset()的内存映射只是为了清除内存页,这是没有任何意义的。
memset()还具有一个以上线程可以在任何时候清除内存的优点。对内存映射进行更改是一个单线程进程。
发布于 2018-04-18 11:10:48
madvise(..., MADV_DOTNEED)应该等同于Linux上匿名映射上的munmap/mmap。这有点奇怪,因为这不是我理解“不需要”的语义应该是什么的方式,但它确实丢弃了Linux上的页面。
$ cat > foo.c
#include <sys/types.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
int
main(int argc, char **argv)
{
int *foo = mmap(NULL, getpagesize(), PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
*foo = 42;
printf("%d\n", *foo);
madvise(foo, getpagesize(), MADV_DONTNEED);
printf("%d\n", *foo);
return 0;
}
$ cc -o foo foo.c && ./foo
42
0
$ uname -sr
Linux 3.10.0-693.11.6.el7.x86_64MADV_DONTNEED在其他操作系统上不这么做,所以这绝对不是可移植的。例如:
$ cc -o foo foo.c && ./foo
42
42
$ uname -sr
Darwin 17.5.0但是,您不需要取消映射,只需覆盖映射即可。作为一个好处,这是一个更可移植的:
$ cat foo.c
#include <sys/types.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
int
main(int argc, char **argv)
{
int *foo = mmap(NULL, getpagesize(), PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
*foo = 42;
printf("%d\n", *foo);
mmap(foo, getpagesize(), PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0);
printf("%d\n", *foo);
return 0;
}
$ cc -o foo foo.c && ./foo
42
0
$而且,我也不确定你是否正确地设定了基准。创建和删除映射可能非常昂贵,我不认为空闲的归零会有多大帮助。新mmap:ed页面直到第一次被使用才会被映射,而在Linux上,这意味着写而不是读,因为如果第一次访问页面是读而不是写,Linux就会做一些愚蠢的事情。因此,除非基准写入新的mmap:ed页面,否则我怀疑您以前的解决方案和我在这里建议的解决方案实际上都不会比一个愚蠢的memset更快。
发布于 2018-04-18 16:24:59
注意:这是而不是的答案,我只是需要格式化功能。
顺便说一句:/dev/zero全零页甚至可能不存在,并且.read()方法的实现方式如下(类似的情况发生在dev/null,它只返回length参数):
struct file_operations {
...
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
...
};
static ssize_t zero_read (struct file *not_needed_here, char __user * buff, size_t len, loff_t * ignored)
{
memset (buff, 0, len);
return len;
}https://stackoverflow.com/questions/49896578
复制相似问题