Linux read函数深入

之前Linux read函数浅析-高速缓存说到Linux下读写文件都是通过高速缓存区实现的,应用程序要和底层的设备打交道,首先是通过高速缓存区,要是所要读的数据不存在,高速缓存区会帮应用程序代理去“拿”数据,高速缓存区的处理程序会向设备的驱动程序发出读请求,等待数据返回,这里就说说高速缓存区怎么实现和底层设备驱动函数交互。

对于每一个I/O,都会对应一个request请求项(可以理解成是加了一个结构体,请求项指定了本次文件要读的扇区编号,还有扇区数目等),对应每一个设备都由一个request队列。
对于一个块设备,内核初始化或者mount挂载的时候都必须先向内核申请注册,注册好之后会分配一个相应的队列,这个队列就是request请求项的链表。

block_read

sector是扇区,对于块设备而言,实际的操作读写都是基于扇区的,Linux里面是有块的概念,翻译成buffer,一个buffer是由几个扇区sectors构成 ,一般一个扇区都是512字节,一个buffer是两个扇区,所以是1K。但是我们的应用程序进行对块设备操作,比如对一个已经open(排除直接操作块设备)的文件,进行read和write,都是从高速缓存区进行操作的,高速缓存区是内核空间一段连续的内存空间,但是这段空间是有结构的,由一个一个buffer组成(链表),应用程序进行read或者write的时候,页管理程序会去查找对应的高速缓存区里有没有所请求的buffer,如果已经存在就直接返回buffer,如果不存在,这时候就需要去调用设备的驱动程序了。大概的流程如下

read() ==>> sys_read() ==>> bread() ==>> ll_rw_block() ==>> make_request() ==>> add_request() ==>> do_xx_request() ==>> read_intr() ==>> end_request

应用程序调用read,系统调用sys_read,该函数会根据inode结构判断相应的文件属性,sys_read()调用bread,是设备读写的抽象,可以读取硬盘,管道等。bread会查找所请求的buffer是不是在高速缓存里面,不存在就调用ll_rw_block,ll_rw_block是low_level_read_write_block的缩写,是底层的读写函数封装。ll_rw_block的函数作用就是调用make_request()建立一个请求项,请求项指定了本次文件要读的扇区编号,还有扇区数目等,还指定了具体文件系统的读写方法do_xx_request,xx是一种抽象,比如硬盘文件是do_hd_request。然后通过add_request()把该请求项插入对应的设备请求项队列,在插入的时候会判断设备是否存在其他请求项,如果没有就直接插入,并且调用下面的处理函数;如果已经有请求项等待处理,这时候就通过电梯算法插入请求队列,电梯算法会根据磁盘请求的盘面,磁道等合并一些请求项。

请求项建立好了,到现在位置每个设备都由自己的请求队列,请求队列上都是请求项,一个请求项都是对应一次读写操作。接下来请求项的处理就是通过do_xx_request()来实现,在do_xx_request里面,就会对设备请求项队列的每个请求项依次进行处理。下面先说说底层CPU是怎么和设备通信的

CPU <<==>>(发送读/写命令) <<==>> 控制器(控制器缓存) <<==>> 驱动器(数据源)

read_intr2
CPU操作驱动器(比如硬盘)上的数据一般都会通过控制器,CPU是皇帝,下发命令给控制器,控制器收到命令通知驱动器读数据或者写数据。但是控制器怎么通知CPU它的数据准备好了呢?通过中断,具体是首先CPU和控制器约定好中断函数,一旦控制器完成了相应的命令就会去调用这个中断函数。比如一旦控制器从驱动器完成了读操作(一个扇区的数据从驱动器存到了控制器缓存区),就会调用事先注册好的read_intr()函数,read_intr()函数会把控制器缓存中的数据存到内存相应位置(内存高速缓存区),然后判断是否还有扇区要读(请求项里面指定了要读哪些扇区,总共有几个)?如果还没读完,就还是设定好下次读取完成的中断函数是read_intr(),这里是递归调用了。直到所有扇区都已经读完了,这时候就会调用end_request(),删除对应的请求项,唤醒相应的等待进程等。
read_intr
至此,read()函数的底层调用就完成了。

标签:Linux, OS, Kernel

评论已关闭