高级I/O之存储映射I/O
存储映射I/O(Memory-mapped I/O)使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区,则相应字节就自动地写入文件。这样就可以在不使用read和write的情况下执行I/O。
为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这是由mmap函数实现的。
#include <sys/mman.h> void *mmap(void *addr, size_t len, int prot, int flag, int filedes, off_t off); 返回值:若成功则返回映射区的起始地址,若出错则返回MAP_FAILED
addr参数用于指定映射存储区的起始地址。通常将其设置为0,这表示由系统选择该映射区的起始地址。此函数的返回地址是该映射区的起始地址。
filedes指定要被映射文件的描述符。在映射该文件到一个地址空间之前,先要打开该文件。len是映射的字节数。off是要映射字节在文件中的起始偏移量。
prot参数说明对映射存储区的保护要求,见表14-9。
表14-9 映射存储区的保护要求
prot | 说明 |
PROT_READ
PROT_WRITE PROT_EXEC PROT_NONE |
映射区可读
映射区可写 映射区可执行 映射区不可访问 |
可将prot参数指定为PROT_NONE,或者是PROT_READ、PROT_WRITE、PROT_EXEC任意组合的按位或。对指定映射存储区的保护要求不能超过文件open模式访问权限。例如,若该文件是只读打开的,那么对映射存储区就不能指定PROT_WRITE。
图14-11显示了一个存储映射文件。(见http://www.cnblogs.com/nufangrensheng/p/3508169.html中图7-3所示进程存储空间的典型安排情况。) 在此图中,“起始地址”是mmap的返回值。映射存储区位于堆和栈之间,这属于实现细节,各种实现之间可能不尽相同。
图14-11 存储映射文件的例子
flag参数影响映射存储区的多种属性:
MAP_FIXED 返回值必须等于addr。因为这不利于可移植性,所以不鼓励使用此标志。如果未指定此标志,而且addr非0,则内核只把addr视为在何处设置映射区的一种建议,但是不保证会使用所要求的地址。将addr指定为0可获得最大可移植性。
MAP_SHARED 这一标志说明了本进程对映射区所进行的存储操作的配置。此标志指定存储操作修改映射文件,也就是说,存储操作相当于对该文件的write。必须指定本标志或下一个标志(MAP_PRIVATE),但不能同时指定两者。
MAP_PRIVATE 本标志说明,对映射区的存储操作导致创建该映射文件的一个私有副本。所有后来对该映射区的引用都是引用该副本,而不是原始文件。(此标志的一种用途是用于调试程序,它将程序文件的正文部分映射至一存储区,但允许用户修改其中的指令。任何修改只影响程序文件的副本,而不影响原文件。
每种实现都可能还有另外一些MAP_xxx标志值,它们是这种实现所特有的。
off和addr的值(如果指定了MAP_FIXED)通常应当是系统虚存页长度的倍数。虚存页长可用带参数_SC_PAGESIZE或_SC_PAGE_SIZE的sysconf函数(见http://www.cnblogs.com/nufangrensheng/p/3496323.html)得到。因为off和addr常常指定为0,所以这种要求一般并不重要。
因为映射文件的起始偏移量受系统虚存页长度的限制,那么如果映射区的长度不是页长的整数倍时,将如何呢?假定文件长12字节,系统页长512字节,则系统通常提供512字节的映射区,其后的500字节被设置为0。可以修改这500字节,但任何变动都不会在文件中反映出来。于是,我们不能用mmap将数据添加到文件中。为了做到这一点,我们必须首先加长该文件,这将示于程序清单14-12中。
与映射存储区相关的有SIGSEGV和SIGBUS两个信号。信号SIGSEGV通常用于指示进程试图访问对它不可用的存储区。如果进程企图存数据到mmap指定为只读的映射存储区,那么也产生此信号。如果访问映射区的某个部分,而在访问时这一部分实际上已不存在,则产生SIGBUS信号。例如,用文件长度映射了一个文件,但在引用该映射区之前,另一个进程已将该文件截短。此时,如果进程企图访问对应于该文件已截去部分的映射区,则会收到SIGBUS信号。
在调用fork之后,子进程继承存储映射区(因为子进程复制父进程地址空间,而存储映射区是该地址空间的一部分),但是由于同样的理由,调用exec后的新程序则不继承存储映射区。
调用mprotect可以更改一个现存映射存储区的权限。
#include <sys/mman.h> int mprotect(void *addr, size_t len, int prot); 返回值:若成功则返回0,若出错则返回-1
prot的许可值与mmap中prot参数一样(表14-9)。地址参数addr的值必须是系统页长的整数倍。
如果在共享存储映射区中的页已被修改,那么我们可以调用msync将该页冲洗到被映射的文件中。msync函数类似于fsync(见http://www.cnblogs.com/nufangrensheng/p/3498760.html),但作用于存储映射区。
#include <sys/mman.h> int msync(void *addr, size_t len, int flags); 返回值:若成功则返回0,若出错则返回-1
如果映射是私有的,那么不修改被映射的文件。与其他存储映射函数一样,地址必须与页边界对齐。
flags参数使我们对如何冲洗存储区有某种程度的控制。我们可以指定MS_ASYNC标志以简化被写页的调度。如果我们希望在返回之前等待写操作完成,则可指定MS_SYNC标志。一定要指定MS_ASYNC和MS_SYNC中的一个。
MS_INVALIDATE是一个可选标志,使用它以通知操作系统丢弃与底层存储器没有同步的任何页。若使用了此标志,某些实现将丢弃在指定范围中的所有页,但这并不是所期望的。
进程终止时,或调用了munmap之后,存储映射区就被自动解除映射。关闭文件描述符filedes并不解除映射区。
#include <sys/mman.h> int munmap(caddr_t addr, size_t len); 返回值:若成功则返回0,若出错则返回-1
munmap不会影响被映射的对象,也就是说,调用munmap不会使映射区的内容写到磁盘文件上。对于MAP_SHARED区磁盘文件的更新,在写到存储映射区时按内核虚存算法自动进行。在解除了映射后,对于MAP_PRIVATE存储区的修改被丢弃。
实例
程序清单14-12用存储映射I/O复制一个文件(类似于cp(1)命令)。
程序清单14-12 用存储映射I/O复制文件
#include "apue.h" #include <fcntl.h> #include <sys/mman.h> int main(int argc, char *argv[]) { int fdin, fdout; void *src, *dst; struct stat statbuf; if(argc != 3) err_quit("usage: %s <fromfile> <tofile>", argv[0]); if((fdin = open(argv[1], O_RDONLY)) < 0) err_sys("can't open %s for reading", argv[1]); if((fdout = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, FILE_MODE)) < 0) err_sys("can't create %s for writing", argv[2]); if(fstat(fdin, &statbuf) < 0) /*need size of input file */ err_sys("fstat error"); /* set size of output file */ if(lseek(fdout, statbuf.st_size - 1, SEEK_SET) == -1) err_sys("lseek error"); if(write(fdout, "", 1) != 1) err_sys("write error"); if((src = mmap(0, statbuf.st_size, PROT_READ, MAP_SHARED, fdin, 0)) == MAP_FAILED) err_sys("mmap error for input"); if((dst = mmap(0, statbuf.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fdout, 0)) == MAP_FAILED) err_sys("mmap error for output"); memcpy(dst, src, statbuf.st_size); /* does the file copy */ exit(0); }
该程序首先打开两个文件,然后调用fstat得到输入文件的长度。在为输入文件调用mmap和设置输出文件长度时都需使用输入文件长度。调用lseek,然后写一个字节以设置输出文件的长度(可参考http://www.cnblogs.com/nufangrensheng/p/3498080.html)。如果不设置输出文件的长度,则对输出文件调用mmap也可以,但是对相关存储区的第一次引用会产生SIGBUS。也可使用ftruncate函数来设置输出文件的长度,但是并非所有系统都支持该函数扩充文件长度(见http://www.cnblogs.com/nufangrensheng/p/3502755.html)。
然后对每个文件调用mmap,将文件映射到存储区,最后调用memcpy将输入缓冲区的内容复制到输出缓冲区。从输入缓冲区(src)取数据字节时,内核自动读输入文件;在将数据存入输出缓冲区(dst)时,内核自动将数据写到输出文件中。
数据被写入文件的确切时间依赖于系统的页管理算法。某些系统设置了守护进程,在系统运行期间,它“慢条斯理”地将脏页写到磁盘上。如果想要确保数据安全地写到文件中,则需在进程终止前以MS_SYNC标志调用msync。
将一个普通文件复制到另一个普通文件中时,存储映射I/O比较快。但是有一些限制,例如,不能用其在某些设备(例如网络设备或终端设备)之间进行复制,并且在对被复制的文件进行映射后,也要注意该文件的 长度是否改变。
本篇博文内容摘自《UNIX环境高级编程》(第二版),仅作个人学习记录所用。关于本书可参考:http://www.apuebook.com/。