【问题标题】:How to properly write to a file using File::Map?如何使用 File::Map 正确写入文件?
【发布时间】:2019-05-09 07:48:34
【问题描述】:

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

文件被映射到一个变量,该变量可以像任何其他变量一样被读取,并且可以使用标准 Perl 技术(例如 regexps 和 substr)写入。

虽然我有兴趣替换的数据已在文件中正确替换,但我正在丢失数据,因为文件保持其原始大小并且数据最终被截断。新数据比旧数据大一点。这两件事都被警告,如使用以下句子记录:

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

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

对这两个警告的解释读起来就像一个人不应该使用File::Map 写任何东西,但它可能适用于可以处理截断文件或整体文件大小根本没有改变的情况。但是第一个引用明确提到支持写入,该规则没有任何例外。

那么,有没有一些特殊的方法可以安全地使用File::Map 编写,例如让基础文件增加等等?第一个警告使用directly 的措辞,我觉得还有其他更好的支持方式来编写?

我目前只是在映射视图上使用=~ 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 和使用具有读写访问权限的文件句柄(例如,以+< 模式打开)有什么区别?当您使用读写文件句柄时,您还必须注意不要破坏现有数据并在缩小文件时截断它,但有时它仍然有用。

标签: perl memory-mapped-files


【解决方案1】:

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

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

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

  • 任何改变字符串缓冲区而不改变其大小的东西都是合适的。

    $ 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
    
  • 任何替换字符串缓冲区的东西(例如分配给标量)都不行。

    ...有点。该模块注意到您已经替换了标量的缓冲区。它继续将新缓冲区的内容复制到映射内存,然后用指向映射内存的指针替换标量缓冲区。

    $ 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
    

    除了警告可以使用no warnings qw( substr ); 静音,[1] 唯一的缺点是这样做需要使用memcpy 复制length($map) 字节,而使用@987654327 @ 只需要复制length($repl) 字节。

  • 任何改变字符串缓冲区大小的东西都是不行的。

    $ 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

我已经提交了ticket


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

【讨论】:

  • 能否请您添加使用正则表达式替换的示例?因为我的问题中提到了这一点。 perl -MFile::Map=map_file -we"map_file my $map, 'scratch', '+&lt;'; $map =~ s/^./1/"perl -MFile::Map=map_file -we"map_file my $map, 'scratch', '+&lt;'; $map =~ s/.$/11/" 后者在增加缓冲区时会导致警告。
  • 同样,映射的大小是固定的,所以你不能这样做
  • 我明白这一点,只是想提供一个示例,表明它不仅适用于使用substr,而且由于缓冲区大小增加,相应的情况不再适用。
  • 我添加了一个示例,显示 s/// 可以就地修改,但已经有一个示例显示尝试更改地图大小失败。
【解决方案2】:

first quote

文件被映射到一个变量,该变量可以像任何其他变量一样被读取,并且可以使用标准 Perl 技术(例如 regexps 和 substr)写入。

在“简单”标题下。

确实如此:您可以简单地编写操作字符串的 Perl 代码,数据将最终保存在文件中。

但是,在Warnings 部分我们有:

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

由于 perl 内部的工作方式,不可能编写一个允许直接赋值但性能良好的映射实现。作为一种折衷方案,如果你这样做,File::Map 能够解决这个问题,但它会警告你你正在做一些你不应该做的事情。仅当use warnings 'substr' 生效时才会发出此警告。

也就是说,通过 mmap'd 变量进行写入效率不高,除非可以就地完成对字符串缓冲区的修改(字符串必须先组装并存储在内存中,然后才复制到文件中) .如果您对此没问题,可以使用no warnings 'substr' 禁用警告。

此外,查看代码,似乎只有一个用例在其中写入根本不会导致警告,尽管我不明白我是如何触发它的。

这就是您尝试将缓冲区写入自身的情况。当标量实际修改到位时,就会发生这种情况。其他情况是替换字符串缓冲区时的解决方法(例如,因为它已被覆盖:$foo = $bar)。对于真正的就地修改,不需要额外的工作并且您不会收到警告。

但这对您没有帮助,因为无法使用固定大小的映射缓冲区就地完成字符串的增长。

无法更改文件的大小。这不是因为 File::Map,而是因为底层的 mmap system call 适用于固定大小的映射,并且不提供任何自动调整文件大小的选项。

如果您需要编辑文件(尤其是小文件),我建议改用edit in Path::Tiny

【讨论】:

  • 使用 Path::Tiny 的额外好处:还有 edit_lines 可以逐行工作;通过写入临时文件然后以原子方式将其重命名为原始文件,所有编辑都安全地完成;并且 edit 和 edit_lines 有一个 _utf8 版本,这可能是您在编辑文本文件时想要的。
  • Re "我不确定如何触发它(可能是自赋值,$foo = $foo?)",Perl 字符串是可变的。该路径在字符串缓冲区被修改而不是被替换时采用,例如在使用substr($s, 2, 1, '!') 时。这是修改映射内存的正确方法。
  • Re "也就是说,通过 mmap'd 变量写入效率不高",嗯,不是真的。该警告不是性能警告。添加到标量的 GET 魔法使得对字符串的所有更改——即使使用substr($s, 2, 1, '!') 正确完成——也会导致子调用。虽然这是一个相对昂贵的操作,但它并没有那么昂贵(特别是因为有问题的 sub 是用 C 编写的)。
  • @ikegami 我已经编辑了答案的那一部分。希望现在更准确。至于效率:它的字面意思是“不可能编写一个允许直接分配但性能良好的映射实现”。
  • 是的。但是由于模块支持直接赋值,所以无论你是否使用警告代码,惩罚都是不变的。所以,1)这不是警告的内容。并且 2) 惩罚实际上非常小(如果您更换了缓冲区,则对标量的每次更改 + 一个 memcpy 的子调用)。
猜你喜欢
  • 2016-05-24
  • 1970-01-01
  • 1970-01-01
  • 2020-11-08
  • 2012-05-28
  • 2020-10-23
  • 1970-01-01
  • 1970-01-01
  • 2015-07-02
相关资源
最近更新 更多