普通读写文件方式的缺点
使用文件IO的read/write来进行文件的普通读写时,函数经过层层的调用后,才能够最终操作到文件,以读(read函数)为例:
上图把驱动层的代码算在OS层了,以上画的只是一个示意的过程,并不代表函数就叫这个名字。在读写时中间会有很多的调用过程,数据需要在不同的缓存间倒腾,最终才能从文件到应用缓存,或者从应用缓存到文件,效率很低。
疑问:为什么中间经过一系列的捣腾后效率会很低呢?
答:
(1)cpu执行一堆的函数,很耗费cpu资源,而且浪费时间
(2)中间一堆的缓存都是函数从内存开辟的,浪费内存资源,而且数据在各缓存间倒腾时也很耗费时间
疑问:read、write这种普通读写文件的方式效率不高,那效率不高还要它干嘛?
答:因为对于数据量较少的情况来说,这种普通读写方式还是非常方便的,而且数据量较少时,效率并不会太低,只有当数据量非常大时,效率的影响才会非常的明显。所以对于数据量很少的情况来说,我们最常用的还是普通的读写方式。
疑问:使用标准IO函数的读和写,中间过程也很多吗?
答:当然,因为“标准io”本来就是封装文件IO来实现的。
mmap的原理
mmap是memory map的缩写,意思是存储映射。以映射普通文件为例:
既然直接read、write很费劲,那我干脆抛弃read、write的操作,mmap采用直接映射的方式实现,mmap映射时,其实就会将普通文件的硬盘空间的物理地址映射到进程空间的虚拟地址。
通常情况下,进程空间的虚拟地址只映射自己底层物理空间的物理地址,但是使用mmap时,他会将文件的硬盘空间的地址也映射到虚拟地址空间,这么一来应用程序就可以直接通过映射的虚拟地址操作文件,根本就不需要read、write函数了,使用地址操作时省去了繁杂的中间调用过程,可以快速对文件进行大量数据的输入输出。
疑问:使用存储映射时,read、write被省掉了,open是不是也被省掉了?
答:open不能省,必须要将文件open后,才能使用mmap进行映射。
疑问:映射时,具体映射到了进程空间的什么位置呢?
答:映射到了“进程应用空间”堆和栈中间那片虚拟地址的位置。
进程空间实际上是虚拟内存空间,这块虚拟内存空间又分为内核空间,应用空间
(1)进程内核空间:用于映射“OS”所在的物理内存空间
(2)进程应用空间:用于映射“应用程序”所在的物理内存空间
对比IPC之共享内存
存储映射与共享内存在原理上很像,都是进程虚拟内存空间向外做映射
共享内存
原理
共享内存是让不同的进程空间映射到同一片物理内存上,然后通过共享的物理内存来实现进程间通信。
过程
①shmget:创建共享内存,其实就是从物理内存中划出一片准备用于共享的空间出来
②shmat:调用该函数的进程,将自己的进程空间的某片虚拟地址映射到共享内存上。程序员不需要知道共享内存空间的起始物理地址是多少,shmat它会知道。至于说映射后的起始虚拟地址一般也是由shmat自己选择的,程序员不需要干预。
③shmdt:取消映射
④shmctl:将被取消映射的共享内存删除(释放)
对比存储映射和共享内存
1)存储映射,其实也可以用来实现进程间通信
比如A和B进程都映射到同一个普通文件上,这时A进程往里写数据,B进程从里面读数据,反过来也是一样的,如此就实现了进程间的通信。但是这顶多只算是广义上的通信,所谓广义上的通信就是,只要不是OS提供专门的IPC,就不是专门的进程间通信,只能算是广义的IPC。实际上,我们也不会使用mmap映射普通文件来实现进程间通信,因为操作硬盘的速度相比操作内存来说低了很多,如果你想实现进程间大量数据通信的话,完全可以使用与存储映射原理类似的“共享内存”来实现,而且速度很快。
2)虽然存储映射和共享内存原理相似,但是各自用途不同
(a)共享内存 实现进程间大量数据通信(共享)。
(b)存储映射 对文件进行大量数的高效输入输出。
API
mmap函数
原型
#include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
功能
将文件所在的磁盘空间映射到进程空间。
参数
addr:人为指定映射的起始虚拟地址
如果设置为NULL,表示由内核决定映射的起始虚拟地址,这也是最常见的设置方式,这与我们调用shmat映射共享内存时指定NULL是一样的。
如果设置不为NULL,就表示由自己指定,指定的起始虚拟地址必须是虚拟页(4k)的整数倍,这与自己指定shmat的映射起始虚拟地址也是一样的。
length:映射长度,也就是你想对文件映射多长。
prot:指定对映射区的操作权限,可指定如下宏:
PROT_EXEC:映射区的内容可执行。
如果你映射的是普通文件是一个可执行文件的话,将映射权限指定为PROT_EXEC后,是能够通过映射后的虚拟地址去执行文件中的“指令”。
PROT_READ:映射区的内容可读。
ROT_WRITE:映射区的内容可写
以上三种选项可相互 | 操作。比如:PROT_EXEC | PROT_READ
PROT_NONE:映射区不允许访问(不允许执行、读、写),一般不会指定这个,如果指定为不可访问的话,映射就没有意义了。
flags:向映射区写入了数据,是否将数据立即更新到文件中。
(a)MAP_SHARED:立即更新。
fd:需要被映射文件的描述符。
offset:表示从文件头的offset处开始映射。一般都指定为0,表示从文件头开始映射。
返回值
调用成功,返回映射的起始虚拟地址,失败则返回(void*)-1,errno被设置。
munmap函数
原型
#include <sys/mman.h> int munmap(void *addr, size_t length);
功能
取消映射
参数
addr:映射的起始虚拟地址
length:需要取消的长度
返回值
调用成功返回0, 失败则-1, errno被设置。
代码演示
写一个例子程序,将A文件的大量数据复制到B文件中。如果采用传统方式,使用read函数从A文件读出数据,然后向B文件write,如果数据量很大的话,复制的效率会非常低,此时我们就可以使用存储映射来实现。
mmap映射文件size为0的文件时,会映射失败,映射失败时内核会向进程发送一个SIGBUS信号,提示mmap失败了,这个信号的默认处理方式是终止,所以当进程收到这个信号时就被异常终止了。如果你不想被这个信号终止,你可以自己忽略或者屏蔽这个信号,一般来说我们不需要忽略和屏蔽该信号。
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <string.h> 5 #include <strings.h> 6 #include <errno.h> 7 #include <sys/mman.h> 8 #include <sys/types.h> 9 #include <sys/stat.h> 10 #include <fcntl.h> 11 12 13 14 void print_err(char *str, int line, int err_no) 15 { 16 printf("%d, %s: %s\n", line, str, strerror(err_no)); 17 exit(-1); 18 } 19 20 int main(void) 21 { 22 int srcfd = -1; 23 int dstfd = -1; 24 void *srcaddr = NULL; 25 void *dstaddr = NULL; 26 27 28 /* 打开源文件 */ 29 srcfd = open("./file_lock.h", O_RDWR); 30 if(srcfd == -1) print_err("open file_lock.h fial", __LINE__, errno); 31 32 /* 打开目标文件 */ 33 dstfd = open("./file", O_RDWR|O_CREAT|O_TRUNC, 0664); 34 if(dstfd == -1) print_err("open file fial", __LINE__, errno); 35 36 /* mmap映射源文件 */ 37 struct stat src_stat = {0}; 38 fstat(srcfd, &src_stat);//获取文件属性(主要是想得到文件的size) 39 srcaddr = mmap(NULL, src_stat.st_size, PROT_READ, MAP_SHARED, srcfd, 0); 40 if(srcaddr == (void *)-1) print_err("mmap srcfile fail", __LINE__, errno); 41 42 /* mmap映射目标文件 */ 43 ftruncate(dstfd, src_stat.st_size); 44 /* 45 参数1:指定映射的起始虚拟地址,如果制定NULL表示由mmap指定 46 参数2: 要映射的长度 47 参数3:指定映射后的操作权限,PROT_WRITE/PROT_READ/PROT_EXEC/PROT_NONE 48 参数4:是否立即更新到文件中,指定MAP_SHARED,表示理解更新 49 参数5:你要映射的那个文件的fd 50 参数6:指定一个偏移,表示你要从文件的什么位置开始映射 */ 51 dstaddr = mmap(NULL, src_stat.st_size, PROT_WRITE, MAP_SHARED, dstfd, 0); 52 if(dstaddr == (void *)-1) print_err("mmap dstfile fail", __LINE__, errno); 53 54 55 /* 想办法讲源文件的数据复制到目标文件中 */ 56 memcpy(dstaddr, srcaddr, src_stat.st_size); 57 58 59 return 0; 60 } 61