【问题标题】:Parsing mmaped file with strtok?用strtok解析映射文件?
【发布时间】:2016-02-27 12:22:06
【问题描述】:

这是我的问题:我想映射文件“filename.txt”,它基本上每行包含两对字符串:

"string1 string2
 string3 string4
 string5 string6..."

然后我想使用 strtok 分隔不同的字符串。

所以我像这样映射文件:

// open file
if ((fdsrc = open("filename.txt", O_RDONLY)) < 0) {
        fprintf(stderr, "src open error");
        exit(1);
    }

// get the size of the file
if (fstat(fdsrc, &statbuf) < 0) {
    fprintf(stderr, "fstat error");
    exit(1);
}

// mmap the file
if ((src = mmap(0, statbuf.st_size, PROT_READ, MAP_SHARED, fdsrc, 0)) == (caddr_t) -1) {
    fprintf(stderr, "mmap src");
    exit(1);
}

当我跑线时

printf("src: %s \n", src);

它会正确打印文件的内容!

但是当我尝试将单词分开时

char* token;
token = strtok(src, " \n");
while (token != NULL) {
    token = strtok(NULL, " \n");
}

输出是分段错误。 那为什么我不能使用 StrTok 呢?

【问题讨论】:

  • strtok 是 DESTRUCTIVE - 它在标记字符串时写入字符串。解决方案:1)在尝试“strtok()”之前将每个字符串复制到本地缓冲区,或2)打开映射读/写(而不是只读)。

标签: c string parsing mmap strtok


【解决方案1】:

strtok() 修改它所操作的字符串。假设您不想更改文件内容,则需要更改 mmap() 选项。

您正在以只读方式打开和映射文件:

if ((fdsrc = open("filename.txt", O_RDONLY)) < 0) {
...
if ((src = mmap(0, statbuf.st_size, PROT_READ, MAP_SHARED, fdsrc, 0)) == (caddr_t) -1) {
...

使用PROT_READ|PROT_WRITEMAP_PRIVATE 映射文件:

src = mmap(0, statbuf.st_size, PROT_READ|PROT_WRITE, MAP_PRIVATE, fdsrc, 0);
if (src == (caddr_t) -1) {

您可能需要使用O_RDWR 而不是O_RDONLY 打开文件

请注意:

如果文件大小与用于映射的页面大小的倍数完全匹配,则该文件将不是以 NUL 结尾的字符串,并且当strtok() 尝试读取映射末尾之后,您可能会得到一个 SIGSEGV .

在这种情况下,您可以在文件映射后立即mmap() 一个零填充页面。

【讨论】:

  • 这看起来是个坏建议。该文件将被修改,可能不是 OP 想要的。
  • @chqrlie 错了。根据man7.org/linux/man-pages/man2/mmap.2.htmlMAP_PRIVATE 创建一个私有的写时复制映射。映射的更新对映射同一文件的其他进程不可见,也不会传递到基础文件。 ...
  • 我的错,你是对的。但我坚持认为它仍然是一种扭曲的解析文本文件的方式。具体来说,不应使用strtok(),因为缓冲区可能不会在映射结束之前被'\0' 终止,因此会调用未定义的行为,例如,如果文件长度是页面大小的精确倍数。
  • 您的建议不能保证有效:mmap 的 BSD 手册页说:如果 offset 或 len 不是页面大小的倍数,则映射区域 可能 超出指定范围。超出映射对象末尾的任何扩展都将被零填充。因此它可能有效,也可能无效。在对内存保护进行细粒度控制的系统上,映射区域可能是基于字节的。
  • 好点。 不适合胆小的人是轻描淡写的。
【解决方案2】:

您的文件以只读方式映射为PROT_READ。但是strtok() 修改了它的第一个参数src,并得到了分段错误。您需要在使用strtok 之前制作一个可写副本,或者切换到只读取其输入的机制。在我看来,将该缓冲区的保护更改为PROT_RW 似乎很奇怪,尤其是如果您打算在程序的其他地方使用该文件的未修改内容。

作为替代方案,我建议使用strstr()(或不需要空字节终止的替代实现)来定位行尾子字符串,然后在找到的位置开始下一次搜索最后一次出现,加上子字符串的长度。请参阅下面有关空字节终止的注释。一个简化的例子:

  const char *delim = "\n";                                                                                                                          
  const char *start = src;                                                                                                                           
  const char *end = NULL;                                                                                                                            
  const int srclen = statbuf.st_size;                                                                                                                
  const int delim_length = strlen(delim);                                                                                                            

  while (start && start < (src + srclen)) {                                                                                                          
    end = strstr(start, delim);                                                                                                                      

    if (NULL == end) {  
      // use of %.* to print at most X chars from string.                                                                                                                             
      printf("Token: %.*s\n", (int) (src + srclen - start), start);                                                                                 
      break;                                                                                                                                         
    } else {                                                                                                                                         
      printf("Token: %.*s\n", (int) (end - start), start);                                                                                           
      start = end + delim_length;                                                                                                                    
    }                                                                                                                                                
  }                     

mmap 区域不能以 nul 字节结尾(如评论所建议的那样)

strstr() 适用于以空字符结尾的字符串。您的映射区域可能不会以空字节结尾。内核很可能会使用\0s 擦除最后一个映射的内存页面(超过文件结尾)的剩余部分,以避免进程之间的数据泄漏,但如果您的文件长度恰好是页面大小的倍数,你在使用strstr() 时会遇到麻烦——不会有一个空字节来支持你。

您可以推出自己的小字符串查找器strnstr()。或者强制在末尾加盖另一个空页。

【讨论】:

  • 您不应该使用strstr,因为mmapped 缓冲区不一定是'\0' 终止的。
  • 感谢@chqrlie。你是对的。这让我陷入了一个小小的测试螺旋。当seek() 超过EOF(返回0B)时,允许read() 调用,所以我想知道询问mmap 比文件大的区域会产生什么。我最终发现(至少在 linux 上)是,如果文件大小不是页面大小的倍数,则包含文件数据的最后一个缓冲区页面将用零填充。例如,将 10B 文件读入 8MB mmap 缓冲区(带 4K 页)将为索引 10 到 4095 提供零,但如果您读取索引 4096 和 8MB 之间的任何内容,则会出现总线错误 (SIGBUS)。
【解决方案3】:

strtok() 修改您向其传递指针的char 数组。

mmap 文件处于只读模式,因此当strtok 尝试修改内存时,您遇到了违规。

在读写模式下mmap 文件是个坏主意,文件会被修改并可能损坏。

strtok 不适合您的用途,请编写您自己的匹配函数,该函数不修改其参数数组并返回偏移量和长度。

还要注意mmapped 内存不应该被访问超过文件的大小并且不一定是'\0' 终止,因此你不应该使用字符串函数来搜索它(strchr,@987654330 @, strlen...) 也不能从中复制 (strcpy)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-02
    • 1970-01-01
    • 1970-01-01
    • 2015-05-11
    • 2017-07-18
    相关资源
    最近更新 更多