6.7.2 缓冲区高速缓存

Linux 采用了缓冲区高速缓存机制,而不同于其他操作系统的“写透”方式,也就是说,当你把一个数据写入文件时,内核将把数据写入内存缓冲区,而不是直接写入磁盘。

在这里要用到一个数据结构 buffer_head 它是用来描述缓冲区的数据结构,缓冲区的大小一般要比页面尺寸小,所以页面中可以包含数个缓冲区,同一页面中的缓冲区用链表连接。回忆页面结构page,其中有一个域buffer_head buffer就是用来指向缓冲区的,这个结构的详细内容请参见虚拟文件系统。

由于使用了缓冲技术,因此有可能出现这种情况:写磁盘的命令已经返回,但实际的写入磁盘的操作还未执行。

基于上述原因,应当使用正常的关机命令关机,而不应直接关掉计算机的电源。用户也可以使用 sync 命令刷新缓冲区高速缓存,从而把缓冲区中的数据强制写到磁盘中。在 Linux 系统中,除了传统的 update 守护进程之外,还有一个额外的守护进程 dbflush,这一进程可频繁运行不完整的 sync 从而可避免有时由于 sync 命令的超负荷磁盘操作而造成的磁盘冻结,一般的情况下,它们在系统引导时自动执行,且每隔30秒执行一次任务。

sync命令使用基本的系统调用sync()来实现。

dbflush Linux 系统中由 update 启动。如果由于某种原因该进程僵死了,则内核会发送警告信息,这时需要手工启动该进程(/sbin/update)。

 

1. 页面缓存的详细描述。

经内存映射的文件每次只读取页面内容,读取后的页面保存在页面缓存中,利用页面缓存,可提高文件的访问速度。如图6.19所示,页面缓存由 page_hash_table 组成,它是一个mem_map_t(struct page 数据结构)的指针向量。页面缓存的结构是 Linux 内核中典型的哈希表结构。众所周知,对计算机内存的线性数组的访问是最快速的访问方法,因为线性数组中的每一个元素的位置都可以利用索引值直接计算得到,而这种计算是简单的线性计算。但是,如果要处理大量数据,有时由于受到存储空间的限制,采用线性结构是不切合实际的。但如果采用链表等非线性结构,则元素的检索性能又会大打折扣。哈希表则是一种折衷的方法,它综合了线性结构和非线性结构的优点,可以在大量数据中进行快速的查找。哈希表的结构有多种,在 Linux 内核中,常见的哈希结构和图6.19 的结构类似。要在这种哈希表中访问某个数据,首先要利用哈希函数以目标元素的某个特征值作为函数自变量生成哈希值作为索引,然后利用该索引访问哈希表的线性指针向量。哈希线性表中的指针代表一个链表,该链表所包含的所有节点均具有相同的哈希值,在该链表中查找可访问到指定的数据。哈希函数的选择非常重要,不恰当的哈希函数可能导致大量数据映射到同一哈希值,这种情况下,元素的查找将相当耗时。但是,如果选择恰当的哈希函数,则可以在性能和空间上得到均衡效果。

 

Linux 页面缓存中,访问 page_hash_table 的索引由文件的 VFS(虚拟文件系统)索引节点 inode 和内存页面在文件中的偏移量生成。有关 VFS 索引节点的内容将虚拟文件中讲到,在这里,应知道每个文件的 VFS 索引节点 inode 是唯一的。


            6.19 Linux 页面缓存示意图

 

当系统要从内存映射文件中读取某一未加锁的页面时,就首先要用到函数:

 

find_page (struct inode * inode, unsigned long offset)

 

它完成如下工作:

首先是在“页面缓存”中查找,如果发现该页面保存在缓存中,则可以免除实际的文件读取,而只需从页面缓存中读取,这时,指向 mm_map_t 数据结构的指针被返回到页面故障的处理代码;部分代码如下:

 

for (page = page_hash(inode, offset); page ; page = page->next_hash)

 

/*函数page_hash()是从哈希表中找页面*/

{if (page->inode != inode)

      continue;       

if (page->offset != offset)      

   continue;

   /* 找到了特定页面 */  

   atomic_inc(&page->count);

   set_bit(PG_referenced, &page->flags);/*设访问位*/ 

   break;  }

   return page;

}

如果该页面不在缓存中,则必须从实际的文件系统映象中读取页面,这时Linux 内核首先分配物理页面然后从磁盘读取页面内容。

如果可能,Linux 还会预先读取文件中下页面内容到页面缓存中,而不等页面错误发生才去“请页面”,这样做是为了提高装入代码的速度。(有关代码在filemap.c中,如generic_file_readahead()等函数),这样,如果进程要连续访问页面,则下一页面的内容不必再次从文件中读取了,而只需从页面缓存中读取。

随着映象的读取和执行,页面缓存中的内容可能会增多,这时,Linux 可移走不再需要的页面。当系统中可用的物理内存量变小时,Linux 也会通过缩小页面缓存的大小而释放更多的物理内存页面。

2. 有关页面缓存的函数:

 

先看把读入的页面如何存于缓存,这要用到函数add_to_page_cache(),它完成把指定的“文件页面”记入页面缓存中。

 

static inline void add_to_page_cache(struct page * page,

   struct inode * inode, unsigned long offset)

{  /*设置有关页面域,引用数,页面使用方式,页面在文件中的偏移 */

    page->count++;

   page->flags &= ~((1 << PG_uptodate) | (1 << PG_error));  

     page->offset = offset;

     add_page_to_inode_queue(inode, page);/* 把页面加入inode节点队列*/  

     add_page_to_hash_queue(inode, page);/* 把页面加入哈page_hash_table[]*/}

 

注:inode的部分请看虚拟文件章节。

page_hash_table[]的定义:

 

extern struct page * page_hash_table[PAGE_HASH_SIZE];

 

下面是有关对哈唏表操作的部分代码:

static inline void add_page_to_inode_queue(struct inode * inode, struct page * page)

{  struct page **p = &inode->i_pages;/*指向物理页面*/

   inode->i_nrpages++;/*节点中调入内存的页面数目增1*/

   page->inode = inode; /*指向该页面来自的文件节点结构,相互连成链*/

     page->prev = NULL;   

if ((page->next = *p) != NULL)

page->next->prev = page;

   *p = page;  }

 

把页面加入哈表:

static inline void add_page_to_hash_queue(struct inode * inode, struct page * page)

{  struct page **p = &page_hash(inode,page->offset);

   page_cache_size++;/*哈希表中记录的页面数目加1*/

   set_bit(PG_referenced, &page->flags);/*设置访问位*/

page->age = PAGE_AGE_VALUE;/*设缓存中的页面‘年龄’为定值,为淘汰做准                                                          */

   page->prev_hash = NULL;

     if ((page->next_hash = *p) != NULL)

   page->next_hash->prev_hash = page; *p = page;

}

 

有关页面的刷新函数:

 

remove_page_from_hash_queue(page);   /*从哈希表中去掉页面*/  

remove_page_from_inode_queue(page);  /*inode节点中去掉页面*/