通过文件读写和内存映射两种方式将点云输出为pcd格式文件
一、两种方法将点云输出为pcd格式的速度比较
点云数量 | I/O耗时(毫秒ms) | mmap+memcpy耗时(毫秒ms) |
---|---|---|
1千(1000) | 0.679 | 0.086 |
1万(10000) | 2.489 | 0.869 |
10万(100000) | 7.223 | 1.890 |
100万(1000000) | 68.235 | 16.602 |
从不同数量级点云的测试结果中,可以看到内存映射+内存拷贝的方法更快。
造成速度差异的原因:
1.常规文件I/O:
为了提高读写效率和保护磁盘,使用了页缓存机制,读文件时需要先将文件页从磁盘拷贝到页缓存中。由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需将页缓存中的数据页再次拷贝到内存对应的用户空间中。要通过两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。
2.内存映射加内存拷贝mmap+memcpy:
mmap内存映射时,首先创建新的虚拟内存区域,然后建立起文件磁盘地址和虚拟内存区域的映射,这两步中没有任何文件拷贝操作。之后访问数据时,发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,从磁盘中将数据传入内存的用户空间中,供进程使用。
总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。 而mmap操控文件,实现了用户空间和内核空间的数据直接交互,用内存读写取代I/O读写,从磁盘到用户主存,只需要一次数据拷贝,对文件的读取操作跨过了页缓存,减少了一次数据拷贝,提高了文件操作效率。
二、mmap简介
1.mmap相关概念
mmap()系统调用,在调用进程的虚拟地址空间中创建一个新内存映射。映射分为两种:
1.文件映射(内存映射文件)
将一个文件的一部分直接映射到调用进程的虚拟内存中。一旦一个文件被映射之后就可以通过在相应的内存区域中操作字节来访问文件内容了。映射的分页会在需要的时候从文件中自动加载。这种映射也被称为,基于文件的映射,或内存映射文件。
2.匿名映射
一个匿名映射没有对应的文件,相反,这种映射的分页会被初始化为0。可以把它看成是一个内容总是被初始化为0的虚拟文件映射。
一个进程的映射中的内存,可以与其他进程中的映射共享,即各个进程的页表条目指向RAM中相同分页,这种行为会在以下两种情况下发生:
情况一:当两个进程映射了一个文件的同一个区域时,它们会共享物理内存的相同分页。
情况二:通过fork()创建的子进程会继承父进程的映射的副本,并且这些映射所引用的物理内存分页与父进程中相应映射所引用的分页相同。
当两个或更多个进程共享相同分页时,每个进程都有可能会看到其他进程对分页内容做出的变更,当然这要取决于映射是私有的还是共享的。
1.私有映射(MAP_PRIVATE)
在映射内容上发生的变更,对其他进程不可见。对于文件映射来讲,变更将不会在底层文件上进行,尽管一个私有映射的分页在上面介绍的情况中初始时是共享的,但对映射内容所做出的变更对各个进程来将则是私有的。内核使用了写时复制(copy-on-write)技术完成了这个任务。这意味着,每当一个进程试图修改一个分页的内容时,内核首先会为该进程创建一个新分页,并将需要修改的分页中的内容复制到新分页中,以及调整进程的页表。正因为这个原因,MAP_PRIVATE映射,也被称为私有写时复制映射。
2.共享映射(MAP_SHARED)
在映射内容上发生变更,对所有共享同一个映射的其他进程都可见,对文件映射来讲,变更将会发生在底层的文件上。
以上四种不同的方式(文件、匿名、私有、共享)可以组合实现如下效果:
1.私有文件映射(文件 + 私有)-> 进程初始化
映射的内容被初始化为一个文件区域中的内容,多个映射同一个文件的进程初始时会共享同样的内存物理分页,但系统使用写时复制技术,使得一个进程对映射的修改对其他进程不可见。这种映射的主要用途是,使用一个文件的内容来初始化一块内存区域。比如,根据二进制可执行文件,或共享库文件的相应部分来初始化一个进程的文本和数据段。
2.私有匿名映射(匿名 + 私有)-> malloc大块内存
每次调用mmap()创建一个私有匿名映射时都会产生一个新映射,该映射与同一(或不同)进程创建的其他匿名映射是不同的,即不会共享物理分页。尽管子进程会继承父进程的映射,但写时复制语义确保在fork()之后父进程和子进程不会看到其他进程对映射所做出的修改。私有匿名映射的主要用途是,为一个进程分配新内存(用0填充),例如,在分配大块内存时,malloc()会为此而使用mmap()。
3.共享文件映射(文件 + 共享)-> 无关进程IPC
所有映射一个文件的同一区域的进程会共享同样的内存物理分页,这些分页的内容将被初始化为该文件区域。对映射内容的修改将直接在文件中进程。这种映射主要用于两个用途:第一,它允许内存映射I/O,这表示一个文件会被加载到进程的虚拟内存中的一个区域中,并且对该块内容的修改会自动写入到这个文件中,因此,内存映射I/O为使用read()和write()来执行文件I/O这种做法提供了一种替代方案。第二,允许无关进程共享一块内容,以便以一种类似于System V共享内存段的方式来执行快速IPC。
4.共享匿名映射(匿名 + 共享)-> 相关进程IPC
与私有匿名映射一样,每次调用mmap()创建一个共享匿名映射时,都会产生一个新的,与任何其他映射不共享分页的截然不同的映射。这里的差别在于,映射的分页不会被写时复制,这意味着,当一个子进程在fork()之后继承映射时,父进程和子进程共享同样的RAM分页,并且一个进程对映射内容所做出的变更会对其他进程可见。共享匿名映射允许以一种类似于System V共享内存段的方式来进行IPC,但只有相关进程之间才能这么做。
2.mmap的头文件、函数原型、以及参数描述:
(1)头文件和函数原型
//头文件:
#include <unistd.h>
#include <sys/mman.h>
//函数原型:
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
(2)参数描述
1)addr
参数指定了映射被放置的虚拟地址。
如果将addr指定为NULL,那么内核会为映射选择一个合适的地址,这是创建映射的首选做法。或者在addr中指定了一个非NULL值时,内核会在选择将映射放置在何处时将这个参数值作为一个提示信息来处理。在实践中,内核至少会将指定的地址舍入到最近的一个分页边界处。不管采用何种方式,内核会选择一个不与任何既有映射冲突的地址。
2)length
参数指定了映射的字节数。
尽管length无需是一个系统分页大小(sysconf(_SC_PAGESIZE)返回值)的倍数,但内核会以分页大小为单位来创建映射,因此实际上length会被向上提升为分页大小的下一个倍数。
3)prot
参数是一个位掩码。
它指定了施加于映射之上的保护信息,其取值要么是PROT_NONE,要么是下面列出的其他三个标记的组合(取OR):
Value | Des |
---|---|
PROT_NONE | 区域无法访问 |
PROT_READ | 区域内容可读取 |
PROT_WRITE | 区域内容可修改 |
PROT_EXEC | 区域内容可执行 |
4)flags
参数是一个控制映射操作各个方面的选项的位掩码。这个掩码必须只包含下列值中的一个:
Value | Des |
---|---|
MAP_PRIVATE | 创建一个私有映射。区域中内容上所发生的变更对使用同一映射的其他进程是不可见的。对于文件映射来讲,所发生的变更将不会反应在底层文件上。 |
MAP_SHARED | 创建一个共享映射。区域中内容上所发生的变更对使用MAP_SHARED特性映射同一区域的进程是可见的。对于文件映射来讲,所发生的变更将直接反应在底层文件上。对文件的更新将无法确保立即生效,具体可参考对msync()系统调用。 |
other | 除了MAP_PRIVATE和MAP_SHARED之外,在flags中还可以有选择地对其他标记取OR |
fd
和offset
是用于文件映射的(匿名映射将忽略它们):
5)fd
是一个标识被映射的文件的文件描述符。
6)offset
指定了映射在文件中的起点,它必须是系统分页大小的倍数。要映射整个文件就需要将offset
指定为0,并且将length
指定为文件大小。
3.mmap+memcpy将点云输出为pcd格式文件的步骤:
(1) 调用open打开(新建)文件, 并返回描述符fd.
(2) 用mmap建立内存映射, 并返回映射首地址指针start.
(3) 用memcpy和指针,将点云中的数据信息拷贝到映射到的内存中.
(4) 用munmap(void *start, size_t lenght)关闭内存映射.
(5) 调用close关闭文件fd.
4.mmap使用细节
1.使用mmap需要注意的一个关键点是,mmap映射区域大小必须是物理页大小(page_size)的整倍数(与操作系统以及系统的位数相关,32位系统为4k(4096))。原因是,内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。为了匹配内存的操作,mmap从磁盘到虚拟地址空间的映射也必须是页。
2.内核可以跟踪被内存映射的底层对象(文件)的大小,进程可以合法的访问在当前文件大小以内又在内存映射区以内的那些字节。也就是说,如果文件的大小一直在扩张,只要在映射区域范围内的数据,进程都可以合法得到,这和映射建立时文件的大小无关。具体情形参见“情形三”。
3.映射建立之后,即使文件关闭,映射依然存在。因为映射的是磁盘的地址,不是文件本身,和文件句柄无关。同时可用于进程间通信的有效地址空间不完全受限于被映射文件的大小,因为是按页映射。
文件映射的三种情况(以32位操作系统中的物理页大小为例):
1.一个文件的大小是5000字节,mmap函数从一个文件的起始位置开始,映射5000字节到虚拟内存中。
分析:因为单位物理页面的大小是4096字节,虽然被映射的文件只有5000字节,但是对应到进程虚拟地址区域的大小需要满足整页大小,因此mmap函数执行后,实际映射到虚拟内存区域8192个 字节,5000~8191的字节部分用零填充。映射后的对应关系如下图所示:
此时:
(1)读/写前5000个字节(0~4999),会返回操作文件内容。
(2)读字节5000~8191时,结果全为0。写5000~8191时,进程不会报错,但是所写的内容不会写入原文件中 。
(3)读/写8192以外的磁盘部分,会返回一个SIGSECV错误。
2.一个文件的大小是5000字节,mmap函数从一个文件的起始位置开始,映射15000字节到虚拟内存中,即映射大小超过了原始文件的大小。
分析:由于文件的大小是5000字节,和情形一一样,其对应的两个物理页。那么这两个物理页都是合法可以读写的,只是超出5000的部分不会体现在原文件中。由于程序要求映射15000字节,而文件只占两个物理页,因此8192字节~15000字节都不能读写,操作时会返回异常。映射后的对应关系如下图所示:
此时:
(1)进程可以正常读/写被映射的前5000字节(0~4999),写操作的改动会在一定时间后反映在原文件中。
(2)对于5000~8191字节,进程可以进行读写过程,不会报错。但是内容在写入前均为0,另外,写入后不会反映在文件中。
(3)对于8192~14999字节,进程不能对其进行读写,会报SIGBUS错误。
(4)对于15000以外的字节,进程不能对其读写,会引发SIGSEGV错误。
3.一个文件初始大小为0,使用mmap操作映射了1000*4K的大小,即1000个物理页大约4M字节空间,mmap返回指针ptr。
分析:如果在映射建立之初,就对文件进行读写操作,由于文件大小为0,并没有合法的物理页对应,如同情形二一样,会返回SIGBUS错误。但如果每次操作ptr读写前,先增加文件的大小,那么ptr在文件大小内部的操作就是合法的。例如,文件扩充4096字节,ptr就能操作ptr ~ [ (char)ptr + 4095]的空间。只要文件扩充的范围在1000个物理页(映射范围)内,ptr都可以对应操作相同的大小。这样方便随时扩充文件空间,随时写入文件,不造成空间浪费。
二、I/O和mmap+memcpy测试代码
1.I/O(文件读写)
#include<fstream>
#include<iostream>
#include<string>
#include<vector>
#include<ctime>
#include<algorithm>
#include<cstdlib>
using namespace std;
//自定义点的结构
struct Point
{
float x;
float y;
float z;
float empty;
int line;
int intensity;
Point(float _x, float _y, float _z, int _line, int _intensity) : x(_x), y(_y), z(_z), line(_line), intensity(_intensity) {}
};
void makepcd(const string &file_name, const vector<Point> &points)
{
//----------------打开文件,文件名不存在就创建新文件
ofstream fout(file_name, ios::binary);
int points_num = points.size();
//----------------开始写入数据头,描述点云结构
fout << "# .PCD v0.7 - Point Cloud Data file format\n";
fout << "VERSION 0.7\n";
fout << "FIELDS x y z line intensity\n";
fout << "SIZE 4 4 4 4 4\n";
fout << "TYPE F F F I I\n";
fout << "COUNT 1 1 1 1 1\n";
fout << "WIDTH " << points_num << "\n";
fout << "HEIGHT " << 1 << "\n";
fout << "VIEWPOINT 0 0 0 1 0 0 0\n";
fout << "POINTS " << points_num << "\n";
fout << "DATA binary\n";
fout.close();
//----------------开始写入二进制点云数据流
ofstream bfout(file_name, ios::binary|ios::ate|ios::in);
for(const auto &p : points)
{
bfout.write((char *)(&p.x), sizeof(float));
bfout.write((char *)(&p.y), sizeof(float));
bfout.write((char *)(&p.z), sizeof(float));
bfout.write((char *)(&p.line), sizeof(int));
bfout.write((char *)(&p.intensity), sizeof(int));
}
bfout.close();
//----------------写入完成
}
int main()
{
clock_t start, finish;
double duration;
vector<Point> points;
srand(time(0));
for(int i = 1000; i > 0; --i)
{
int x = rand() % 100;
int y = rand() % 100;
int z = rand() % 100;
int intensity = rand() % 100;
int line = rand() % 1000;
points.push_back(Point(x,y,z,line,intensity));
}
start = clock();
makepcd("binary_write.pcd", points);
finish = clock();
duration = (double)(finish- start) / CLOCKS_PER_SEC;
printf("I/O solve %d points total use %f ms\n", (int)points.size(), duration * 1000);
return 0;
}
2.mmap+memcpy(内存映射+内存拷贝)
#include <ctime>
#include <cstdlib>
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
#include <string>
#include <sstream>
#include <vector>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
using namespace std;
//自定义单个点结构包含的信息字节大小
#define FIELD_SIZE 20;
//自定义的点的结构
struct Point
{
float x;
float y;
float z;
float empty;
int line;
int intensity;
Point() {}
Point(float _x, float _y, float _z, int _line, int _intensity) : x(_x), y(_y), z(_z), line(_line), intensity(_intensity) {}
~Point() {}
};
string generateHeader(const vector<Point> &cloud)
{
ostringstream oss;
oss.imbue(std::locale::classic());
//-------------------生成点云头部描述信息
oss << "# .PCD v0.7 - Point Cloud Data file format\n"
<<"VERSION 0.7\n"
<<"FIELDS x y z line intensity\n"
<<"SIZE 4 4 4 4 4\n"
<<"TYPE F F F I I\n"
<<"COUNT 1 1 1 1 1\n"
<<"WIDTH " << cloud.size()
<<"\nHEIGHT 1"
<<"\nVIEWPOINT 0 0 0 1 0 0 0\n"
<<"POINTS " << cloud.size()
<<"\nDATA binary\n";
return (oss.str());
}
//-----------封一下open函数和close函数,用于接收返回值,可根据返回值输出错误信息
inline int raw_open(const char * pathname, int flags, int mode) { return ::open(pathname, flags, mode); }
inline int raw_open(const char * pathname, int flags) { return ::open(pathname, flags); }
inline int raw_close(int fd) { return ::close(fd); }
int writeBinary (const string file_name, const vector<Point> &cloud)
{
if(cloud.empty ())
{
cout << "Input point cloud has no data!" << endl;
return (-1);
}
int data_idx = 0;
std::ostringstream oss;
oss << generateHeader(cloud);//将头部描述信息加入流
oss.flush();
//获取描述信息字节大小data_idx,后续作为头部信息内存拷贝的大小参数
data_idx = static_cast<int>(oss.tellp());
int fd = raw_open(file_name.c_str(), O_RDWR | O_CREAT | O_TRUNC, S_IRWXG | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);//打开一个新文件,获得文件描述符fd
if(fd < 0)
{
cout << "[pcl::PCDWriter::writeBinary] Error during open!" << endl;
return (-1);
}
//data_size为点云xyz坐标等数据的大小
size_t data_size = cloud.size() * FIELD_SIZE;
//对应于上文(二、4.使用细节)中的情况3,因为open打开的是一个大小为0的新文件,扩充在磁盘中的空间后,才能对映射的内存进行读写操作,否则文件没有对应合法的物理页。
//fallocate函数的作用是扩充文件在磁盘中的空间大小
//第一个参数为open()函数返回的文件描述符,指定了作用于哪个文件
//第二个参数mode,指定扩充方式
//第三个参数offset,指定偏移量
//第四个参数为size,指定扩充大小
::fallocate(fd, 0, 0, data_idx + data_size);
//将扩充后的文件映射到虚拟内存,返回映射后内存的首地址map
char* map = static_cast<char*>(::mmap(nullptr, data_idx + data_size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED, fd, 0));
// Copy header
memcpy(&map[0], oss.str().c_str(), data_idx);
//所有操作结束后,关闭映射需要map,所以用新指针out作为后续memcpy操作的指针
char* out = &map[0] + data_idx;
// Copy data
for(const auto &point : cloud)
{
memcpy(out, (&point.x), 4);
out += 4;
memcpy(out, (&point.y), 4);
out += 4;
memcpy(out, (&point.z), 4);
out += 4;
memcpy(out, (&point.line), 4);
out += 4;
memcpy(out, (&point.intensity), 4);
out += 4;
}
//关闭映射,第一个参数为内存中首地址,第二个参数为映射大小
::munmap(map, (data_idx + data_size));
raw_close(fd);
return 0;
}
int main()
{
int begin, end;
vector<Point> points;
srand(time(0));
for(int i = 100000; i > 0; --i)
{
int x = rand() % 100;
int y = rand() % 100;
int z = rand() % 100;
int intensity = rand() % 100;
int line = rand() % 1000;
points.push_back(Point(x,y,z,line,intensity));
}
begin = clock();
writeBinary("mmap_binary.pcd", points);
end = clock();
cout << "to_pcd: " << end - begin << endl;
return 0;
}