磁盘缓存
磁盘缓存和其他类型的缓存系统目的一样,都是为了提高系统的性能。磁盘缓存的方法是利用内存来保存部分磁盘数据,内存数据的读写速度远远快于磁盘读写,来提高系统性能。
缓存的位置
内核空间(kernel space):缓存在内核中实现。对应用程序来说是透明的。
用户空间(user space)缓存:由应用程序自己管理缓存,如C标准库中的stdio,就实现了缓存功能。用户空间程序更了解实际数据的使用情况,缓存策略也会更有针对性,比内核的缓存策略更加优化。如数据库系统一般会使用自己的缓存管理功能来代替内核的。
使用场景:
读磁盘块:如果要读的磁盘块已在内存中,直接使用内存中数据;否则给磁盘块分配内存空间,将磁盘块数据读入到内存。
写磁盘块:如果要写的磁盘块已在内存中,直接将数据写入内存中;否则给磁盘块分配内存空间,在将数据写入内存中。
内存和磁盘数据的同步
对于读操作来说,内存数据和磁盘数据一直一致,无需同步。
对于写操作来说,内存数据和磁盘数据会不一致,需要一定的同步方法。
Linux的磁盘缓存
Linux从诞生起就提供了磁盘缓存功能,磁盘缓存的实现也经历了几代:
1)buffer cache
从linux诞生到2.0版本,都采用这个方式。buffer cache以磁盘块为单位。
2) buffer cache和page cache
在linux 2.2和2.4中采用,存在两套缓存系统,page cache以page为单位实现缓存。
3) page cache
从linux 2.4.10开始采用,只有一套缓存page cache。原来的buffer cache的功能通过page cache来实现。
本文主要介绍linux 1.0中的buffer cache实现,即第一代实现。
接口
buffer cache的几个主要接口函数,基于这几个函数,可以实现磁盘读写。
extern struct buffer_head * getblk(dev_t dev, int block, int size);
extern void brelse(struct buffer_head * buf);
extern struct buffer_head * bread(dev_t dev, int block, int size);
extern struct buffer_head * breada(dev_t dev,int block,...);
extern unsigned long bread_page(unsigned long addr,dev_t dev,int b[],int size,int prot);
实现分析
1)数据结构
struct buffer_head {
# 实际的数据
char * b_data; /* pointer to data block (1024 bytes) */
# 磁盘块大小,不同文件系统的block大小不同
unsigned long b_size; /* block size */
unsigned long b_blocknr; /* block number */
dev_t b_dev; /* device (0 = free) */
unsigned short b_count; /* users using this block */
unsigned char b_uptodate;
unsigned char b_dirt; /* 0-clean,1-dirty */
unsigned char b_lock; /* 0 - ok, 1 -locked */
unsigned char b_req; /* 0 if the buffer has been invalidated */
struct wait_queue * b_wait;
struct buffer_head * b_prev; /* doubly linked list of hash-queue */
struct buffer_head * b_next;
struct buffer_head * b_prev_free; /* doubly linked list of buffers */
struct buffer_head * b_next_free;
struct buffer_head * b_this_page; /* circular list of buffers in one page */
struct buffer_head * b_reqnext; /* request queue */
};
buffer_head表示一个缓存快,和一个磁盘块对应。
(dev, blocknu)用来表示该缓存块分配给哪个块磁盘。
b_data用来存储实际的缓存数据。
b_count表示有多少进程(process)要使用该缓存快。如果b_count == 0,表示该缓存块是空闲(当前没有进程使用),可以分配给其他磁盘块。
hash_table结构
快来快速查找给定的磁盘块是否已经在缓存中。
已经分配的缓存块(buffer_head)全部保存在hash_table中,采用静态hash实现。通过(dev, blocknu)将缓存快散列到hash table中。具有相同hash value的缓存块组织成双向循环(circular)链表,通过b_prev和b_next实现。
free list结构
缓存块的基于LRU(least recently used)排序的列表。列表的第一项是LRU项。一个缓存块被使用后(调用getblk),放在该列表的最后。该列表中的缓存块不都是空闲的,需要根据b_count来决定。b_count==0表示空闲,可以分配给其他磁盘块。通过b_prev_free和b_this_page来实现。
2)同步控制
与同步有关的字段及初始化值:
b_dirt=0;
b_uptodate=0;
b_req=0;
b_dirt,表示缓存数据是否和磁盘数据一致,1表示数据不一致。在写缓存块操作中,需要将其设置为1。内核会按照一定策略将b_dirt == 1的缓存块内容写回到磁盘上。用户执行sync命令,也可以将缓存中所有不一致缓存块写回到磁盘上。驱动程序将数据写回磁盘后,会将b_dirt修改为0。
b_uptodate,是否同步了缓冲块和块设备的内容,或者说缓存块中的数据是否为最新。
针对读磁盘:当把内容从硬盘读入缓冲块后,b_uptodate被赋值为1。设备驱动程序会在数据读取完毕后设置该位。
针对写磁盘:当把数据被写入缓冲块时,需要将该位设置成1,b_dirt也设置为1。
3)并发控制
当驱动程序在读写磁盘块时,会将相应的缓存块上锁。此时缓存块中的数据处于不一致状态,进程不能使用。
b_count=1; 表示有多少个进程需要使用这个缓存块;
getblk,会增加b_count
brelse,会减小b_count
b_lock;锁,用来同步驱动程序进程和其他使用该缓存块的进程。1表示该缓存块上锁。
wait_on_buffer会检查该标志位,如果已经上锁,则该进程进入等待状态。
底层驱动程序会在读写磁盘块时,对相应缓存块上锁。ll_rw_block
b_wait;等待使用该缓存块的队列。
内存分配
缓存块中的b_data是系统根据需要动态分配的,每次分配一页内存(page)。每页内存可以容纳4块磁盘块(BLOCK_SIZE = 1024)。
getblk
getblk是最复杂的算法:处理一下情况
1) 磁盘块已经在内存中,直接返回该缓存块
2)不在内存中
2.1)有可供分配的缓存块(tmp->b_count==0),将该缓冲块分配给相应的磁盘块。
2.2)没有可供分配的缓存块
2.2.1)系统剩余内存足够,分配额外的缓冲块。
2.2.2)等待其他进程释放缓冲块。
参考书
The Design and Implementation of the UNIX Operating System (1986) by M Bach
该书第三章对buffer cache的设计做了详尽的描述,尤其是getblk算法的实现。
buffer cache的对其他缓存系统的设计和实现有很大的参考价值。其中的基本原理具有普适性:如缓存块的管理,缓存和实际数据的同步,并发控制等。