与linux0.11将普通内存和缓存分成不同的物理内存来处理不同,linux2.4.0将这两者抽象成统一的内存管理接口。而在阅读linux2.4.0源码的过程中,发现内存的换出和缓存的释放在结构上似乎具有很强的相似性,具体体现在以下几点:
- 内存换出时,要考虑到内存是不是最近有被使用(LRU Last Recent Used)。而高速缓存也不会在进程释放完就马上释放,会停留在内存一段时间(因为从硬盘读数据到内存会影响到OS的效率),也需要通过LRU去将高速缓存释放
- 内存换出是将暂时不用的内存写入到交换盘,待以后需要用到时通过do_page_fault换入到内存中;而高速缓存也需要将进程写入到内存的数据存储到硬盘中
1.Page 的LRU管理
关于page的LRU管理,有三个链表:active_list 、inactive_dirty_list、inactive_clean_list(与前两者全局的链表不同,这个在zone的管理结构中,因为这个已经向硬盘写入,但是没有回收而已)。
Page一般在使用的时候是存在于active_list;如果普通内存或者高速缓存因为长时间没使用(识别长时间没使用的方法--page老化),会将page从active_list移至inactive_dirty_list;在内存缺少的时候,当前进程或者一些周期性线程(例如kswapd)将page洗净(page_launder),将page从inactive_dirty_list移至inactive_clean_list;最后kreclaimd线程会将page从inactive_clean_list中移除(之前能从hash表中查到,之后就再也查不到page的数据了),释放至buddy系统中,供其他的进程内存分配使用。
2.Page的老化
Page的老化是LRU算法的基础,在linux2.4.0中,函数refill_inactive会查看page是否有受到访问,如果受到了访问,age_page_up_nolock;如果没有受到,就age_page_down_ageonly。直到page.age为0时,会deactivate_page_nolock,将page从active_list移至inactive_dirty_list。
refill_inactive-->refill_inactive_scan
refill_inactive_scan
{
....
//高速缓存的递增在这里做,普通内存的递增在swap_out(2.4.16将两者都抽象到Referenced)
//但是递减,普通内存和高速缓存都在这做
if (PageTestandClearReferenced(page)) {
age_page_up_nolock(page);
page_active = 1;
} else {
age_page_down_ageonly(page);
....
}
2.1普通内存的老化
查看普通内存是否受到访问,需要借助硬件机制。在MMU中,将page映射到虚拟内存,每次通过虚拟地址访问,都会通过pgd-->pmd-->pte去访问page。在MMU访问这些pgd/pmd/pte时,都会将_PAGE_ACCESSED置上,再通过ptep_test_and_clear_young来检查page是否受到访问。
swap_out会通过链表init_mm.mmlist.next来遍历所有进程的mm_struct结构体中的所有vma,从而确保遍历进程申请的所有虚拟地址,来检查page是否受到了访问,如下图try_to_swap_out。
(注:这样遍历似乎在效率上存在很大的问题,例如vma申请了,但是没有映射内存;还比如说page被映射到了MMU中,但是因为效率的原因不允许被换出。出现了这些情况,linux2.4.0似乎还是一如既往的遍历了所有的vma。所以后期似乎对这一块有所优化,将page映射的pte通过红黑树放在page的管理结构中,这样就可以通过page去查看pte是否被访问,或者将pte对应的page移至其他内存上,从而减少内存碎片,具体参考 深入剖析Linux内核反向映射机制_Smile to the World-CSDN博客)
refill_inactive-->swap_out-->swap_out_mm-->swap_out_vma-->swap_out_pgd-->swap_out_pmd-->try_to_swap_out
try_to_swap_out
{
...
onlist = PageActive(page);
/* Don't look at this pte if it's been accessed recently. */
//普通内存受到访问age递增
if (ptep_test_and_clear_young(page_table)) {
age_page_up(page);
goto out_failed;
}
//如果之前不再active_list中,refill_inactive_scan就不会将age递减。所以需要在这里递减
//据《linux内核源代码情景分析》第125页说:在do_swap_page的时候,page不会立即添加到active_list,而是在page_launder时在做,从看代码可知:如果是从hash中获取page,则会在active_list中,但是如果要从盘上读取,需要经过add_to_swap_cache,这里有可能将其添加到inactive_dirty_list。
if (!onlist)
/* The page is still mapped, so it can't be freeable... */
age_page_down_ageonly(page);
...
}
2.2高速缓存的老化
高速缓存的老化,从目前看的代码主要有三种情形:
1.通过sys_write/sys_read 初次读写buffer时
do_generic_file_read-->__add_to_page_cache-->lru_cache_add
generic_file_write-->__grab_cache_page-->add_to_page_cache_unique-->__add_to_page_cache-->lru_cache_add
lru_cache_add
{
...
add_page_to_active_list(page);
//这里作者说age为0的情况很少产生,估计是因为内存是脏的
/* This should be relatively rare */
if (!page->age)
deactivate_page_nolock(page);
...
}
2.通过sys_write/sys_read 再次读写buffer时
do_generic_file_read-->__find_page_nolock-->age_page_up
generic_file_write-->__grab_cache_page-->__find_lock_page-->__find_page_nolock-->age_page_up
age_page_up
{
if (!page->age)
activate_page(page);
/* The actual page aging bit */
page->age += PAGE_AGE_ADV;
if (page->age > PAGE_AGE_MAX)
page->age = PAGE_AGE_MAX;
}
3.通过block号读写高速缓存时:
getblk-->touch_buffer
//设置标志位,然后在refill_inactive_scan中递增age
#define touch_buffer(bh) SetPageReferenced(bh->b_page)
前两种方式主要是VFS在用,第三种方式主要是具体的文件系统(例如 ext2)用。
3.Active_list-->inactive_dirty_list
当age在refill_inactive_scan中减到0之后,page就彻底老化,需要将其和硬盘关联。所以linux对下面变量做了抽象:
page.index:page在盘(或者inode)内的页面偏移
address_space:page到盘(或者inode)的映射,在这个结构体中记录了页面是否clean/dirty/locked,并且有将页面写入/读出的方法。另外;对于普通内存来说是swapper_space,对于ext2高速缓存是inode.i_mapping(实体是inode.i_data,操作方法是ext2_aops)。
3.1普通内存
普通内存在refill_inactive_scan(或者try_to_swap_out)中将page.age递减置0,会在try_to_swap_out将pte和page的映射断开,之后如果进程需要通过MMU访问内存,需要通过do_page_fault,然后在hash表中查找重新建立映射了。
refill_inactive-->swap_out-->swap_out_mm-->swap_out_vma-->swap_out_pgd-->swap_out_pmd-->try_to_swap_out
try_to_swap_out
{
...
//这里pte被写0,所有后面!pte_dirty(pte)跳到drop_pte时是0
pte = ptep_get_and_clear(page_table);
flush_tlb_page(vma, address);
//初次进来,这个标志没置
if (PageSwapCache(page)) {
entry.val = page->index;
if (pte_dirty(pte))
set_page_dirty(page);
set_swap_pte:
swap_duplicate(entry);
set_pte(page_table, swp_entry_to_pte(entry));
drop_pte:
UnlockPage(page);
mm->rss--;
//将page放入到inactive_ditry_list
deactivate_page(page);
page_cache_release(page);
out_failed:
return 0;
}
...
//虚拟内存和物理内存映射一般是在do_page_fault中建立,出现写时复制或者swap时,如果vma有write_access,会pte_mkdirty。
//有两种情况没dirty:1.这个pte没被写过,被映射到零页,直接释放pte,在do_page_fault时在重新申请0页
2.如果这个页面是通过mmap,可以通过do_page_fault从硬盘中读取(另外pipe不是这种情况,因为pipe是通过file--inode的机制去访问page而不是MMU)
if (!pte_dirty(pte))
goto drop_pte;
/*
* Ok, it's really dirty. That means that
* we should either create a new swap cache
* entry for it, or we should write it back
* to its own backing store.
*/
//通过mmap建立映射,set_page_dirty将page加入到address_space的脏队列中,之后通过同步写入因硬盘
if (page->mapping) {
set_page_dirty(page);
goto drop_pte;
}
/*
* This is a dirty, swappable page. First of all,
* get a suitable swap entry for it, and make sure
* we have the swap cache set up to associate the
* page with that swap entry.
*/
//这里是MMU首次老化的内存
entry = get_swap_page();
if (!entry.val)
goto out_unlock_restore; /* No swap space left */
/* Add it to the swap cache and mark it dirty */
//将page和swap_space映射,将其加入其脏队列中,并添加置address_space和page.index组成的hash表中。
//然后将pte设置成交换盘的索引,以便do_page_fault时能从hash表或者硬盘中读入
add_to_swap_cache(page, entry);
set_page_dirty(page);
goto set_swap_pte;
...
}
try_to_swap_out将如下的结构写入到pte中,因为present位是0,所以之后再访问该page的时候,会产生页面中断,执行do_page_fault,然后去hash表或者从交换盘中读取page到内存中。
(注:swap具体函数清参考get_swap_page和swap_free。这里记录一些变量含义:
type表示交换盘的编号;
offset表示page在交换盘中的偏移;
swap_info[type].swap_map[offset]表示盘上页面的计数;
swap_list.head指向的是按照prior排列的swap_file链表,
swap_info[type].next指向这个链表的下一个,
swap_list.next指向下一个分配时应该选择的swap_file。)
3.2高速缓存
高速缓存在refill_inactive_scan中将page.age递减置0的时候,会立即将其放入inactive_dirty_list
refill_inactive-->refill_inactive_scan
refill_inactive_scan
{
if (PageTestandClearReferenced(page)) {
age_page_up_nolock(page);
page_active = 1;
} else {
age_page_down_ageonly(page);
//在alloc_page中的rmqueue中,从buddy系统中获取page时,已经将page.count置1,如果只有单个进程MMU的引用或者只有buffer_head的引用,page.count为2;如果是单个进程mmap,那么这个page既被映射到MMU中,又被加载到bh中,所以page.count为3.
//这里只是单独处理高速缓存的部分,处理MMU引用在try_to_swap_out。所以如果存在page->buffers,说明其被bh引用,page.count最大是2,才能释放到inactive_dirty_list。
if (page->age == 0 && page_count(page) <=
(page->buffers ? 2 : 1)) {
//将page加入到inactive_dirty_list中
deactivate_page_nolock(page);
page_active = 0;
} else {
page_active = 1;
}
....
}
4.Inactive_dirty_list-->inactive_clean_list
active_list-->inactive_dirty_list,主要涉及到函数refill_inactive,以及其调用的两个函数refill_inactive_scan和swap_out,其主要是用来识别哪些page是进程长期不使用的,可以将这些page腾出来,在内存短缺的时候供其他进程使用。
所以当内存短缺的时候,需要执行page_launder,将Inactive_dirty_list中不干净的页面洗净(同步到盘上),将其转入inactive_clean_list,待进一步的回收。
4.1普通内存
普通内存在try_to_swap_out因为pte_dirty已经set_page_dirty,所以在page_launder会检查page这个标志位是否置上,如果置上再将其写入到盘上
page_launder
{
...
//遍历整个inactive_dirty_listlist,注意这里如果不移到其他管理链表中,还在inactive_dirty_list中时,会将其插入到最后
maxscan = nr_inactive_dirty_pages;
while ((page_lru = inactive_dirty_list.prev) != &inactive_dirty_list &&
maxscan-- > 0) {
page = list_entry(page_lru, struct page, lru);
...
if (PageDirty(page)) {
int (*writepage)(struct page *) = page->mapping->a_ops->writepage;
int result;
if (!writepage)
goto page_active;
//在第二次进来的时候再向盘上写
/* First time through? Move it to the back of the list */
if (!launder_loop) {
list_del(page_lru);
list_add(page_lru, &inactive_dirty_list);
UnlockPage(page);
continue;
}
/* OK, do a physical asynchronous write to swap. */
ClearPageDirty(page);
page_cache_get(page);
spin_unlock(&pagemap_lru_lock);
//page->mapping->a_ops->writepage,如果是交换盘是swap_writepage,如果是mmap,则是ext2_writepage
result = writepage(page);
page_cache_release(page);
/* And re-start the thing.. */
spin_lock(&pagemap_lru_lock);
//写完,在这里退出此次page的操作
if (result != 1)
continue;
/* writepage refused to do anything */
set_page_dirty(page);
goto page_active;
}
...
else if (page->mapping && !PageDirty(page)) {
/*
* If a page had an extra reference in
* deactivate_page(), we will find it here.
* Now the page is really freeable, so we
* move it to the inactive_clean list.
*/
//从inactive_dirty_list移到inactive_clean_list
del_page_from_inactive_dirty_list(page);
add_page_to_inactive_clean_list(page);
UnlockPage(page);
cleaned_pages++;
}
...
}
swap_writepage-->rw_swap_page-->rw_swap_page_base
rw_swap_page_base
{
...
//从swap_info中获取要写的设备(dev)或者inode(swapf)
get_swaphandle_info(entry, &offset, &dev, &swapf);
//获取底层的参数dev block(物理)
if (dev) {
zones[0] = offset;
zones_used = 1;
block_size = PAGE_SIZE;
} else if (swapf) {
int i, j;
unsigned int block = offset
<< (PAGE_SHIFT - swapf->i_sb->s_blocksize_bits);
block_size = swapf->i_sb->s_blocksize;
for (i=0, j=0; j< PAGE_SIZE ; i++, j += block_size)
if (!(zones[i] = bmap(swapf,block++))) {
printk("rw_swap_page: bad swap file\n");
return 0;
}
zones_used = i;
dev = swapf->i_dev;
}
...
//在其中提交申请submit_bh
brw_page(rw, page, dev, zones, block_size);
..
}
4.2高速缓存
高速缓存不像普通内存换出那样在页面短缺的时候才同步,会周期性的(或者在balance_dirty发现dirty的bh过多时)通过bdflush去flush_dirty_buffers,以防止脏缓存的集中性写入。
balance_dirty-->wakeup_bdflush-->flush_dirty_buffers
bdflush-->flush_dirty_buffers
flush_dirty_buffers
{
...
bh = lru_list[BUF_DIRTY];
...
for (i = nr_buffers_type[BUF_DIRTY]; i-- > 0; bh = next) {
next = bh->b_next_free;
...
//提交申请submit_bh,写缓存
ll_rw_block(WRITE, 1, &bh);
...
}
...
}
另外因为内存短缺而执行page_launder,会将这些干净页面放入到inactive_clean_list中,如果遇到页面短缺程度比较大,也会在try_to_free_buffers中将一些老化但是没同步的内存同步到盘上,然后释放至inactive_clean_list。
page_launder
{
...
//遍历整个inactive_dirty_listlist,注意这里如果不移到其他管理链表中,还在inactive_dirty_list中时,会将其插入到最后
maxscan = nr_inactive_dirty_pages;
while ((page_lru = inactive_dirty_list.prev) != &inactive_dirty_list &&
maxscan-- > 0) {
page = list_entry(page_lru, struct page, lru);
...
if (page->buffers) {
...
//从inactive_dirty_list移除
del_page_from_inactive_dirty_list(page);
...
//释放bh,如果短缺(wait)严重会同步缓存
/* Try to free the page buffers. */
clearedbuf = try_to_free_buffers(page, wait);
...
if (!clearedbuf) {
...
else/* page->mapping && page_count(page) == 2 */ {
//添加至inactive_clean_list
add_page_to_inactive_clean_list(page);
cleaned_pages++;
}
...
}
}
try_to_free_buffers
{
...
do {
struct buffer_head *p = tmp;
tmp = tmp->b_this_page;
//检查是否写到盘上
if (buffer_busy(p))
goto busy_buffer_page;
} while (tmp != bh);
...
//释放page对应的bh至unused_list,如果有足够的bh,则将其释放至slab
if (p->b_dev != B_FREE) {
remove_inode_queue(p);
__remove_from_queues(p);
} else
__remove_from_free_list(p, index);
__put_unused_buffer_head(p);
//将bh对page的引用计数释放
page->buffers = NULL;
page_cache_release(page);
...
busy_buffer_page:
if (wait) {
//向盘上同步
sync_page_buffers(bh, wait);
/* We waited synchronously, so we can free the buffers. */
if (wait > 1 && !loop) {
loop = 1;
goto cleaned_buffers_try_again;
}
}
return 0;
}
注:bh和page是两套系统:
1.bh在使用时,存放在lru_list中
2.free_list主要是用于一些磁盘的管理结构,例如super,inode.i_block等,这些结构都是内核使用的,不需要通过page去访问。因为这个page所对应的多个block可能是不同设备中的不连续的block。
3.unused_list相比于free_list,更偏重于进程的访问(sys_read/sys_write),一个page和inode中连续的block是相对应的,page.index指的是page在inode中的逻辑偏移。但是在free_list的bh不足时,会向unused_list申请bh。
4.在内核释放一些不需要的bh时,是通过brelse释放,存放在lru_list中;
如果内核在执行过程中出问题,需要将之前依赖的一些bh释放时,会使用bforget接口,将其释放至freelist,但是还能通过hash(dev,block)找到bh;
但是可能因为一些特殊情况(例如set_blocksize改变文件系统的逻辑块大小),会将brelse彻底释放的bh放到free_list中(因为blocksize已经改变,要重新将其分配)。因为bh已无效,hash(dev,block)是断开的;
如果要将bh释放给slab,需要通过__put_unused_buffer_head(一般是进程出错自己释放,或者由try_to_free_buffers释放page的时候一起释放)
5.总结
是否在hash中 | 是否指向实际内存(page) | |
lru_list | 在 | 有内存 |
free_list | 在(小概率不在) | 有内存 |
unused_list | 不在 | 没内存 |
5.Inactive_clean_list-->buddy系统
这主要是将page彻底回收(与address_space断开),然后通过free_page释放给buddy系统。
kreclaimd
{
...
//将page从address_space断开,并且将hash移除
page = reclaim_page(zone);
...
//释放给buddy
__free_page(page);
...
}
reclaim_page
{
...
maxscan = zone->inactive_clean_pages;
while ((page_lru = zone->inactive_clean_list.prev) !=
&zone->inactive_clean_list && maxscan--) {
page = list_entry(page_lru, struct page, lru);
...
//这两个分别从swapper_space,和inode.i_mapping中移除,解除hash。
/* OK, remove the page from the caches. */
if (PageSwapCache(page)) {
__delete_from_swap_cache(page);
goto found_page;
}
if (page->mapping) {
__remove_inode_page(page);
goto found_page;
}
...
}
...
found_page:
//从inactive_clean_list中移除
del_page_from_inactive_clean_list(page);
...
}
6.总结
普通内存换出:
1.在refill_inactive函数中分别通过try_to_swap_out检查是否受到访问,并在refill_inactive_scan将其老化。如果长时间没受到访问,refill_inactive会在try_to_swap_out后面将其和pte解除映射,存放至inactive_dirty_list。
2.在内存短缺的时候,会通过page_launder将内存写到盘上,并添加至inactive_clean_list。
3.如果该zone的free_page较少,会通过kreclaimd将其释放给buddy
注:当page释放给inactive_dirty_list时,因为已经和pte断开,所以在此访问时只能通过do_page_fault中的do_swap_page将page换入。如果此时page还没释放给buddy,可以在hash中查找;如果已经完全释放,则需要通过read_swap_cache(最后调用rw_swap_page_base读)将page从盘上读进来
高速缓存的释放:
1.进程会通过sys_read/sys_write(内核会通过getblk)增加page.age,并且在refill_inactive中的refill_inactive_scan将其老化;如果彻底老化,refill_inactive_scan后续会将其移至inactive_dirty_list
2.平时会有bdflush(或者在balance_dirty发现dirty的bh过多时)去flush_dirty_buffers,将缓存同步,当内存短缺时,会通过page_launder将其移至inactive_clean_list(如果特别短缺,page_launder也会在try_to_free_buffers中做脏缓存的同步)
3.如果该zone的free_page较少,会通过kreclaimd将其释放给buddy,之后要读只能再次从盘上读取。
本文参考《Linux内核源代码情景分析》