I/O基础术语
学习Java NIO之前,需要先了解这些主流的术语概念。
缓冲区操作
进程执行 I/O 操作,就是向操作系统发出请求,让它要么把缓冲区里的数据排干(写),要么用数据把缓冲区填满(读)。
上图简单描述了数据从外部磁盘向运行中的进程的内存区域移动的过程。进程使用 read( )系统调用(有时称为“陷阱”),要求其缓冲区被填满。内核随即向磁盘控制硬件发出命令,要求其从磁盘读取数据。磁盘控制器把数据直接写入内核内存缓冲区,这一步通过 DMA(直接存储器存取) 完成,无需主 CPU 协助。一旦磁盘控制器把缓冲区装满,内核即把数据从内核空间的临时缓冲区拷贝到进程执行 read( )调用时指定的缓冲区(用户空间)。
- 用户空间:是常规进程所在区域。JVM 就是常规进程,驻守于用户空间。用户空间是非特权区域:比如,在该区域执行的代码就不能直接访问硬件设备。
- 内核空间:是操作系统所在区域。内核代码有特别的权力:它能与设备控制器通讯,控制着用户区域进程的运行状态,所有 I/O 都直接或间接通过内核空间。
发散/汇聚
许多操作系统能把缓冲区组装/分解过程进行得更加高效。根据发散/汇聚的概念,进程只需一个系统调用,就能把一连串缓冲区地址传递给操作系统。然后,内核就可以顺序填充或排干多个缓冲区,读的时候就把数据发散到多个用户空间缓冲区,写的时候再从多个缓冲区把数据汇聚起来。
虚拟内存
虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如RAM)的使用也更有效率。
- 虚拟内存与物理内存的关系是多对一
- 虚拟内存空间可大于实际可用的物理内存空间
需要知道的是,设备控制器无法通过内核空间的DMA将数据存储到用户空间,但是通过虚拟内存技术,即把内核空间地址与用户空间的虚拟地址映射到同一个物理地址,DMA 硬件(只能访问物理内存地址)就可以填充对内核与用户空间进程同时可见的缓冲区。
这样节省了内核与用户空间的往来拷贝的花销,但前提条件是,内核与用户缓冲区必须使用相同的页对齐,缓冲区的大小还必须是磁盘控制器块大小(通常为 512 字节磁盘扇区)的倍数(为什么必须是倍数?因为内存页是磁盘块大小的倍数,方便寻址)。
操作系统把内存地址空间划分为页,即固定大小的字节组。内存页的大小总是磁盘块大小的倍数,通常为 2 次幂(这样可简化寻址操作)。
虚拟和物理内存页的大小总是相同的。
内存页面调度
为了实现[虚拟内存空间可大于实际可用的物理内存空间]的特性,就引出了虚拟内存分页技术。
分页的思想是:程序运行时用到哪页就为哪页分配内存,没用到的页暂时保留在硬盘上。从本质上说,物理内存充当了分页区的高速缓存;而所谓分页区,即从物理内存置换出来,转而存储于磁盘上的内存页面。
把内存页大小设定为磁盘块大小的倍数,这样内核就可直接向磁盘控制硬件发布命令,把内存页写入磁盘,在需要时再重新装入。
对于采用分页技术的现代操作系统而言,这也是数据在磁盘与物理内存之间往来的唯一方式。
现代 CPU 包含一个称为内存管理单元(MMU)的子系统,逻辑上位于 CPU 与物理内存之间。该设备包含虚拟地址向物理内存地址转换时所需映射信息(一定包含已经加载到物理内存的页映射,不一定能够包含仍在分页区的页映射)。
- 当 CPU 引用某内存地址时,MMU负责确定该地址所在页(往往通过对地址值进行移位或屏蔽位操作实现),并将虚拟页号转换为物理页号(这一步由硬件完成,速度极快)。
- 如果当前不存在与该虚拟页形成有效映射的物理内存页,MMU 会向 CPU 提交一个页错误。
- 页错误随即产生一个陷阱(类似于系统调用),把控制权移交给内核,附带导致错误的虚拟地址信息,然后内核采取步骤验证页的有效性。
- 内核会安排页面调入操作,把缺失的页内容读回物理内存。(移除在物理内存中暂时不用的页来腾出空间)
- 如果待移出的页已经被修改过了,还必须首先执行页面调出,把页内容拷贝到磁盘上的分页区。
- 如果所要求的地址不是有效的虚拟内存地址(不属于正在执行的进程的任何一个内存段),则该页不能通过验证,段错误随即产生。于是,控制权转交给内核的另一部分,通常导致的结果就是进程被强令关闭。
- 一旦出错的页通过了验证,MMU 随即更新,建立新的虚拟到物理的映射(如有必要,中断被 移出页的映射),用户进程得以继续。
文件I/O
文件系统把一连串大小一致的数据块组织到一起。有些块存储元信息,如空闲块、目录、索引等的映射,有些包含文件数据。单个文件的元信息描述了哪些块包含文件数据、数据在哪里结束、最后一次更新是什么时候,等等。
当用户进程请求读取文件数据时,文件系统需要确定数据具体在磁盘什么位置,然后着手把相关磁盘扇区读进内存。
老式的操作系统往往直接向磁盘驱动器发布命令,要求其读取所需磁盘扇区。
采用分页技术的现代操作系统则利用请求页面调度取得所需数据。
操作系统还有个页的概念,其大小或者与基本内存页一致,或者是其倍数。
采用分页技术的操作系统执行 I/O 的全过程可总结为以下几步:
- 确定请求的数据分布在文件系统的哪些页(磁盘扇区组)。磁盘上的文件内容和元数据可能跨越多个文件系统页,而且这些页可能也不连续。
- 在内核空间分配足够数量的内存页,以容纳得到确定的文件系统页。
- 在内存页与磁盘上的文件系统页之间建立映射。
- 为每一个内存页产生页错误。
- 虚拟内存系统俘获页错误,安排页面调入,从磁盘上读取页内容,使页有效。
- 一旦页面调入操作完成,文件系统即对原始数据进行解析,取得所需文件内容或属性信息。
需要注意的是,这些文件系统数据也会同其他内存页一样得到高速缓存。对于随后发生的 I/O请求,文件数据的部分或全部可能仍旧位于物理内存当中,无需再从磁盘读取即可重复使用。
大多数操作系统假设进程会继续读取文件剩余部分,因而会预读额外的文件系统页。如果内存 争用情况不严重,这些文件系统页可能在相当长的时间内继续有效。
类似的步骤在写文件数据时也会采用。这时,文件内容的改变(通过 write( ))将导致文件系统页变脏,随后通过页面调出,与磁盘上的文件内容保持同步。文件的创建方式是,先把文件映射到空闲文件系统页,在随后的写操作中,再将文件系统页刷新到磁盘。
内存映射文件
内存映射 I/O 使用文件系统建立从用户空间直到可用文件系统页的虚拟内存映射。这样做有几 个好处:
- 用户进程把文件数据当作内存,所以无需发布 read( )或 write( )系统调用。
- 当用户进程碰触到映射内存空间,页错误会自动产生,从而将文件数据从磁盘读进内存。如果用户修改了映射内存空间,相关页会自动标记为脏,随后刷新到磁盘,文件得到更新。
- 操作系统的虚拟内存子系统会对页进行智能高速缓存,自动根据系统负载进行内存管理。
- 数据总是按页对齐的,无需执行缓冲区拷贝。
- 大型文件使用映射,无需耗费大量内存,即可进行数据拷贝。
如果数据缓冲区是按页对齐的,且大小是内建页大小的倍数,那么,对大多数操作系统而言,其处理效率会大幅提升。
文件锁定
文件锁定机制允许一个进程阻止其他进程存取某文件,或限制其存取方式。
通常的用途是控制共享信息的更新方式,或用于事务隔离。数据库等复杂应用严重信赖于文件锁定。
文件锁定有两种方式:共享的和独占的。
- 多个共享锁可同时对同一文件区域发生作用;
- 独占锁要求相关区域不能有其他锁定在起作用。
共享锁和独占锁的经典应用,是控制最初用于读取的共享文件的更新:
某个进程要++读取文件++,会先取得该文件或该文件部分区域的共享锁。第二个希望++读取相同文件区域++的进程也会请求共享锁。两个进程可以并行读取,互不影响。
但是,假如有第三个进程要++更新该文件++,它会请求独占锁。该进程会处于阻滞状态,直到既有锁定(共享的、独占的)全部解除。一旦给予独占锁,其他共享锁的读取进程会处于阻滞状态,直到独占锁解除。
上图为共享锁阻断独占锁请求
上图为独占锁阻断共享锁请求
文件锁有建议使用和强制使用之分。
- 建议型文件锁会向提出请求的进程提供当前锁定信息,但操作系统并不要求一定这样做,而是由相关进程进行协调并关注锁定信息。多数 Unix 和类 Unix 操作系统使用建议型锁,有些也使用强制型锁或兼而有之。
- 强制型锁由操作系统或文件系统强行实施,不管进程对锁的存在知道与否,都会阻止其对文件锁定区域的访问。微软的操作系统往往使用的是强制型锁。
流I/O
流的传输一般(也不必然如此)比块设备慢,经常用于间歇性输入。
多数操作系统允许把流置于非块模式,这样,进程可以查看流上是否有输入,即便当时没有也不影响它干别的。这样一种能力使得进程可以在有输入的时候进行处理,输入流闲置的时候执行其他功能。
另一种更优选择是将流置于就绪性选择,就绪性选择与非块模式类似(常常就是建立在非块模式之上),但是把查看流是否就绪的任务交给了操作系统。操作系统受命查看一系列流,并提醒进程哪些流已经就绪。这样,仅仅凭借操作系统返回的就绪信息,进程就可以使用相同代码和单一线程,实现多活动流的多路传输。