部分文本上的并发 mmap()
concurrent mmap() on a portion of text
我问了 关于如何读取从偏移量 pos
到偏移量 end
到 mmap()
的文本文件的问题。特别是文本文件由 多线程 使用以下代码读取:
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;
}
未报告的变量在其他地方声明和初始化。什么 顺便说一下,下面的代码读取了 整个 文本文件,而不是从偏移量 pos
到 end
。
上瘾时,程序在多个线程中突然终止(无错误输出),而在只有一个线程读取整个文件时正确终止。
更新:
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
(禁用 assert
s),那么您就得到了两个重叠的问题:
- 您没有检查
mmap
的 return 值(即使启用了 assert
,它也没有被正确检查;return 的错误 mmap
是 MAP_FAILED
,不是 NULL
)
- 你永远不会
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
/delete
或 malloc()
/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 这里的行为。
我问了 pos
到偏移量 end
到 mmap()
的文本文件的问题。特别是文本文件由 多线程 使用以下代码读取:
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;
}
未报告的变量在其他地方声明和初始化。什么 顺便说一下,下面的代码读取了 整个 文本文件,而不是从偏移量 pos
到 end
。
上瘾时,程序在多个线程中突然终止(无错误输出),而在只有一个线程读取整个文件时正确终止。
更新:
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
(禁用 assert
s),那么您就得到了两个重叠的问题:
- 您没有检查
mmap
的 return 值(即使启用了assert
,它也没有被正确检查;return 的错误mmap
是MAP_FAILED
,不是NULL
) - 你永远不会
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
/delete
或 malloc()
/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 这里的行为。