Buffer cache(缓冲区缓存)

本片是转的站内人翻译的国外的博客。

之所以发这个,是因为这对于后来讲解page cache和buffer cache的关系是很有意义的。如果博客的读者想在buffer cache和page cache的关系上搞透,那么我建议你要看完本篇、翻译的kernel对内存的管理和翻译的page cache以及最后我的总结篇


  现代操作系统的核心功能之一就是协调各个进程共享一个计算机的资源。首先它必须能隔离进程,使得某个问题进程不能损害其他进程,比如通过内存隔离;其次它也要提供有效的机制让不同的进程能协同工作,比如通过文件或者管道。我们来介绍介绍文件的一个知识点:buffer cache。
  磁盘驱动从/往磁盘拷贝数据,buffer cache就是磁盘块的临时拷贝。缓存磁盘块有一个明显的好处:磁盘的访问速度明显的比内存低,因此频繁地访问内存中的磁盘块会提升系统的速度。即便如此,性能也不是buffer cache存在的最大的原因。当两个不同的进程访问相同的磁盘块的时候,磁盘块就处于共享的状态了。buffer cache序列化了对磁盘块的访问,实现了基本的进程间安全的协调。

数据结构

  磁盘硬件大部分都是512bytes一个块,叫做sector。disk drive和buffer cache通过叫做buffer的struct协调磁盘扇区的使用。每一个buffer代表了某个disk设备的某个扇区的内容。dev和sector字段提供设备和扇区号,data字段提供磁盘扇区的硬盘拷贝。buffer的数据经常不是和磁盘保持同步的:可能还没有读取,也可能已经脏了(更新了)但还没有刷到磁盘。flags字段标识了buffer的状态:B_VALID标志表示已读,B_DIRTY表示已脏,B_BUSY表示已经被某个进程占用,其他进程不可以使用了。当一个buffer处于B_BUSY的时候,buffer就叫被锁了。

磁盘驱动

  IDE设备提供了访问连接到PC的disk的功能。IDE现在已经落后于正热的SCSI和SATA了,但是这个接口很简单,会让我们概览全局而不是陷入硬件细节地讨论问题。
  内核启动的时候在main中调用ideinit初始化磁盘驱动。Ideinit初始化idelock的同时准备了硬件,比如说禁用一些硬件中断。Ideinit调用picenable和ioapicenable以启用IDE_IRQ中断。调用picenable启用单处理器中断,调用ioapicenable启用多处理器中断,但是仅仅最后一个号的cpu会处理磁盘中断。
  下一步,ideinit探测磁盘硬件。首先调用idewait去等待到磁盘可以接受命令了。等磁盘控制器(disk controller)准备好之后,ideinit就可以检查有多少个disk了。假定有第0个disk,由于引导程序和内核在disk 0,必须再去检查disk 1.如果没有,则认为其他disk不存在,以此类推。
  在ideinit之后,知道buffer cache调用iderw,disk都不会再被使用了。如果buffer设置了B_DIRTY,则iderw就会将这个buffer写入disk;如果B_VALID没有设置,则iderw会从disk读取这个buffer。
  Disk访问都是毫秒级的,这对于cpu来说是个很长的时间。对于操作系统来说,比较有效的方法是监听磁盘操作完成的中断并调度其他程序跑。Iderw维护了一个请求队列,简单的IDE磁盘控制器一次只能处理一个操作。Disk driver维护处理对首的buffer,其他的简单地等待。
  Iderw添加新的buffer到队列的尾部。如果这个buffer是队首,则通过调用idestart把它发送给磁盘硬件;否则就等待成为队首。
  Idestart通过buffer的flags分发读或写给设备和扇区。如果是写操作,idestart必须提供数据,然后中断会通知数据已经写完了。如果是读,会有中断通知数据已经准备好了,完了其他的handler就会读取它了。
  最终disk将完成操作并触发中断,之后就会调用驱动ideintr处理它。Ideintr会查阅第队列中的第一个buffer看看发生了什么。如果buffer已经在等待读取了并且磁盘控制器有这个数据,那么ideintr就会通过insl将数据读入buffer。现在buffer就准备好了,ideintr设置B_VALID标志,清楚B_DIRTY,然后唤醒睡眠在这个buffer的进程。

Buffer cache

  就像开头说的,buffer cache是同步访问磁盘块的,确保了一次只有一个内核进程可以编辑文件系统的数据。Buffer cache通过bread来阻塞进程:如果两个进程对没有被使用的相同的设备和扇区号调用bread,其中一个会返回一个buffer,另一个等待直到前一个调用了brelse。
  Buffer cache是一个buffers的双链表。Binit使用静态数组buf中的NBUF初始化list。所有其他的对buffer cache的访问都通过bcache.head引用的链表,而不是buf数组。
  Bread通过调用bget获取指定扇区的锁定的buffer。如果buffer需要从disk读取,则bread调用iderw去处理。
  Bget根据device和sector号浏览buffer列表来查找buffer。如果有这样一个buffer,bget需要先对其上锁。如果buffer没有被使用,bget先设置为B_BUSY状态并返回。如果buffer使用中,bget就sleep等待其释放。当sleep返回后,bget不能假定buffer现在可用。事实上由于sleep释放之后要重新获取锁,没有保证这时能获取到正确的buffer,因为这时候有可能buffer又被其他disk sector用了。Bget只能硬着头皮重头再来。
  如果指定的扇区没有buffer,bget需要创建一个,可能复用不同扇区持有过的。它会扫描buffer list,找不是busy状态的。Bget编译block的元数据记录到新的device和sector号上,标记为busy,然后返回。
  因为buffer cache是同步使用的,对于某个磁盘扇区只有一个buffer是非常重要的。分配buffer需要是安全的,通过bget锁来控制。
  如果所有的buffer都busy,将会出错,bget会出错。一个比较友好的响应是直到获取到free buffer之前都睡眠。

现状

  实际上设备驱动远比这里讲的磁盘驱动的方式复杂,但是基本思想是一致的:设备都比cpu慢很多,因此使用硬中断通知操作系统状态的改变。现代的磁盘控制器可以同一时刻接受多个磁盘请求,也会让他们重排序以更合理使用机械臂。当磁盘简单的时候,操作系统会自己实现队列的调整,操作系统的IO调度策略都会有这种调整。
  其他的硬件也惊人的相似:网卡的buffers缓存网络包,音频设备的buffers缓存音频包,显卡的buffers缓存视频数据和命令队列等等。高带宽的设备经常使用direct memory access(DMA)而不是驱动中显式的I/O接口(insl,outsl)。DMA允许disk或者其他控制器直接访问物理内存。驱动提供给设备buffer的物理地址然后设备直接从主存拷贝到自己的缓存空间,一旦完成就发送中断。使用DMA意味着整个传输过程CPU不再参与,这样会更加有效并且不经过cpu的cache层次。
  实际操作系统中buffer cache也远比这里说的复杂,但是都提供两个目标:缓存和同步。buffer的大小也是和硬件的page size匹配的,这样有助于性能和实现。

译者结语

 本篇只是翻译一个大学文章,只是对于buffer cache的简介,可能很多人都没有看爽,看完了不是很了解其在整个IO过程中的位置以及与page cache的关系。我想说没关系,我们后续会有网络数据包在host内存和NIC缓冲的流转的介绍,你会对缓冲有更加深刻的理解。

猜你喜欢

转载自blog.csdn.net/maxlovezyy/article/details/70332720