一个进程拥有独立并且连续虚拟地址空间,在32位体系结构中进程的地址空间是4G。不过,内核在管理进程的地址空间时是以内存区域为单位。内存区域是进程整个地址空间中一个独立的内存范围,它在内核中使用vm_area_struct数据结构来描述。每个内存区域都有自己访问权限以及操作函数,因此进程只能对有效范围的内存地址进行访问。
存储映射I/O是一种基于内存区域的高级I/O操作,它将磁盘文件与进程地址空间中的一个内存区域相映射。当从这段内存中读数据时,就相当于读磁盘文件中的数据,将数据写入这段内存时,则相当于将数据直接写入磁盘文件。这样就可以在不使用基本I/O操作函数read和write的情况下执行I/O操作。
1.基本实现方法
实现存储映射I/O的核心操作是通过mmap系统调用将一个给定的磁盘文件映射到一个存储区域中
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
关于该函数定义中各个参数说明Linux上的man手册已经解释的很清楚,在此不再赘述。这里需要特别说明的是prot和flags参数。prot用来指定对映射区域的保护要求,但是它的保护范围不能超过文件open时指定的打开权限。比如以只读(PROT_READ)方式打开一个文件,那么以读写(PROT_READ|PROT_WRITE)方式保护内存区域是不合法的。flags用来指定内存区域的多种属性,两个典型的取值是MAP_SHARED和MAP_PRIVATE。MAP_SHARED标志指定了进程对内存区域的修改会影响到映射文件。而当对flags指定MAP_PRIVATE时,进程会为该映射内存区域创建一个私有副本,对该内存区的所有操作都是在这个副本上进行的,此时对内存区域的修改并不会影响到映射文件。
下面列出一个简单的示例程序,它将磁盘文件映射到一个内存区域中,通过mmap返回的指针先读文件,再写文件。可以看到对文件的读和写操作都是通过内存映射I/O的方式完成的。
int main()
{
int fd;
char *buf = NULL;
int i;
//打开一个文件
if (-1 == (fd = open("./mapping_file.txt", O_RDWR))) {
printf("open file error!\n");
exit(1);
}
//将文件映射到进程的一个内存区域
buf = mmap(NULL, 100, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (!buf) {
printf("mmap error!\n");
exit(1);
}
//对映射内存读数据
for (i = 0; i < 100; i++)
printf("%c", buf[i]);
//对映射内存写数据
if (buf[0] == 'H')
buf[0] = 'h';
else
buf[0] = 'H';
system("cat ./mapping_file.txt");
return 0;
}
2.使用内存映射I/O进行文件拷贝
使用基本I/O操作函数如何实现一个类似cp命令的程序?比如我们要将A文件复制到B文件,那么程序的基本框架是这样的:
1.open()文件A和文件B
2.将A文件的内容read()到buffer
3.将buffer中的数据write()到文件B
4.close()文件A和文件B
如果使用内存映射I/O来实现cp命令,那么它的基本框架是这样的:
1.open()文件A和文件B
2.mmap()文件A和文件B,其中src和dest分别为两个文件映射到内存的地址
3.将以src为起始的len长字节数据memcpy()到dest
4.close()文件A和文件B
示例程序如下:
int main()
{
int srcfd, destfd;
struct stat statbuf;
char *src = NULL, *dest = NULL;
//打开两个文件
if (-1 == (srcfd = open("./src.txt", O_RDONLY))) {
printf("open src file error!\n");
exit(1);
}
if (-1 == (destfd = open("./dest.txt", O_RDWR | O_CREAT | O_TRUNC))) {
printf("open dest file error!\n");
exit(1);
}
//获取原始文件的长度
if (-1 == fstat(srcfd, &statbuf)) {
printf("fstat src file error!\n");
exit(1);
}
//设置输出文件的大小
if (-1 == lseek(destfd, statbuf.st_size - 1, SEEK_SET)) {
printf("lseek error!\n");
exit(1);
}
if (-1 == write(destfd, "", 1)) {
printf("write error!\n");
exit(1);
}
if ((src = mmap(0, statbuf.st_size, PROT_READ, MAP_SHARED, srcfd, 0)) == MAP_FAILED) {
printf("mmaping src file error!\n");
exit(1);
}
if ((dest = mmap(0, statbuf.st_size + 2, PROT_READ | PROT_WRITE, MAP_SHARED, destfd, 0)) == MAP_FAILED) {
printf("mmaping dest file error!\n");
exit(1);
}
memcpy(dest, src, statbuf.st_size);
printf("src file:\n");
system("cat ./src.txt");
printf("dest file:\n");
system("cat ./dest.txt");
close(srcfd);
close(destfd);
return 0;
}
按照上述列出的基本框架,该程序首先打开两个文件,通过fstat()获得源文件的长度。因为在mmap两个文件以及设置目的文件长度时都需要源文件的长度。设置目的文件通过lseek()即可完成,如果没有设置目的文件的长度,那么将会产生总线错误(引发信号SIGBUS)。然后分别mmap()两个文件到进程的地址空间,最后调用memcpy()将源文件内存区的数据拷贝到目的文件内存区。
通过基本I/O和内存映射I/O均可以进行文件拷贝,那么两者的效率谁更高一些?这其实是个很难回答的问题。不管是使用基本I/O操作函数还是mmap方式,操作系统都会在内存中进行缓存(cache),而且在不同的应用场景、不同的平台下结果都会收到影响。但是抛开这些因素单从对文件操作这个方面来说,内存映射方式比read和write方式要快。
如果使用read/write方式进行文件拷贝,首先将数据从用内核缓冲区复制到用户空间缓冲区,这是read的过程;再将数据从用户空间缓冲区复制到内核缓冲区,这是write过程。如果是内存映射方式,则直接是用户空间中数据的拷贝,也就是将源文件所映射内存中的数据拷贝到目的文件所映射的内存中。这样就避免了用户空间和内核空间之间数据的来回拷贝。
但是内存映射方式并不是完美的,它所映射的文件只能是固定大小,因为文件所映射的内存区域大小在mmap时通过len已经指定。另外,文件映射的内存区域的大小必须以页大小为单位。比如系统页大小为4096字节,假定映射文件的大小为20字节,那么该页剩余的4076字节全部被填充为0。虽然通过映射地址可以访问并修改剩余字节,但是任何变动都不会在映射文件中反应出来。由此可见,使用内存映射进行大数据量的拷贝比较有效