文章目录
@[TOC] 1.引入
1.1 标准I/O
1.2直接I/O
1.3 mmap
1.引入
让我们先看一下Linux系统下IO结构模型
从图中可以看到标准 I/O、mmap、直接 I/O 这三种 I/O 方式在流程上的差异
1.1 标准I/O
大多数文件系统的默认I/O操作都是标准I/O。在Linux的缓存I/O机制中,数据先从磁盘复制到内核空间的缓冲区,然后从内核空间缓冲区复制到应用程序的地址空间。
读操作:操作系统检查内核的缓冲区有没有需要的数据,如果已经缓存了,那么就直接从缓存中返回;否则从磁盘中读取,然后缓存在操作系统的缓存中。
写操作:将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说写操作就已经完成,至于什么时候再写到磁盘中由操作系统决定,除非显示地调用了sync等同步命令。
缓存I/O的优点:1)在一定程度上分离了内核空间和用户空间,保护系统本身的运行安全;2)可以减少读盘的次数,从而提高性能。
缓存I/O的缺点:数据在传输过程中需要在应用程序地址空间和缓存之间进行多次数据拷贝操作,这些数据拷贝操作所带来的CPU以及内存开销是非常大的。
下面我们read()操作为例,下图是read操作时流程
它会导致数据先从磁盘拷贝到 Page Cache 中,然后再从 Page Cache 拷贝到应用程序的用户空间,这样就会多一次内存拷贝。系统这样设计主要是因为内存相对磁盘是高速设备,即使多拷贝 100 次,内存也比真正读一次硬盘要快。
写操作
前面提到写操作讲数据从用户控件复制到内核空间的缓存中,数据什么时候写到磁盘由应用程序采用的写操作机制决定,默认是采用延迟写机制,应用程序只需要将数据写到页缓存就可以了,完全不需要等待数据全部被写入磁盘,系统会负责定期将页缓存数据写入磁盘。
从中可以看出来,缓存 I/O 可以很大程度减少真正读写磁盘的次数,从而提升性能。但是延迟写机制可能会导致数据丢失,那系统究竟会在什么时机真正把页缓存的数据写入磁盘呢?
Page Cache 中被修改的内存称为“脏页”,内核通过 flush 线程定期将数据写入磁盘。具体写入的条件我们可以通过 /proc/sys/vm 文件或者sysctl -a | grep vm 命令得到。
在实际应用中,如果某些数据我们觉得非常重要,是完全不允许有丢失风险的,这个时候我们应该采用同步写机制。在应用程序中使用 sync、fsync、msync 等系统调用时,内核都会立刻将相应的数据写回到磁盘。
1.2直接I/O
直接IO就是应用程序直接访问磁盘数据,而不经过内核缓冲区,这样做的目的是减少一次从内核缓冲区到用户程序缓存的数据复制。比如说数据库管理系统这类应用,它们更倾向于选择它们自己的缓存机制,因为数据库管理系统往往比操作系统更了解数据库中存放的数据,数据库管理系统可以提供一种更加有效的缓存机制来提高数据库中数据的存取性能。
直接IO的缺点:如果访问的数据不在应用程序缓存中,那么每次数据都会直接从磁盘加载,这种直接加载会非常耗时。通常直接IO与异步IO结合使用,会得到比较好的性能。(异步IO:当访问数据的线程发出请求之后,线程会接着去处理其他事,而不是阻塞等待)
直接I/O流程
从图中你可以看到,直接 I/O 访问文件方式减少了一次数据拷贝和一些系统调用的耗时,很大程度降低了 CPU 的使用率以及内存的占用。
但是直接与磁盘交互非常耗时,所以只有确定标准I/O开销非常巨大才考虑使用直接I/O。
1.3 mmap
mmap是指将硬盘上文件的位置与进程逻辑地址空间中一块大小相同的区域一一对应,当要访问内存中一段数据时,转换为访问文件的某一段数据。这种方式的目的同样是减少数据在用户空间和内核空间之间的拷贝操作。当大量数据需要传输的时候,采用内存映射方式去访问文件会获得比较好的效率。
使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,这意味着在对文件进行处理时将不必再为文件申请并分配缓存,所有的文件缓存操作均由系统直接管理,由于取消了将文件数据加载到内存、数据从内存到文件的回写以及释放内存块等步骤,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。
mmap的优点:
减少系统调用。我们只需要一次 mmap() 系统调用,后续所有的调用像操作内存一样,而不会出现大量的 read/write 系统调用。
减少数据拷贝。普通的 read() 调用,数据需要经过两次拷贝;而 mmap 只需要从磁盘拷贝一次就可以了,并且由于做过内存映射,也不需要再拷贝回用户空间。
可靠性高。mmap 把数据写入页缓存后,跟缓存 I/O 的延迟写机制一样,可以依靠内核线程定期写回磁盘。但是需要提的是,mmap 在内核崩溃、突然断电的情况下也一样有可能引起内容丢失,当然我们也可以使用 msync来强制同步写。
从上面的图看来,我们使用 mmap 仅仅只需要一次数据拷贝。看起来 mmap 的确可以秒杀普通的文件读写,那我们为什么不全都使用 mmap 呢?事实上,它也存在一些缺点:
虚拟内存增大。mmap 会导致虚拟内存增大,我们的 APK、Dex、so 都是通过 mmap 读取。而目前大部分的应用还没支持 64 位,除去内核使用的地址空间,一般我们可以使用的虚拟内存空间只有 3GB 左右。如果 mmap 一个 1GB 的文件,应用很容易会出现虚拟内存不足所导致的 OOM。
磁盘延迟。mmap 通过缺页中断向磁盘发起真正的磁盘 I/O,所以如果我们当前的问题是在于磁盘 I/O 的高延迟,那么用 mmap() 消除小小的系统调用开销是杯水车薪的。
在 Android 中可以将文件通过MemoryFile或者MappedByteBuffer映射到内存,然后进行读写,使用这种方式对于小文件和频繁读写操作的文件还是有一定优势的。我通过简单代码测试,测试结果如下。
从上面的数据看起来 mmap 好像的确跟写内存的性能差不多,但是这并不正确,因为我们并没有计算文件系统异步落盘的耗时。在低端机或者系统资源严重不足的时候,mmap 也一样会出现频繁写入磁盘,这个时候性能就会出现快速下降。
mmap 比较适合于对同一块区域频繁读写的情况,推荐也使用线程来操作。用户日志、数据上报都满足这种场景,另外需要跨进程同步的时候,mmap 也是一个不错的选择。Android 跨进程通信有自己独有的 Binder 机制,它内部也是使用 mmap 实现。
利用 mmap,Binder 在跨进程通信只需要一次数据拷贝,比传统的 Socket、管道等跨进程通信方式会少一次数据拷贝。