部分文本上的并发 mmap()

concurrent mmap() on a portion of text

我问了 关于如何读取从偏移量 pos 到偏移量 endmmap() 的文本文件的问题。特别是文本文件由 多线程 使用以下代码读取:

void getNextKeyValue() {
    key = pos;//value is the actual file offset
    char *mmappedData = (char*) mmap(NULL, end-pos+1, PROT_READ, MAP_PRIVATE , fd, pos);
    assert(mmappedData != NULL);
    value.assign(mmappedData);
    assert(munmap(mmappedData, end-pos+1)==0);
    morePairs = false;
}

未报告的变量在其他地方声明和初始化。什么 顺便说一下,下面的代码读取了 整个 文本文件,而不是从偏移量 posend

上瘾时,程序在多个线程中突然终止(无错误输出),而在只有一个线程读取整个文件时正确终止。

更新:

Following this example (you can try my version, using cout insted of write, HERE with ./main main.cpp 10 20) 我发现我做错的是我通过cout<<mmappedData<<endl打印读取的数据。 Insted 如果我使用 write(STDOUT_FILENO, mmappedData+pos-pa_offset, end-pos); 打印文本的右侧部分。

我仍然不明白的是 为什么 整个文本存储在 mmappedData 中(或 addr 下面的链接示例): mmap usage 明确指出读取的字节数是从第 4 个 arg 开始的第 2 个 arg。

新信息更新:

您的问题是您误解了 mmap 的工作原理。 mmap 映射内存页面,而不是字节;即使您只要求映射 24 个字节,它实际上也会映射几千字节(在大多数系统上为 4KB),并且不能保证数据 NUL 终止(事实上,除非映射到达文件末尾,对于文本输入文件,它可能没有 NUL 终止)。 std::string::assign 方法仅使用 char * 作为参数使用 C-style 字符串逻辑;它只是不断地读取字符,直到遇到 NUL。如果您是 "lucky",mmap 之后的页面是可读的并且包含一个 NUL,它只是将 NUL 之前的所有内容复制到字符串中(或者等效地执行cout << mmappedData,把它写出来),如果不是,你在映射后尝试访问未映射的内存地址时出现段错误。

如果目标是从char*分配特定数量的字节到std::string,你需要使用assign的two-arg形式来使开始和长度就像你对 write 所做的那样明确,然后它将只使用你需要的数据:

value.assign(mmappedData+pos-pa_offset, end-pos);

原推测答案:

给出的代码不够清晰,无法排除其他问题,但如果您不断地重新读取值,并且设置了 NDEBUG(禁用 asserts),那么您就得到了两个重叠的问题:

  1. 您没有检查 mmap 的 return 值(即使启用了 assert,它也没有被正确检查;return 的错误 mmapMAP_FAILED,不是 NULL)
  2. 你永远不会 munmap-ing; munmap 调用在 assert 内,并且在禁用 assert 的情况下,您断言的代码将被预处理器删除。

所以如果你有大量线程一遍又一遍地调用这个函数(特别是如果被映射的区域很大),你最终会 运行 虚拟内存不足 space ;如果每次调用都映射 1 MB,即使在 64 位系统上(实际上只有 47 位用户模式虚拟地址 space),你也会 运行 超出虚拟地址 space在〜134M电话之后。如果每次调用都映射 1 GB,那么在 ~134K 次调用后你会 运行 出局。当然,在 32 位系统上,您会更快地达到极限

除此之外,我无从知晓是否还有其他问题; value.assign 如果是共享数据,也很容易成为问题。

以这种方式从文件中读取数据将是有效的 single-threaded。每个 mmap/munmap 对都需要对进程的地址 space 内存映射进行两次修改 - 并且只有一个线程可以在任何时候导致对地址 space 映射的更改。更糟糕的是,更改进程的地址 space 映射本身就是一项昂贵的操作。

映射整个文件 - 保留它映射的 - 并根据需要从映射中复制数据,或者使用 low-level 不依赖于任何的 IO 调用共享值或状态,例如 pread()。因为你已经有一个文件的打开文件描述符 fd ,所以像这样:

void getNextKeyValue() {
    char buffer[end-pos+1];
    ssize_t result = pread(fd, buffer, end-pos+1L, pos);
    assert(result == ( end-pos+1L ) );
    value.assign(buffer);
    morePairs = false;
}

请注意,您可以 而不是 使用 lseek() 然后 read() - 这不是原子操作 - 另一个线程可以在一个线程的文件指针之后修改文件指针调用 lseek() 但在调用 read().

之前

并且不要在每次调用 getNextKeyValue() 时使用 new/deletemalloc()/free() 作为缓冲区 - 同样有效 single-threads读取数据。

有几件事让我担心你的方法。首先,我无法想象所讨论的文件实际上是一个文本文件,因为如果您只使用原始指针调用 assign() 并且似乎以某种方式确定了大小,它似乎就可以工作。这意味着要么你嵌入了 NUL,要么你总是将所有内容复制到下一个 NUL(它可能在映射范围之外的某个地方)。无论如何,代码有味道!这也是为什么 cout << ptr "fails" 虽然 write(stdout, ptr, size) 工作,但在第一种情况下 strlen() 调用中出现缓冲区溢出的原因。

其次,你为什么要使用 mmap()?我想您希望在所需的大小或时间方面获得一些好处。然而,普通的旧 C++ IOStreams 可能会 mmap() 滑动 window 到文件本身,这是非常有效的。但是,您必须让 IOStreams 能够轻松地执行此操作:

// turn off some syncing that you shouldn't need
std::ios_base::sync_with_stdio(false);
// disable any character conversions
std::ifstream in;
in.imbue(std::locale::classic());
in.open(filename, std::ios_base::binary);

经典语言环境不包含任何 non-trivial 字符转换方面。这应该不是必需的,但将其明确化也无妨。二进制标志用于告诉流缓冲区它不应该执行任何 CR/LF 转换,这在 MS Windows 上尤其不同。

C++ IOStreams 的最后一个有点棘手的事情是定位。除了起始位置 (a default-constructed streampos),您只应该给从 tellp() 或 [= 收到的 seekp()seekg() 赋值21=]。但是,还是有希望的:如果没有任何字符转换,字节偏移量可能只适合您。如果没有,您仍然可以构建跟踪其周围位置的代码,尽管这可能有点棘手。我可以想象你甚至必须依赖 implementation-defined 这里的行为。