使用File::Map如何正确地向文件写入数据?

8
我经常使用File::Map将特别小的文本文件映射到内存中,并且例如对其执行一些只读正则表达式处理。现在我有一个用例需要替换文件中的一些文本,因此认为我仍然可以使用File::Map,因为它记录了以下内容:

文件被映射到一个变量中,可以像任何其他变量一样进行读取,可以使用标准Perl技术(如正则表达式和substr)进行写入。

虽然我需要替换的数据已经正确地替换到了文件中,但是我会失去数据,因为文件保留了原始大小,并且在最后截断数据。新数据比旧数据稍微多一点。这两个问题都在以下句子中被警告:

不建议直接向内存映射文件中写入

将新值截断为内存映射的大小

这两个警告的解释似乎意味着永远不应该使用File::Map写入任何内容,但是在某些情况下也可能起作用,例如可以接受截断文件或者整个文件大小根本没有改变。但是第一个引用明确提到了写操作是被支持的,没有任何例外。
因此,是否有一种安全的方式可以使用File::Map进行写入,例如增加底层文件等?第一个警告使用了“直接”这个词,我感觉可能有其他更好的支持写入的方式?
我目前只是在映射视图上使用=~ s///,这似乎是错误的方法。我甚至找不到任何人尝试过使用File::Map进行写入,仅找到官方测试执行的和我执行的相同,并期望得到警告。此外,查看代码时,似乎只有一种用例中写入时根本不会产生警告,尽管我不理解如何触发该用例:
static int mmap_write(pTHX_ SV* var, MAGIC* magic) {
        struct mmap_info* info = (struct mmap_info*) magic->mg_ptr;
        if (!SvOK(var))
                mmap_fixup(aTHX_ var, info, NULL, 0);
        else if (!SvPOK(var)) {
                STRLEN len;
                const char* string = SvPV(var, len);
                mmap_fixup(aTHX_ var, info, string, len);
        }
        else if (SvPVX(var) != info->fake_address)
                mmap_fixup(aTHX_ var, info, SvPVX(var), SvCUR(var));
        else
                SvPOK_only_UTF8(var);
        return 0;
}

https://metacpan.org/source/LEONT/File-Map-0.55/lib/File/Map.xs#L240

如果要避免使用写操作,那么为什么文档明确提到支持它呢?但在除一种情况外的所有情况下,它至少会导致一个警告,所以看起来并不被支持。


使用 File::Map 和使用具有读写访问权限的文件句柄(例如,在 +< 模式下打开)之间有什么区别?当您使用读写文件句柄时,还必须小心不要覆盖现有数据并在缩小文件时截断它,但有时仍然很有用。 - mob
2个回答

8

一个mmap是将文件的一部分固定大小映射到内存中。

各种映射函数将所提供标量的字符串缓冲区设置为映射的内存页。如果请求,操作系统将反映对该缓冲区的任何更改到文件以及反之亦然。

使用mmap的正确方法是修改字符串缓冲区,而不是替换它。

  • Anything that changes the string buffer without changing its size is appropriate.

    $ perl -e'print "\0"x16' >scratch
    
    $ perl -MFile::Map=map_file -we'
       map_file my $map, "scratch", "+<";
       $map =~ s/\x00/\xFF/g;             # ok
       substr($map, 6, 2, "00");          # ok
       substr($map, 8, 2) = "11";         # ok
       substr($map, 7, 2) =~ s/../22/;    # ok
    '
    
    $ hexdump -C scratch
    00000000  ff ff ff ff ff ff 30 32  32 31 ff ff ff ff ff ff  |......0221......|
    00000010
    
  • Anything that replaces the string buffer (such as assigning to the scalar) is not ok.

    ...kinda. The module notices you've replaced the scalar's buffer. It proceeds to copy the contents of the new buffer to the mapped memory, then replaces the scalar's buffer with the pointer to the mapped memory.

    $ perl -e'print "\0"x16' >scratch
    
    $ perl -MFile::Map=map_file -we'
       map_file my $map, "scratch", "+<";
       $map = "4" x 16;  # Effectively: substr($map, 0, 16, "4" x 16)
    '
    Writing directly to a memory mapped file is not recommended at -e line 3.
    
    $ hexdump -C scratch
    00000000  34 34 34 34 34 34 34 34  34 34 34 34 34 34 34 34  |4444444444444444|
    00000010
    

    Aside from the warning can be silenced using no warnings qw( substr );,[1] the only down side is that doing this way requires using memcpy to copy length($map) bytes, while using substr($map, $pos, length($repl), $repl) only requires copying length($repl) bytes.

  • Anything that changes the size of string buffer is not ok.

    $ perl -MFile::Map=map_file -we'
       map_file my $map, "scratch", "+<";
       $map = "5" x 32;  # Effectively: substr($map, 0, 16, "5" x 16)
    '
    Writing directly to a memory mapped file is not recommended at -e line 3.
    Truncating new value to size of the memory map at -e line 3.
    
    $ hexdump -C scratch
    00000000  35 35 35 35 35 35 35 35  35 35 35 35 35 35 35 35  |5555555555555555|
    00000010
    

警告: 如果您缩小了缓冲区,该模块不会发出警告,即使除了使用NUL覆盖一个字节外没有任何影响。

$ perl -e'print "\0"x16' >scratch

$ perl -MFile::Map=map_file -we'
   map_file my $map, "scratch", "+<";
   substr($map, 0, 16, "6" x 16);
   substr($map, 14, 2, "");
'

$ hexdump -C scratch
00000000  36 36 36 36 36 36 36 36  36 36 36 36 36 36 00 36  |66666666666666.6|
00000010

我已经提交了一个工单


  1. 这有点讽刺,因为它或多或少是在不使用substr时发出警告,但我想它也会在“错误地”使用substr时发出警告。

请问能否同时提供使用正则表达式进行替换的示例呢?因为我的问题中提到了这点。比较如下两种方法: perl -MFile::Map=map_file -we"map_file my $map, 'scratch', '+<'; $map =~ s/^./1/"perl -MFile::Map=map_file -we"map_file my $map, 'scratch', '+<'; $map =~ s/.$/11/" 后者会导致警告,因为它增加了缓冲区的大小。 - Thorsten Schöning
再次强调,这个映射有固定大小,因此您无法这样做。 - ikegami
我明白了,只是想提供一个例子,表明它不仅适用于使用 substr,而且对应的情况不再有效,因为缓冲区的大小增加了。 - Thorsten Schöning
我添加了一个示例,展示s///可以原地修改,但已经有一个示例显示尝试更改地图大小会失败。 - ikegami

6

第一引言

文件被映射到一个变量中,可以像任何其他变量一样读取它,并且可以使用标准 Perl 技术(如正则表达式和substr)进行写入。

在“简单性”标题下。

事实上,您可以简单地编写操作字符串的 Perl 代码,数据将最终存储在文件中。

但是,在警告一节中,我们有:

直接向内存映射文件写入不被推荐。由于perl内部工作方式的原因,目前还没有一种映射实现方式既能够允许直接赋值又能够保持良好性能。为了达成折中方案,如果你还是这样做了,File::Map可以修复混乱,但会警告你正在做一些不应该做的事情。只有在使用use warnings 'substr'时才会发出此警告。
也就是说,除非可以就地修改字符串缓冲区(必须先将字符串组装并存储在内存中,然后才能将其复制到文件中),否则通过mmap'd变量进行写操作是不高效的。如果您可以接受这一点,可以使用no warnings 'substr'来禁用警告。
此外,查看代码时,似乎只有一种情况下写入不会产生任何警告,尽管我不理解如何触发它。
这是在尝试将缓冲区写入自身的情况。当标量实际上被就地修改时,就会发生这种情况。其他情况是解决方案,用于替换字符串缓冲区(例如,因为它被覆盖:$foo = $bar)。对于真正的就地修改,不需要额外的工作,也不会收到警告。
但这并不能帮助您,因为使用固定大小映射缓冲区无法进行原地增长字符串。
更改文件的大小是不可能的。这不是因为File::Map,而是因为底层mmap系统调用基于固定大小映射,并且不提供任何自动调整文件大小的选项。
如果您需要编辑文件(特别是小文件),我建议使用Path::Tiny中的edit

使用Path::Tiny的其他好处:还有edit_lines可以逐行工作;所有编辑都是通过先写入临时文件,然后原子性地将其重命名为原始文件来安全完成的;而且edit和edit_lines有一个_utf8版本,每当编辑文本文件时,这很可能是您想要的。 - Grinnz
2
关于“我不确定如何触发它(也许是通过自我赋值,$foo = $foo?)”,Perl字符串是可变的。当字符串缓冲区被修改而不是替换时,例如使用substr($s, 2, 1, '!')时,就会采用这种方式。这是修改映射内存的正确方法。 - ikegami
关于“也就是说,通过mmap变量进行写入并不高效”,嗯,其实并不完全正确。这个警告并不是性能警告。将GET魔法添加到标量中使得对字符串的所有更改——即使使用substr($s, 2, 1, '!')正确地完成——都会导致一个子调用。虽然这是一个相对昂贵的操作,但它并不是那么昂贵(特别是因为相关的子程序是用C编写的)。 - ikegami
@ikegami 我已经编辑了我的回答的那一部分。希望现在更准确了。至于效率:它字面上说:“目前还没有可能编写一个映射实现,既可以进行直接赋值,又能表现良好”。 - melpomene
是的。但由于该模块支持直接赋值,无论您是否使用警告代码,惩罚都是恒定的。所以,1)这不是警告的内容。另外,2)这个惩罚实际上很小(每次更改标量时进行子调用+如果替换缓冲区则进行memcpy)。 - ikegami

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接