只是想找本简单的书来试试英文,本以为《JavaNio》会够简单(单从API上来看),结果第一章的关于操作系统I/O的知识给跪了,查了很多wiki也没完全搞清楚,话说回来这些知识也不是一天能搞定的,特别是针对高级语言开发者。
  很幸运,虽然理解不透切,并且可能有理解有误差的地方,但学到了很多概念,至少算是把I/O方面知识图谱完善了一下。

  在操作系统IO中,有以下几个基本的概念:
    Buffer
    Vectored I/O
    Virtual Memory
    Page
    File I/O
    Stream I/O & Non Blocking Model

Buffer

  Buffer是IO操中最基础的部分,所有的输入/输出操作都就是将数据从buffer移进或者移出的动作。
  当进程/程序需要读取一个外部数据到进程内存时,进程首先向kernel发出read()的系统调用(System Call),kernel则调用Disk Controller,通过DMA将数据,无需借助CPU,直接从disk写入到kerner space中buffer。当kernel内的buffer写入完毕后则将这个buffer中的数据复制到用户进程内的buffer中。
  如下图所示:
buffer
  之所以不直接将数据写入到进程内存的原因如下:
    1.user space不能访问硬件,而kernel space是操作系统的使用的内存,有权限直接访问硬件。
    2.另一方面,硬件也不能使用访问虚拟内存地址(Process中使用的是虚拟内存地址,后面会讲到)。
    2.读取面向块的存储设备时,用户进程请求的读入非对称的数据块,需要kernel进行组装与拆解。

  所有的IO操作都需要经过kernel,无论是直接的或者间接的。

Vectored I/O

  允许在一次系统调用中,传入多个buffer,并顺序的完成对多个buffer的填充或抽干。有以下优点:
    1.高效:减少系统调用次数
    2.原子操作:操作多个buffer时,具有原子性

  对于这个特性我的理解不多,详情点这

Virtual Memory

  虚拟内存是现代操作系统都使用的一种内存管理技术,其原理是使用在用户进程中虚拟内存地址代替物理内存地址。
由进程产生的内存地址为虚拟内存地址,被为放在进程内存里的一个叫virtual address space连续的区域,这些虚拟内存地址在经过转换后,便能指向物理的内存区域。

  特点与优点如下:
    1.多个虚拟地址可以指向同一个内存物理区域,可实现内存的多重映射。同时也是实现进程隔离的一个要素。
    2.虚拟地址可指向外部存储设备的中的page区。
    3.虚拟地址与物理地址的转换由CPU的MMU完成,由于使用硬件,速度很快。

内存多重映射
  前面buffer里说到,从外部硬件读取数据到进程内存时,需要先读取到kernel space中的buffer(经过组装/拆解),再复制到进程内存中的buffer里去。而使用通过虚拟内存地址,实现内存多重映射,可以避免这一次复制:
  将kernel与process中的buffer区域都使用虚拟地址,将这两组虚拟地址映射到同一内存区域,既这个内存区域对process与kernel都可见:process可以直接操作,DC也可以直接从硬盘写入。如下图所示:
mutiple_mapping
  多重映射需要user space与kernel space中的内存页大小相同,并且是buffer必须是硬盘“块”大小的倍数。

Paging

  操作系统会将内存中按固定长度、连续的的块切分为“分页”,对于程序,分页是内存分配时的最小单位。且如果将内存的页大小设置为硬盘中块大小的倍数,kernel则可以通过直接指令将页存放至磁盘中。virtual address space中的虚拟地址仍可以指向这些存放到硬盘中的“虚拟分页”,不过CPU并无法直接使用它们。这里kernel通过一很狡猾的方法实现对这个磁盘上分页的“按需加载”。
physical_address_space
Kernel的圈套 – 虚拟分页加载
  现代CPU中有一个叫MMU(Memory Management Unit),用于虚拟地址到物理地址转换的处理单元,当CPU需要使用一个内存区域时,MMU先确定这个内存区域存在于哪个虚拟页中,再将虚拟页地址转换为物理页地址,并读取。或该物理页地址并不存在,则MMU抛出错误。这个错误会被kernel捕捉到,错误中附带中虚拟地址的信息。而kernel正好利用这个错误,通过虚拟地址确认的存放于硬盘中的虚拟分页并将其加载入内存。一旦该分页区域可用后,CPU便可以通过MMU获取到正确的数据,进程也继续执行。
  在将虚拟分页调入内存时,若内存不够用了,则需要将其它空闲状态的分页清空,若该分页被修改过,则需要先执行写出操作。
  kernel正是利用MMU的地址映射报错,实现对硬盘数据中的分页数据“按需加载”。

File I/O

  File I/O是相对于Disk I/O(上面说的)存在的。是针对Disk I/O的一层封装和文件化的抽象。

  disk自身只负责存储数据,并不知道这些数据该如果组合成语义化的文件。而kernel通过利用filesystem,对disk中的块进行组织/解释,实现对disk中的块进行文件化的管理。

  filesystem管理一批相同大小的块,其中一部分块用于存储metadata,而另一部分则用于存储真实的数据,metadata可用于描述文件数据所使用的块地址,最后一次更新等信息。

  当进程请求加载一个文件时,通过filesystem可以得到该文件具体存放在哪些块中。在一些老系统中,此时会通过dc将数据全部加载入内存。而现在操作系统则更多利用mmc异常,实现”按需读取”。

  *filesystem中也有分页的概念,这些页通常是内存页的倍数。
  实现文件下载的过程如下:
    1.filesystem确定需要读取文件的分布在哪些filesystem pages中。
    2.在内存中分配足够的与之对应的内存页(书上是这样写的,但这里应该是分配的虚拟内存地址)。
    3.在kernel中,建立虚拟内存地址与文件系统中页的映射。
    4.通过MMU报错,实现对整个文件中所有内存页的加载。
    5.加载完成后,filesystem对内存中页数据进行解析。

  filesystem中的数据,也会像其它内存页一样,会被缓存。在接下来的File IO中,如果发果目标文件存在于内存中,则直接使用。

Memory Mapped File
  通过前面提到的内存多重映射与filesystem,结合这两点,实现将用户内存区域直接映射到可操作的filesystem pages。
mutiple_mapping
  这样做的好处有:
    1.process直接面对memory,不需要再调用read() write()系统调用。
    2.对内存页的访问与修改,会自动触发数据在内存与磁盘间的读入与写出。
    3.处理大文件时不会占用整个文件大小的内存。非常适合用于处理大文件。

Stream I/O & Non Blocking Model

  除了类似与磁盘的一样的面向块的数据源外,像打印机、控制台或者网络都是面向流的。操作系统在处理面向流一般会比面向块的数据源更慢。另外由于面向流的一大特别是输入与输出都是间接性的,所以操作系统会提供一个非阻塞模式,允许程序检查具体的流是否准备就绪,若准备就绪才调用读入,而不会使程序在调用读入时阻塞直到有数据准备就绪。
  基于非阻塞模式,将检查流是否准备就绪工作交由操作系统完成,并由操作系统通知哪些流准备就绪了,程序单独使用一个线程监听操作系统的返回信息。这种模式被称为readiness selection。被广泛运用于网络编程中。