把块存放在页高速缓存中

来源:互联网 发布:linux sqlplus 安装 编辑:程序博客网 时间:2024/05/17 23:45

VFS(映射层)和各种文件系统以叫做“块”的逻辑单位组织磁盘数据。在Linux内核的旧版本中,主要有两种不同的磁盘高速缓存:页高速缓存和缓冲区高速缓存,前者用来存放访问磁盘文件内容时生成的磁盘数据页,后者把通过VFS(管理磁盘文件系统)访问的块的内容保留在内存中。

 

从2.4.10的稳定版本开始,缓冲区高速缓存其实就不存在了。事实上,由于效率的原因,不再单独分配块缓冲区;相反,把它们存放在叫做“缓冲区页”的专门页中,而缓冲区页保存在页高速缓存中。

 

缓冲区页在形式上就是与称做“缓冲区头”的附加描述符相关的数据页,其主要目的是快速确定页中的一个块在磁盘中的地址。实际上,页高速缓存内的页中的一大块数据在磁盘上的地址不一定是相邻的。

 

1 块缓冲区和缓冲区头

 

每个块缓冲区都有buffer_head类型的缓冲区头描述符。该描述符包含内核必须了解的、有关如何处理块的所有信息。因此,在对所有块操作之前,内核检查缓冲区首部。缓冲区首部的字段位于/include/linux/Buffer_head.h:
struct buffer_head {
      unsigned long b_state;            /* 缓冲区状态标志 */
      struct buffer_head *b_this_page;  /* 指向缓冲区页的链表中的下一个元素的指针 */
      struct page *b_page;              /* 指向拥有该块的缓冲区页的描述符的指针 */

      sector_t b_blocknr;               /* 与块设备相关的块号(起始逻辑块号) */
      size_t b_size;                    /* 块大小 */
      char *b_data;                     /* 块在缓冲区页内的位置 */

      struct block_device *b_bdev;      /* 指向块设备描述符的指针 */
      bh_end_io_t *b_end_io;            /* I/O完成方法 */
       void *b_private;                 /* 指向I/O完成方法数据的指针 */
      struct list_head b_assoc_buffers; /* 为与某个索引节点相关的间接块的链表提供的指针 */
      atomic_t b_count;                 /* 块使用计数器 */
};

 

缓冲区头的两个字段编码表示块的磁盘地址:b_bdev字段表示包含块的块设备,通常是磁盘或分区;而b_blocknr字段存放逻辑块号,即块在磁盘或分区中的编号。

 

b_data字段表示块缓冲区在缓冲区页中的位置。实际上,这个位置的编号依赖于页是否在高端内存。如果页在高端内存,则b_data字段存放的是块缓冲区相对于页的起始位置的偏移量,否则,b_data存放的就是是块缓冲区的线性地址。

 

b_state字段可以存放几个标志。其中一些标志是通用的,我们在下面把它们列出来了。每个文件系统还可以定义自己的私有缓冲区首部标志。

BH_Uptodate:缓冲区包含有效数据时被置位
BH_Dirty:如果缓冲区脏就置位(表示缓冲区中的数据必须写回块设备)
BH_Lock:如果缓冲区加锁就置位,通常发生在缓冲区进行磁盘传输时
BH_Req:如果已经为初始化缓冲区而请求数据传输就置位
BH_Mapped:如果缓冲区被映射到磁盘就置位,即:如果相应的缓冲区首部的b_bdev和b_blocknr是有效的就置位
BH_New:如果相应的块刚被分配而还没有被访问过就置位
BH_Async_Read:如果在异步地读缓冲区就置位
BH_Async_Write:如果在异步地写缓冲区就置位
BH_Delay:如果还没有在磁盘上分配缓冲区就置位
BH_Boundary:如果两个相邻的块在其中一个提交之后不再相邻就置位
BH_Write_EIO:如果写块时出现I/O错误就置位
BH_Ordered:如果必须严格地把块写到在它之前提交的块的后面就置位(用于日志文件系统)
BH_Eopnotsupp:如果块设备的驱动程序不支持所请求的操作就置位

 

缓冲区头有它们自己的slab分配器高速缓存,其描述符kmem_cache_s存在变量bh_cachep中。alloc_buffer_head()和free_buffer_head()函数分别用于获取和释放缓冲区首部。

struct buffer_head *alloc_buffer_head(gfp_t gfp_flags)
{
      struct buffer_head *ret = kmem_cache_alloc(bh_cachep, gfp_flags);
      if (ret) {
            get_cpu_var(bh_accounting).nr++;
            recalc_bh_state();
            put_cpu_var(bh_accounting);
      }
      return ret;
}

 

void free_buffer_head(struct buffer_head *bh)
{
      BUG_ON(!list_empty(&bh->b_assoc_buffers));
      kmem_cache_free(bh_cachep, bh);
      get_cpu_var(bh_accounting).nr--;
      recalc_bh_state();
      put_cpu_var(bh_accounting);
}

 

缓冲区首部的b_count字段是相应的块缓冲区的引用计数器。在每次对块缓冲区进行操作之前递增计数器并在操作之后递减它。除了周期性地检查保存在页高速缓存中的块缓冲区之外,当空闲内存变得很少时也要对它进行检查,只有引用计数器等于0的块缓冲区才可以被回收。

 

当内核控制路径希望访问块缓冲区时,应该先递增引用计数器。确定块在页高速缓存中的位置的函数(__getblk(),后面会讲)自动完成这项工作,因此,高层函数通常不增加块缓冲区的引用计数器。

 

当内核控制路径停止访问块缓冲区时,应该调用__brelse()或__bforget()递减相应的引用计数器。这两个函数之间的不同是__bforget()还从间接块链表(缓冲区首部的b_assoc_buffers字段)中删除块,并把该缓冲区标记为干净的,因此强制内核忽略对缓冲区所做的任何修改,但实际上缓冲区依然必须被写回磁盘。

 

2 缓冲区页数据结构

 

只要内核必须单独地访问一个块,就要涉及存放块缓冲区的缓冲区页,并检查相应的缓冲区首部。下面是内核创建缓冲区页的两种普通情况:

 

(1)当读或写的文件页在磁盘块中不相邻时。发生这种情况是因为文件系统为文件分配了非连续的块,或因为文件有“洞”。

 

(2)当访问一个单独的磁盘块时(例如,当读超级块或索引节点块时)。

 

在第一种情况下,把缓冲区页的描述符插入普通文件的基树;保存好缓冲区首部,因为其中存有重要的信息,即存有数据在磁盘中位置的块设备和逻辑块号。

 

在第二种情况下,把缓冲区页的描述符插人基树,树根是与块设备相关的特殊bdev文件系统中索引节点的address_space对象。这种缓冲区页必须满足很强的约束条件,就是所有的块缓冲区涉及的块必须是在块设备上相邻存放的。

 

第二种情况的一个应用实例是:如果虚拟文件系统要读大小为1024个字节的索引节块(包含给定文件的索引节点)。内核并不是只分配一个单独的缓冲区,而是必须分配一个整页,从而存放四个缓冲区;这些缓冲区将存放块设备上相邻的4块数据,其中包括所请求的索引节点块。

这里我们将重点讨论第二种类型的缓冲区页,即所谓的块设备缓冲区页(有时简称为块设备页),因为这是读写磁盘文件最多见的情况。

 

在一个缓冲区页内的所有块缓冲区大小必须相同,因此,在80x86体系结构上,根据块的大小,一个缓冲区页可以包括多个缓冲区。

 

如果一个页作为缓冲区页使用,那么与它的块缓冲区相关的所有缓冲区首部都被收集在一个单向循环链表中。缓冲区页描述符的private字段指向页中第一个块的缓冲区(由于private字段包含有效数据,而且页的PG_private标志被设置,因此,如果页中包含磁盘数据并且设置了PG_private标志,该页就是一个缓冲区页。注意,尽管如此,其他与块I/O子系统无关的内核组件也因为别的用途而使用private和PG_private字段);每个缓冲区首部存放在b_this_page字段中,该字段是指向链表中下一个缓冲区首部的指针。此外,每个缓冲区首部还把缓冲区页描述符的地址存放在b_page中。



3 分配块设备缓冲区页

 

当内核发现指定块的缓冲区所在的页不在页高速缓存中时,就分配一个新的块设备缓冲区页。特别是,对块的查找操作会由于下述原因而失败:


(1)包含数据块的页不在块设备的基树中:这种情况下,必须把新页的描述符加到基树中。


(2)包含数据块的页在块设备的基树中,但这个页不是缓冲区页:在这种情况下,必须分配新的缓冲区首部,并将它链接到所属的页,从而把它变成块设备缓冲区页。


(3)包含数据块的缓冲区页在块设备的基树中,但页中块的大小与所请求的块大小不相同:这种情况下,必须释放旧的缓冲区首部,分配经过重新赋值的缓冲区首部并将它链接到所属的页。

 

内核调用函数grow_buffers()把块设备缓冲区页添加到页高速缓存中,该函数接收三个标识块的参数:
- block_device描述符的地址bdev。
- 逻辑块号block(块在块设备中的位置)。
- 块大小size。

 

下面我们就来分析这个重要的函数:
static int
grow_buffers(struct block_device *bdev, sector_t block, int size)
{
      struct page *page;
      pgoff_t index;
      int sizebits;

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

      index = block >> sizebits;

      /*
       * Check for a block which wants to lie outside our maximum possible
       * pagecache index.  (this comparison is done using sector_t types).
       */
      if (unlikely(index != block >> sizebits)) {
            char b[BDEVNAME_SIZE];

            printk(KERN_ERR "%s: requested out-of-range block %llu for "
                  "device %s/n",
                  __FUNCTION__, (unsigned long long)block,
                  bdevname(bdev, b));
            return -EIO;
      }
      block = index << sizebits;
      /* Create a page with the proper size buffers.. */
      page = grow_dev_page(bdev, block, index, size);
      if (!page)
            return 0;
      unlock_page(page);
      page_cache_release(page);
      return 1;
}

 

1. 计算数据页在所请求块的块设备中的偏移量index,然后将block与index对齐。


比如,块大小是512(都是以字节为单位),size << sizebits就是size * 2^sizebits,这个没问题吧!那么512*8=4096(PAGE_SIZE),所以跳出循环时sizebits是3,那么index = block >> sizebits,也就是最后计算出每个块512字节大小的块设备中的对应块block的块设备中的偏移是index = block / 8。然后将block与index对齐:block = index * 8

 

2. 如果需要,就调用grow_dev_page()创建新的块设备缓冲区页。


static struct page *
grow_dev_page(struct block_device *bdev, sector_t block,
            pgoff_t index, int size)
{
      struct inode *inode = bdev->bd_inode;
      struct page *page;
      struct buffer_head *bh;

      page = find_or_create_page(inode->i_mapping, index, GFP_NOFS);
      if (!page)
            return NULL;

      BUG_ON(!PageLocked(page));

      if (page_has_buffers(page)) {
            bh = page_buffers(page);
            if (bh->b_size == size) {
                  init_page_buffers(page, bdev, block, size);
                  return page;
            }
            if (!try_to_free_buffers(page))
                  goto failed;
      }

      /*
       * Allocate some buffers for this page
       */
      bh = alloc_page_buffers(page, size, 0);
      if (!bh)
            goto failed;

      /*
       * Link the page to the buffers and initialise them.  Take the
       * lock to be atomic wrt __find_get_block(), which does not
       * run under the page lock.
       */
      spin_lock(&inode->i_mapping->private_lock);
      link_dev_buffers(page, bh);
      init_page_buffers(page, bdev, block, size);
      spin_unlock(&inode->i_mapping->private_lock);
      return page;

failed:
      BUG();
      unlock_page(page);
      page_cache_release(page);
      return NULL;
}


该函数依次执行以下列子步骤:

 

a. 调用函数find_or_create_page(),传递给它的参数有:块设备的address_space对象(bdev->bd_inode->i mapping)、页偏移index以及GFP_NOFS标志。正如在前面“页高速缓存的处理函数”博文所描述的,find_or_create_page()在页高速缓存中(基树中)搜索需要的页,如果需要,就把新的页插入高速缓存。

 

b. 此时,所请求的页已经在页高速缓存中,而且函数获得了它的描述符地址。函数检查它的PG_private标志;如果为空,说明页还不是一个缓冲区页(没有相关的缓冲区首部),就跳到第e步。

 

c. 页已经是缓冲区页。从页描述符的private字段获得第一个缓冲区首部的地址bh,并检查块大小bh->size是否等于所请求的块大小;如果大小相等,在页高速缓存中找到的页就是有效的缓冲区页,因此跳到第g步。

 

d. 如果页中块的大小有错误,就调用try_to_free_buffers()释放缓冲区页的上一个缓冲区首部,并报错(goto failed)。

 

e. 调用函数alloc_page_buffers()根据页中所请求的块大小分配缓冲区首部,并把它们插入由b_this_page字段实现的单向循环链表(注意那个while循环):


struct buffer_head *alloc_page_buffers(struct page *page, unsigned long size,
            int retry)
{
      struct buffer_head *bh, *head;
      long offset;

try_again:
      head = NULL;
      offset = PAGE_SIZE;
      while ((offset -= size) >= 0) {
            bh = alloc_buffer_head(GFP_NOFS);
            if (!bh)
                  goto no_grow;

            bh->b_bdev = NULL;
            bh->b_this_page = head;
            bh->b_blocknr = -1;
            head = bh;

            bh->b_state = 0;
            atomic_set(&bh->b_count, 0);
            bh->b_private = NULL;
            bh->b_size = size;

            /* Link the buffer to its page */
            set_bh_page(bh, page, offset);

            init_buffer(bh, NULL, NULL);
      }
      return head;
no_grow:
      ……
}


void set_bh_page(struct buffer_head *bh,
            struct page *page, unsigned long offset)
{
      bh->b_page = page;
      BUG_ON(offset >= PAGE_SIZE);
      if (PageHighMem(page))
            /*
             * This catches illegal uses and preserves the offset:
             */
            bh->b_data = (char *)(0 + offset);
      else
            bh->b_data = page_address(page) + offset;
}
inline void
init_buffer(struct buffer_head *bh, bh_end_io_t *handler, void *private)
{
      bh->b_end_io = handler;
      bh->b_private = private;
}


此外,函数alloc_page_buffers调用set_bh_page用页描述符的地址初始化缓冲区首部的b_page字段,用块缓冲区在页内的线性地址或偏移量初始化b_data字段。

 

回到grow_dev_page:

 

f. 调用link_dev_buffers把页的缓冲区头连成一个循环链表,在page结构的字段private中存放第一个缓冲区首部的地址,把PG_private字段置位,并递增页的使用计数器(页中的块缓冲区被算作一个页用户):


static inline void
link_dev_buffers(struct page *page, struct buffer_head *head)
{
      struct buffer_head *bh, *tail;

      bh = head;
      do {
            tail = bh;
            bh = bh->b_this_page;
      } while (bh);
      tail->b_this_page = head;
      attach_page_buffers(page, head);
}


static inline void attach_page_buffers(struct page *page,
            struct buffer_head *head)
{
      page_cache_get(page);   /* 并递增页的使用计数器 */
      SetPagePrivate(page);
      set_page_private(page, (unsigned long)head);
}


#define SetPagePrivate(page)      set_bit(PG_private, &(page)->flags)
#define set_page_private(page, v)      ((page)->private = (v))

 

g. 调用init_page_buffers()函数初始化连接到页的缓冲区首部的字段b_bdev、b_blocknr和b_bstate。因为所有的块在磁盘上都是相邻的,因此逻辑块号是连续的,而且很容易从块得出:


static void
init_page_buffers(struct page *page, struct block_device *bdev,
                  sector_t block, int size)
{
      struct buffer_head *head = page_buffers(page);
      struct buffer_head *bh = head;
      int uptodate = PageUptodate(page);

      do {
            if (!buffer_mapped(bh)) {
                  init_buffer(bh, NULL, NULL);
                  bh->b_bdev = bdev;
                  bh->b_blocknr = block;
                  if (uptodate)
                        set_buffer_uptodate(bh);
                  set_buffer_mapped(bh);
            }
            block++;
            bh = bh->b_this_page;
      } while (bh != head);
}

 

h. 返回页描述符地址。

 

分配块设备缓冲区页后,形成了以下的数据结构关系:
.............

 

4 释放块设备缓冲区页

 

当内核试图获得更多的空闲内存时,就释放块设备缓冲区页。显然,不可能释放有脏缓冲区或上锁的缓冲区的页。内核调用函数try_to_release_page()释放缓冲区页,该函数接收页描述符的地址page,并执行下述步骤(还可以对普通文件所拥有的缓冲区页调用try_to_release_page函数):

 

1. 如果设置了页的PG_writeback标志,则返回0(因为正在把页写回磁盘,所以不可能释放该页)。


2. 如果已经定义了块设备address_space对象的releasepage方法,就调用它(通常没有为块设备定义的releasepage方法)。
3. 调用函数try_to_free_buffers()并返回它的错误代码。

 

函数try_to_free_buffers()依次扫描链接到缓冲区页的缓冲区首部,它本质上执行下列操作:


1. 检查页中所有缓冲区的缓冲区首部的标志。如果有些缓冲区首部的BH_Dirty或BH_Locked标志被置位,说明函数不可能释放这些缓冲区,所以函数终止并返回0(失败)。


2. 如果缓冲区首部在间接缓冲区的链表中,该函数就从链表中删除它。


3. 清除页描述符的PG_private标记,把private字段设置为NULL,并递减页的使用计数器。


4. 清除页的PG_dirty标记。

 

5. 反复调用free_buffer_head(),以释放页的所有缓冲区首部。


6. 返回1(成功)。

0 0
原创粉丝点击