Linux使用的缓存

不管在硬件设计还是软件设计中,高速缓存是获得高性能的常用手段。Linux 使用了多种和内存管理相关的高速缓存。

1.缓冲区高速缓存

    缓冲区高速缓存中包含了由块设备使用的数据缓冲区。这些缓冲区中包含了从设备中读取的数据块或写入设备的数据块。缓冲区高速缓存由设备标识号和块标号索引,因此可以快速找出数据块。如果数据能够在缓冲区高速缓存中找到,则系统就没有必要在物理块设备上进行实际的读操作。

内核为每个缓冲区维护很多信息以有助于缓和写操作,这些信息包括一个“脏(dirty)”位,表示内存中的缓冲区已被修改,必须写到磁盘;还包括一个时间标志,表示缓冲区被刷新到磁盘之前已经在内存中停留了多长时间。因为缓冲区的有关信息被保存在缓冲区首部,所以,这些数据结构连同用户数据本身的缓冲区都需要维护。

缓冲区高速缓存的大小可以变化。当需要新缓冲区而现在又没有可用的缓冲区时,就按需分配页面。当空闲内存变得不足时,例如上一节看到的情况,就释放缓冲区并反复使用相应的页面。

2.页面高速缓存

    页面高速缓存是页面I/O操作访问数据所使用的磁盘高速缓存。我们在文件系统会看到,read( )、write( )和mmap( )系统调用对常规文件的访问都是通过页面高速缓存来完成的。因为页面I/O操作要传输整页数据,因此高速缓存中所保留的信息单元是一个整页面。一个页面包含的数据未必是物理上相邻的磁盘块,因此就不能使用设备号和块号来标识页面。相反,页面高速缓存中一个页面的标识是通过文件的索引节点和文件中的偏移量达到的。

与页面高速缓存有关的操作主要有三种:当访问的文件部分不在高速缓存中时增加一页面,当高速缓存变得太大时删除一页面,以及查找一个给定文件偏移量所在的页面。

3.交换高速缓存

    只有修改后的(脏)页面才保存在交换文件中。修改后的页面写入交换文件后,如果该页面再次被交换但未被修改时,就没有必要写入交换文件,相反,只需丢弃该页面。交换高速缓存实际包含了一个页面表项链表,系统的每个物理页面对应一个页面表项。对交换出的页面,该页面表项包含保存该页面的交换文件信息,以及该页面在交换文件中的位置信息。如果某个交换页面表项非零,则表明保存在交换文件中的对应物理页面没有被修改。如果这一页面在后续的操作中被修改,则处于交换缓存中的页面表项被清零。 Linux 需要从物理内存中交换出某个页面时,它首先分析交换缓存中的信息,如果缓存中包含该物理页面的一个非零页面表项,则说明该页面交换出内存后还没有被修改过,这时,系统只需丢弃该页面。

这里给出有关交换缓存的部分函数及功能:位于/linux/mm/swap_state.c 中。

 

初始化交换缓冲,设定大小,位置的函数:

extern unsigned long init_swap_cache(unsigned long, unsignedlong);

 

显示交换缓冲信息的函数:

extern void show_swap_cache_info(void);

 

加入交换缓冲的函数:

int add_to_swap_cache(unsigned long index, unsigned long entry)

参数index是进入缓冲区的索引(index是索引表中的某一项),entry是‘页面表项’(即此页面在交换文件中的位置记录,这个记录类似页面表项,参见交换机制)

 

复制被换出的页面:

extern void swap_duplicate(unsigned long);

当使用copy_page_tables()调用,来实现子进程在fork()时继承被换出的页面,可参阅交换机制一节。

 

从缓冲区中移去某页面

delete_from_swap_cache(page_nr);

 

4   硬件高速缓存

常见的硬件缓存是对页面表项的缓存,这一工作实际由处理器完成,其操作和具体的处理器硬件有关(但管理要由软件完成),对这一缓存接下来要进一步描述。

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, unsignedlong 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)

{  structpage **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节点中去掉页面*/

 


 

GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐