文件系统-高速缓冲区:
首先我们为什么需要高速缓冲区而不是直接访问块设备中的数据。这是因为,IO设备和内存之间的读写速度不匹配而且有一点数据需要写入或者读出磁盘就访问磁盘,磁盘很快就会损坏,而高速缓冲区就起了一个中间过程的作用,把数据存在高速缓冲区中,需要读取磁盘上的数据时,尝试匹配高速缓冲区中的数据,匹配成功了,那就直接从高速缓冲区中取数据,然后内核再来操作,如果要存入数据,也是先经过缓冲区,再存入磁盘。这样就避免了每次都对IO设备进行操作。
高速缓冲区在整个物理内存中的位置处于内核区和主内存区之间。这里引用《linux0.11代码完全注释》中的图。
在高速缓冲区内部,分成了两个部分,一个是缓冲头结构,另一个是缓冲块。每个缓冲块的大小与块设备上的磁盘逻辑块的大小相同,而缓冲头结构用于连接起缓冲块,以及设置一些属性。结构如图:
那么内核要使用缓冲块时,是怎么和物理设备对应起来的呢?就比如向某个设备写了一些数据,存储在缓冲块中,这个缓冲块怎么才能把数据写入磁盘中。答案是在缓冲头结构中存储了块设备号和缓冲数据的逻辑块号,它们一起唯一确认了缓冲块数据对应的块设备和数据块。并且为了快速的在缓冲区中查看数据块是否在缓冲区中,使用了hash表结构以及空闲缓冲块队列来进行操作和管理,在linux0.11中采用的散列函数是:#define _hash(dev, block) ((unsigned)(dev^block))%NR_HASH
。NR_HASH是哈希数组的长度。结构如图:
在图中,双向箭头代表哈希在同一个表项的双向链表指针,对应b_prev
和b_next
字段。虚线表示当前连接在空闲缓冲块链表中空闲缓冲块之间的链表指针,free_list是空闲链表的头指针。
在内核中使用getblk()
函数来获取合适的缓冲块。该函数调用get_hash_table()
函数,在hash表中确认指定设备号和逻辑块号的缓冲块是否存在,如果存在的话,直接返回对应的缓冲头结构的指针。否则,会从空闲链表头开始对整个空闲链表进行扫描,寻找可以用的空闲缓冲区。有可能可以用的有多个空闲缓冲区,这时就要根据缓冲头结构的修改标志和锁定标志组合而成的权值,来判断哪个空闲块最合适。如果找到的空闲块即没有被修改也没有被锁定,那就使用该空闲块了。如果没有找到空闲块,那就让当前进程进入睡眠,等到继续执行时再次寻找。如果该空闲块被锁定,那么当前进程也需要进入睡眠,等待其它进程解锁。如果在睡眠等待的过程中,该缓冲块又被其它进程占用,就需要重新开始搜索缓冲块了。如果没被其它进程占用,就判断该缓冲块是否已被修改(还未写到盘中),如果已被修改,就将该块写盘,并等待该块解锁。此时如果该缓冲块又被其它进程占用,就又只有重新找空闲的缓冲块了。这里还有一种意外的情况,那就是在当前进程睡眠时,其它进程把我们需要的缓冲块已经加入了hash队列中,所以需要最后搜索一次hash队列,如果在hash队列中找到了该缓冲块,就只好重新执行以上操作了。最后,得到了一块没有被进程引用也没有被上锁以及没有被修改的空闲缓冲块,将该块的引用计数置1,并复位其它的标志,把该缓冲头结构从空闲表中取出。在设置了该缓冲块所属的设备号和相应的逻辑号后,再把该缓冲头结构放入hash表对应表项头部和空闲队列队尾。最后,返回该缓冲块头的指针。流程图如图:
getblk
函数返回的可能是一个新的空闲块,也可能是含有我们需要的数据的缓冲块。因此对于读取数据块操作函数bread()
,就需要判断该缓冲块的更新标志,已知道所含数据是否有效,如果有效则直接返回给进程,否则就调用底层块读写函数ll_rw_block()
,并同时睡眠,等待数据从物理设备写入缓冲块,醒了之后再重新判断是否有效,如果还是不行,那就释放该缓冲块,并返回NULL。流程图如图:
当程序想要释放一个缓冲块时,调用brelse()
函数,释放缓冲块并唤醒因为等待该缓冲块而进入睡眠的进程。
最后,除了驱动程序之外,其它上层程序对块设备的读写操作都需要经过高速缓冲区管理程序来实现数据的读写。它们之间的联系主要是通过bread()
函数和ll_rw_block()
函数实现。如图所示:
参考书籍:《linux0.11内核完全注释》