C#流Stream与IO详解(5)——读取文件的详细流程

【前言】

这里说的是阻塞式读写文件,只说主要的流程,不包括每个流程中为了处理不同情况的更细节处理

【读取文件】

  • 应用程序中(用户层)
    • C#调用FileStream.Read相关接口,传递文件句柄FileHandle,要读取的数据大小count,数据需要放在哪array startIndex等参数到C接口的ReadFile函数
    • C函数中发起系统调用,即调用库函数read() (系统调用的简单理解:操作系统是应用程序的管理者,系统调用是操作系统提供给该程序的通信接口,类似我们常写的对象Manager给对象实例提供的接口一样)
  • 系统调用(进入内核层)
    • CPU软中断:CPU在运行应用程序,执行代码,然后执行到读文件的代码时,发现这是一个系统调用函数,且是读取文件的IO操作,于是发生软中断。CPU保存当前进程信息,运行其他进程。
      • 如果没有中断,那么CPU需要轮询IO设备状态,一直等待IO设备完成读数据的请求,CPU在这段时间内什么也干不了,是对CPU资源的巨大浪费
      • 虽然CPU切换进程也会消耗一定的时间,这相比对等待一般IO设备完成请求而言要小得多
      • 一般来说CPU的速度比IO设备高几个数量级,但如果有个快速IO设备,那么CPU中断和切换进程的时间可能比轮询还多,所以CPU中断不一定比轮询好。在编程时要避免短时间内出现大量IO请求,否则CPU会不断中断,使得操作系统过载并引发活锁。
      • 在网络场景中一般不会使用中断,因为会在短时间内收到大量网络包,中断会导致活锁
    • 虚拟文件系统VFS:文件系统有很多实现,虚拟文件系统是一个抽象的文件系统模型,提供一个通用的接口,操作系统只需要调用接口即可,不需要关心文件系统的具体实现。类似于我们编程常写的基类。
    • 页缓存Page Cache:如果要读取的数据刚好在页缓存中存在,那么把数据从内核态拷贝到用户进程的内存中
    • 文件系统:文件系统提供了VFS的具体实现,如果页缓存中没有要读取的数据,那么就会调用到文件系统对read接口的具体实现
    • 通过块管理层:通用块层提供一个统一的接口供文件系统实现者使用,而不用关心不同设备驱动程序的差异,这样实现出来的文件系统就能用于任何的块设备。通过对设备进行抽象后,不管是磁盘还是机械硬盘,对于文件系统都可以使用相同的接口对逻辑数据块进行读写操作。通用块层会发出一个IO调度请求
    • IO调度层:对通用块层发出的IO调度请求,不一定会立即执行。其他应用程序也可能在不久前发出IO调度请求,这些IO调度请求会被缓存起来,在操作系统认为合适的时机,统一发出IO调度请求。什么时候是合适的时机,这就是IO调度算法要解决的问题,常见的调度算法有先来先服务(FCFS)、最短寻道时间优先(SSTF)、电梯算法等。
    • 设备驱动程序:操作系统需要完成和硬件设备的交互,负责这部分功能的是设备驱动程序。操作系统提供标准接口,设备的生产厂商负责编写设备驱动的具体实现。进而使得操作系统可以兼容不同厂商生产的相同功能的设备而无需关心具体实现细节。因此,插入系统的设备都会先经过安装驱动程序的步骤。我们知道接口有个通用的缺点,即无法使用接口实现者的特殊功能,因为接口本质是为抽象多数而不是全部。例如,在编程上,子类如果非常多,总会有个子类有个特殊方法,而大多数子类没有,这个方法无法暴露在接口上给接口调用者使用。在硬件上就体现为,某个设备有个特殊的功能,但操作系统无法使用。对文件系统而言,这里的驱动程序是磁盘驱动程序。
  • 硬件层
    • 找到数据所在的扇区编号
      • 磁盘包括硬盘,硬盘分为机械硬盘HDD和固态硬盘SSD。磁盘有一定数量的扇区,有n个扇区的磁盘上,编号为0到n-1,这也是磁盘的地址空间。
      • 一般一个扇区的内存大小时512块,制造商保证单个512字节的读写是原子的,即要么完整的读写完成,要么不会完成,不存在其他可能。而一般操作系统一次读写的大小为4kb(或者更多)
    • 磁盘驱动器会根据要读取的扇区编号找到数据所在的磁道
      • 扇区编号与具体的磁道是由固定的映射关系的,知道数据在哪个扇区,磁盘驱动器就可以知道数据在哪个磁道。但还需要将磁盘臂移动到该磁道才能读取数据,这就是一个寻道seek过程。如果数据在不同的磁道,那么就需要在多个磁道间切换。因此,存在一个寻道时间。
      • 一个磁道上由多个扇区,磁盘臂移动到磁道上后需要等待盘片旋转到某个扇区才能读数据,因此存在一个旋转时间
    • 磁头读写数据

【更细节的详细过程】

中断过程:

  • 硬件会提供不同的执行模式来协助操作系统,在用户态下,应用程序不能完全访问硬件资源,在内核态下,操作系统可以访问机器的全部资源。还提供了陷入内核和从陷进返回用户态的指令。
  • 系统调用有明确的调用约定,参数返回值都要放到什么寄存器内,执行那条陷入指令,这需要用汇编手工编码
  • 当机器启动时,操作系统会设置陷阱表trap table,其作用是建立了陷入指令和陷入处理程序的映射关系,操作系统通过某种特殊指令告知硬件陷入处理程序的位置。这一切都是在内核态下完成的
  • 发送中断时,操作系统获取CPU的控制权,当前进程变成阻塞状态(数据读取完成后变成就绪状态)。操作系统通过调度算法确定接下来要运行的进程,随后通过陷入返回指令回到用户态。
  • 如果没有系统调用引起的中断,发生时钟中断时,操作系统也会获取CPU的控制权。

内存与设备交互:

  • DMA(Direct Memory Access):内存和设备之间需要数据传递,原来这部分是由CPU完成了,为了提高效率,这件事交给DMA完成。
    • 读数据时:操作系统告诉DMA,读取的数据存放的起始地址,大小,在哪个设备读取。
    • 当DMA任务完成后,会抛出一个中断请求,告诉操作系统,数据传输的任务完成了。
  • 交互方式:
    • 特权指令:这些指令规定了操作系统将数据发送到特定设备寄存器的方法,这些方法规定了双方交互的协议
    • 内存映射I/O:硬件将设备寄存器作为内存地址提供,当需要访问设备寄存器时,操作系统读取或写入到该内存地址

如何找到扇区:

  • 文件系统会记录每个文件的信息,信息包括文件的大小、文件数据位于哪些数据块中(即扇区中)、文件所有者、访问权限、访问和修改时间等信息。这些信息在一个inode数据结构中。
  • 所有文件的inode信息也要保存在磁盘上,每个文件和文件夹都有一个inode编号(inumber),可以通过inumber拿到inode信息,这构成一个inode table,其在文件系统初始化时就从磁盘加载到内存中。文件系统初始化一般是在操作系统启动也即开机的时候进行。
  • 文件夹也有对应的inode,其包含(文件夹名或者文件名,inumber)列表,可以找到文件夹(即目录)下的文件夹及文件,其构成一个目录树
  • 当打开一个文件时,会根据传入的路径以及目录树递归找到该文件的inumber,从inode table中找到该文件的inode信息。
  • 打开文件时会创建一个file对象的数据结构,这个数据结构中会保存要读取的文件的inumber,同时返回一个文件描述符file descriptor(在windows中也叫文件句柄file handle)
  • 读取文件时根据文件句柄找到file对象,找到文件的inumber,扎到文件inode,根据要传入的StartIndex和length,找到要读取哪些扇区的数据
  • 在多进程中,为了不同进程共用file对象,会多包装一层。每个进程有文件描述符和描述符表,多个进程共享一个文件表,描述符表指向文件表中的某个file对象。

【参考】 

《操作系统导论》

猜你喜欢

转载自blog.csdn.net/enternalstar/article/details/133172685