缓冲技术之三:Linux下I/O操作buffer缓冲块使用流程

0. Linux下缓冲池技术的简单介绍

Linux文件系统中,存在着著名的三大缓冲技术用以提升读写操作效率: inode缓冲区、dentry缓冲区、块缓冲。其中所谓的块缓冲便是我们前面一直在讨论的缓冲池技术,常用来配备IO操作,用来减少IO读取次数,以提升系统效率。

本文便是此前《换成技术》系列的两篇文章的基础上继续讨论缓冲池技术。对于块缓冲体系而言,需要提及的两个概念分别是page cache和buffer cache,每个page cache包含若干buffer cache,即两者的粒度不同,page是在buffer缓冲块基础上封装出的更粗粒度的对象。

内存管理系统和VFS(虚拟文件系统)只与page cache这一粗粒度级别的缓冲对象进行交互,内存管理系统负责维护page cache的分配和回收(如按照LRU策略进行淘汰)。在内核需要读写数据时,使用“内存映射”等复杂机制进可以和物理内存块进行正确的映射。而具体文件系统一般只与buffer cache这一更小粒度级别的缓冲对象交互,它们负责在存储设备和buffer cache之间交换数据,具体的文件系统直接操作的是磁盘等disk部分,而VFS则负责将数个buffer cache包装成page cache提供给用户。

对于具体的Linux文件系统,磁盘等外设存储设备会以磁盘块为单位分配给文件用以存储,所以buffer cache大小正好对应着磁盘块block的大小。引入缓冲区的目的主要还是为了降低对文件存储的外部设备的IO操作次数。每个缓冲区由两个部分组成,第一部分称为缓冲区首部,用数据结果buffer_head表示,而第二部分是真正的存储的数据。(这里可以参考我的关于MiniCRT自定义简化版运行库中提供的堆管理

typedef struct _heap_header
{
    enum{
        HEAP_BLOCK_FREE = 0xABABABAB, //空闲块的魔数
        HEAP_BLOCK_USED = 0xCDCDCDCD, //占用块的魔数
    }type;

    unsigned size;  //当前块的尺寸,该size包括块的信息头的尺寸
    struct _heap_header* next;
    struct _heap_header* prev;
}heap_header;

1. Linux系统下IO操作使用Buffer缓冲块的过程

首先给出page cache的定义

typedef struct page {
    struct list_head list; //mapping has some page list
    struct address_space *mapping; //the inode we belong to 
    unsigned long index;   //our offset within mapping
    struct page *next_hash; //Next page sharing our hash bucket in the pagecache hash table
    //和为了快速管理buffer采用hash table一样,管理page同样擦用了hash table,这个next_hash表示和该
    //page的hash-key一样的下一个page的指针

    atomic_t count; //线程或进程使用计数,在该计数为0时意味着该page已经可以被清除了
    unsigned long flags;  //原子锁标志,有可能存在更新不同步的情况
    struct list_head lru;  //pageout list, eg. active_list; protected by pagemap_lru_lock !

    struct page *pprev_hash; //complement to next_hash和next_hash相对应的指向前面page的指针
    struct buffer_head *buffers; //buffer maps us to a disk block;
    /******
    *On machines where all RAM is mapped into kernel address space, we can simply 
    calculate the virtual address. On machines with highmem some memory  is mapped into 
    kernel virtual memory dynamically.So we need a place to store that address. Note 
    that this field could be 16 bits on x86...Architectures with slow multiplication can 
    define WANT_PAGE_VIRTUAL in asm/page.h
    *********/
#if defined(CONFIG_HIGHMEM) || defined(WANT_PAGE_VIRTUAL)
    void *virtual;
#endif
} mem_map_t;

再给出buffer cache的定义形式,其中首先给出buffer_head结构体的定义内容

struct buffer_head {
    struct buffer_head *b_next; //有效buffer是通过哈希表进行管理的,但哈希表中可能因为(block, dev)组合被映射到同一个key下,
                            //所以提供这个同一key下的buffer_head*链表指针,用以二次遍历锁定准确的buffer
    unsigned long b_blocknr; //block number 该buffer映射的磁盘块块号
    unsigned short b_size;    //block size 该buffer映射的磁盘块内容大小
    unsigned short b_list; //
    kdev_t  b_dev; //该buffer映射的磁盘块隶属的虚拟设备标示号

    atomic_t b_count; //缓冲区读写使用计数,如果为0,意味着该缓冲区内的内容已经没有线程声明要使用了,意味着该Buffer是可以被释放进入空闲队列了
    kdev_t  b_rdev;   //真实设备标识
    unsigned long b_state; //buffer状态标记位,各位对应不同的含义
    unsigned long b_flushtime; //延迟写的上限时间

    struct buffer_head *b_next_free; // lru.free list linkage 指向lru空闲链表中next元素
    struct buffer_head *b_prev_free; //doubly linked list of buffers 指向lru空闲链表中prev元素
    struct buffer_head *b_this_page; //circular list of buffers in one page若该buffer被使用,则该参数指向同一个page的buffer链表
    struct buffer_head *b_reqnext;   //request queue

    struct buffer_head **b_pprev; //doubly linked list of hash-queue hash队列双向链表
    char *b_data; //pointer to data block 指向数据块的指针
    struct page *b_page;  //the page this bh is mapped to 这个buffer映射的页面
    void (*b_end_io)(struct buffer_head *bh, int uptodate); //IO completion IO结束时的执行函数_end
    void *b_private;  //reserved for b_end_io 为IO结尾函数_end保留位

    unsigned long b_rsector;  //real buffer location on disk 缓冲区在磁盘上的实际位置
    wait_queue_head_t b_wait;  
    struct list_head  b_inode_buffers; //doubly linked list of 

    inode dirty buffers//iNode脏缓冲区循环链表

};

在buffer_head结构体中提到其状态标识位是由unsigned long b_state;来表示的,下面来进一步分析该状态参数各位的意义。

enum bh_state_bits
{
    BH_Uptodate, //譬如0x0001,如果缓冲区存在有效数据则置为1,否则为0x0000
    BH_Dirty, //0x0010,如果buffer脏了即数据被修改了,则置该位
    BH_Lock, //如果该缓冲区被锁定了,即存在某一进程或线程正在使用该buffer,则置该位
    BH_Req, //如果缓冲区无效了,则置为该位
    BH_Mapped, //如果缓冲区有一个磁盘映射置该位
    BH_New, //如果该缓冲区是fresh,新分配加入缓冲池的,并且还没有被使用,则置该位;
    BH_Async, //如果缓冲区是进行end_buffer_io_async IO同步则置该位
    BH_Wait_IO, //如果要将这个buffer写回到映射的磁盘中,则置该位
    BH_Launder, //需要重置该buffer,置该位
    BH_Attached, //if b_indoe_buffers is linked into a list则置该位
    BH_JBD, //如果和journal_head 关联置1
    BH_Sync, //如果buffer是同步读取置该位
    BH_Delay, //如果buffer空间是延迟分配置该位
    BH_PrivateStart, //not a state bit, but the first bit available for private allocation by other entities
};

从buffer_head和page结构体可以看出,操作系统为了快速定位具体的缓冲块,采用了hash-table进行管理,故而这里介绍下Linux操作系统中关于Buffer缓冲块的hash方式

/*关于VFS如何管理这几个buffer cache的链表
*1.其中关于存储着有效数据的buffer,是通过hash表管理的,key值是由数据块号+所在设备标识号计算得到
*/

#define _hashfn (dev, block)       \
        ( (  ((dev) << (bh_hash_shift - 6)) ^ ((dev) << (bh_hash_shift - 9)) ) ^ \
            ( ((block) << (bh_hash_shift - 6)) ^ ((block) >> 13) ^ \
              ((block) << (bh_hash_shift - 12)) \
             ) \
        )

介绍了诸多基础的东西,下面看下Linux下如何具体定位一个Buffer缓冲块的函数bread()。其根据虚拟设备号、缓冲块号以及容量参数进行具体定位。

//在具体的文件系统中读取具体一块数据时,调用bread函数
struct buffer_head * bread(kdev_t dev, int block, int size)
{
    struct  buffer_head * bh;

    bh = getblk(dev, block, size);  //根据设备号、块号、和要读取的字节数目返回相应的buffer

    if ( buffer_uptodate(bh) ) //判断是否存在有效数据,如果存在那么直接返回即可
        return bh;

    set_bit(BH_Sync, &bh->b_state);  //如果不存在有效数据,将这个buffer设置为同步状态
    ll_rw_block(READ, l, &bh);  //如果没有有效数据,则需要现场从磁盘中将相应块号的内容读取到buffer中,这个是一个操作系统底层的操作

    wait_on_buffer(bh); //等待buffer的锁打开
    if ( buffer_uptodate(bh) )
        return bh;
    brelse(bh);
    return NULL;
};

getblk()函数的具体实现,其根据相应参数返回具体的缓冲块首地址

struct buffer_head * getblk(kdev_t dev, int block, int size)
{
    for (;;)
    {
        struct buffer_head * bh;
        bh = get_hash_table(dev, block, size); //关键函数,得到hash表中的buffer
        if (bh) {
            touch_buffer(bh);
            return bh; //返回这个buffer
        }

        //如果没有找到对应的buffer,那么试着去增加一个buffer,就是使用下面的grow_buffer函数
        if (!grow_buffers(dev, block, size))  //即调用该函数返回一个足够空间的fresh buffer,用以提供给后面从磁盘读取目标块的内容的缓冲区
            free_more_memory();//如果空间不足,则只能从LRU队列中选出buffer,先看是否已“脏”,若是,则写回磁盘,并清空内容,分配给新的数据块
    }
};
#define hash(dev, block) hash_table[ ( _hashfn(HASHDEV(dev), block) & bh_hash_mask ) ]
#define get_bh(bh)  atomic_inc( &(bh)->b_count )

struct  buffer_head * get_hash_table( kdev_t dev,  int block, int size)
{
    struct buffer_head *bh;
    struct buffer_head **p = &hash(dev, block); //通过hash表查找到对应的 buffer

    read_lock (&hash_table_lock); 

    //判断得到的buffer数组中有没有我们需要的buffer
    for(;;) 
    { 
        bh = *p;
        if (!bh)
            break;

        p = &bh->b_next;
        if (bh->b_blocknr != block)
            continue;
        if (bh->b_size != size)
            continue;
        if (bh->b_dev != dev)
            continue;
        get_bh(bh); //如果有那么直接执行这个函数,这个函数其实已经通过宏给出
        break;
    }

    read_unlock(&hash_table_lock);
    return bh;
};

如果缓冲池不够用,则试着增加新的缓冲块,该操作便是通过grow_buffers()函数实现的。


//如果没找到对应的buffer,那么使用grow_buffer函数增加一个新的buffer,该buffer的状态标记为BH_New
// try to increase the number of buffers available: the size argument is used to determine
//what kind of buffers we want
stastic int grow_buffers (kdev_t dev, unsigned long block, int size)
{
    struct page* page;
    struct block_device *bdev;
    unsigned long index;
    int sizebits;

    /**size must be multiple of hard sectorsize 给出的size必须是硬件扇区的整数倍*/
    if (size & (get_hardsect_size(dev) - 1) )
        BUG();
    if (size < 512 || size > PAGE_SIZE )
        BUG();
    //新加入的缓冲块大小必须在512到PAGE_SIZE

    sizebits = -1;
    do 
    {
        sizebits++;
    } while ((size << sizebits) < PAGE_SIZE);

    index = block >> sizebits;
    block = index << sizebits;

    bdev = bdget( kdev_t_to_nr(dev) );
    if (!bdev)
    {
        printfk("No block device for %s\n", kdevname(dev));
        BUG();
    }

    /*即根据需求的size新开辟一个缓冲页Page*/
    page = grow_dev_page( bdev, index, size );

    atominc_dec( &bdev->bd_count);
    if (!page)
        return 0;

    /*Hash in the buffers on the hash list*/
    hash_page_buffers( page, dev, block, size);
    UnlockPage( page );
    page_cache_release( page );

    /* we hashed up this page, so increment buffermem*/
    atomic_inc( &buffermem_pages );
    return 1;
}

前面说到我们是通过hash-table来管理已经被填入有效数据的缓冲区buffer的,但是其实缓冲区类型是由多种的

#define BUF_CLEAN  0
#define BUF_LOCKED 1 //正在等待被写回的uffer: Buffers scheduled for write
#define BUF_DIRTY  2 //脏buffer,但还没有被安排写回,即延迟写策略不满足
#define NR_DIRTY   3

而事实上,缓冲池中更新策略一个重要的概念便是LRU(least recently used最近最少使用)。而缓冲池的具体实现中除了empty\input\output三种队列,还有便是LRU队列控制的淘汰缓冲队列以及hash-table提供的快速索引。

综合来说,Linux系统为IO读取操作配备的buffer缓冲池是这样起作用的:

  1. 首先在Hash-table中寻找目标buffer,如果找到了该buffer,则直接返回该buffer的buffer_head指针,如果没有,那么意味着要读取的内容并不在buffer缓冲池中,故而要为这份新的数据内容分配一块新的匹配size的buffer;

  2. 先从内存中再直接划分出一块区域作为新添加的缓冲块,加入缓冲池体系中,供应本次使用;

  3. 如果内存空间不足或者已经达到缓冲池规模上限,则开始从LRU队列中取出链首元素,先看是否脏了,如果脏了,则先回写,然后清空内容,将它分配给新的数据块。

你可能感兴趣的:(Linux内核)