当前位置:   article > 正文

内存换出和缓冲区释放_释放内核缓冲区

释放内核缓冲区

与linux0.11将普通内存和缓冲区分成不同的物理内存来处理不同,linux2.4.0将这两者抽象成统一的内存管理接口。而在阅读linux2.4.0源码的过程中,发现内存的换出和缓冲区的释放在结构上似乎具有很强的相似性,具体体现在以下几点:

  1. 内存换出时,要考虑到内存是不是最近有被使用(LRU    Last Recent Used)。而缓冲区也不会在进程释放完就马上释放,会停留在内存一段时间(因为从硬盘读数据到内存会影响到OS的效率),也需要通过LRU去将缓冲区释放
  2. 内存换出是将暂时不用的内存写入到交换盘,待以后需要用到时通过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。

  1. refill_inactive-->refill_inactive_scan
  2. refill_inactive_scan
  3. {
  4. ....
  5. //缓冲区的递增在这里做,普通内存的递增在swap_out(2.4.16将两者都抽象到Referenced)
  6. //但是递减,普通内存和缓冲区都在这做
  7. if (PageTestandClearReferenced(page)) {
  8. age_page_up_nolock(page);
  9. page_active = 1;
  10. } else {
  11. age_page_down_ageonly(page);
  12. ....
  13. }

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博客

  1. refill_inactive-->swap_out-->swap_out_mm-->swap_out_vma-->swap_out_pgd-->swap_out_pmd-->try_to_swap_out
  2. try_to_swap_out
  3. {
  4. ...
  5. onlist = PageActive(page);
  6. /* Don't look at this pte if it's been accessed recently. */
  7. //普通内存受到访问age递增
  8. if (ptep_test_and_clear_young(page_table)) {
  9. age_page_up(page);
  10. goto out_failed;
  11. }
  12. //如果之前不再active_list中,refill_inactive_scan就不会将age递减。所以需要在这里递减
  13. //据《linux内核源代码情景分析》第125页说:在do_swap_page的时候,page不会立即添加到active_list,而是在page_launder时在做,从看代码可知:如果是从hash中获取page,则会在active_list中,但是如果要从盘上读取,需要经过add_to_swap_cache,这里有可能将其添加到inactive_dirty_list。
  14. if (!onlist)
  15. /* The page is still mapped, so it can't be freeable... */
  16. age_page_down_ageonly(page);
  17. ...
  18. }

 2.2缓冲区的老化

        缓冲区的老化,从目前看的代码主要有三种情形:

        1.通过sys_write/sys_read 初次读写buffer时

  1. do_generic_file_read-->__add_to_page_cache-->lru_cache_add
  2. generic_file_write-->__grab_cache_page-->add_to_page_cache_unique-->__add_to_page_cache-->lru_cache_add
  3. lru_cache_add
  4. {
  5. ...
  6. add_page_to_active_list(page);
  7. //这里作者说age为0的情况很少产生,估计是因为内存是脏的
  8. /* This should be relatively rare */
  9. if (!page->age)
  10. deactivate_page_nolock(page);
  11. ...
  12. }

        2.通过sys_write/sys_read 再次读写buffer时

  1. do_generic_file_read-->__find_page_nolock-->age_page_up
  2. generic_file_write-->__grab_cache_page-->__find_lock_page-->__find_page_nolock-->age_page_up
  3. age_page_up
  4. {
  5. if (!page->age)
  6. activate_page(page);
  7. /* The actual page aging bit */
  8. page->age += PAGE_AGE_ADV;
  9. if (page->age > PAGE_AGE_MAX)
  10. page->age = PAGE_AGE_MAX;
  11. }

        3.通过block号读写缓冲区时:

  1. getblk-->touch_buffer
  2. //设置标志位,然后在refill_inactive_scan中递增age
  3. #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表中查找重新建立映射了。

  1. refill_inactive-->swap_out-->swap_out_mm-->swap_out_vma-->swap_out_pgd-->swap_out_pmd-->try_to_swap_out
  2. try_to_swap_out
  3. {
  4. ...
  5. //这里pte被写0,所有后面!pte_dirty(pte)跳到drop_pte时是0
  6. pte = ptep_get_and_clear(page_table);
  7. flush_tlb_page(vma, address);
  8. //初次进来,这个标志没置
  9. if (PageSwapCache(page)) {
  10. entry.val = page->index;
  11. if (pte_dirty(pte))
  12. set_page_dirty(page);
  13. set_swap_pte:
  14. swap_duplicate(entry);
  15. set_pte(page_table, swp_entry_to_pte(entry));
  16. drop_pte:
  17. UnlockPage(page);
  18. mm->rss--;
  19. //page放入到inactive_ditry_list
  20. deactivate_page(page);
  21. page_cache_release(page);
  22. out_failed:
  23. return 0;
  24. }
  25. ...
  26. //虚拟内存和物理内存映射一般是在do_page_fault中建立,出现写时复制或者swap时,如果vma有write_access,会pte_mkdirty。
  27. //有两种情况没dirty:1.这个pte没被写过,被映射到零页,直接释放pte,在do_page_fault时在重新申请0
  28. 2.如果这个页面是通过mmap,可以通过do_page_fault从硬盘中读取(另外pipe不是这种情况,因为pipe是通过file--inode的机制去访问page而不是MMU)
  29. if (!pte_dirty(pte))
  30. goto drop_pte;
  31. /*
  32. * Ok, it's really dirty. That means that
  33. * we should either create a new swap cache
  34. * entry for it, or we should write it back
  35. * to its own backing store.
  36. */
  37. //通过mmap建立映射,set_page_dirty将page加入到address_space的脏队列中,之后通过同步写入因硬盘
  38. if (page->mapping) {
  39. set_page_dirty(page);
  40. goto drop_pte;
  41. }
  42. /*
  43. * This is a dirty, swappable page. First of all,
  44. * get a suitable swap entry for it, and make sure
  45. * we have the swap cache set up to associate the
  46. * page with that swap entry.
  47. */
  48. //这里是MMU首次老化的内存
  49. entry = get_swap_page();
  50. if (!entry.val)
  51. goto out_unlock_restore; /* No swap space left */
  52. /* Add it to the swap cache and mark it dirty */
  53. //将page和swap_space映射,将其加入其脏队列中,并添加置address_space和page.index组成的hash表中。
  54. //然后将pte设置成交换盘的索引,以便do_page_fault时能从hash表或者硬盘中读入
  55. add_to_swap_cache(page, entry);
  56. set_page_dirty(page);
  57. goto set_swap_pte;
  58. ...
  59. }

 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

  1. refill_inactive-->refill_inactive_scan
  2. refill_inactive_scan
  3. {
  4. if (PageTestandClearReferenced(page)) {
  5. age_page_up_nolock(page);
  6. page_active = 1;
  7. } else {
  8. age_page_down_ageonly(page);
  9. //在alloc_page中的rmqueue中,从buddy系统中获取page时,已经将page.count1,如果只有单个进程MMU的引用或者只有buffer_head的引用,page.count2;如果是单个进程mmap,那么这个page既被映射到MMU中,又被加载到bh中,所以page.count3.
  10. //这里只是单独处理缓冲区的部分,处理MMU引用在try_to_swap_out。所以如果存在page->buffers,说明其被bh引用,page.count最大是2,才能释放到inactive_dirty_list。
  11. if (page->age == 0 && page_count(page) <=
  12. (page->buffers ? 2 : 1)) {
  13. //page加入到inactive_dirty_list中
  14. deactivate_page_nolock(page);
  15. page_active = 0;
  16. } else {
  17. page_active = 1;
  18. }
  19. ....
  20. }

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这个标志位是否置上,如果置上再将其写入到盘上

  1. page_launder
  2. {
  3. ...
  4. //遍历整个inactive_dirty_listlist,注意这里如果不移到其他管理链表中,还在inactive_dirty_list中时,会将其插入到最后
  5. maxscan = nr_inactive_dirty_pages;
  6. while ((page_lru = inactive_dirty_list.prev) != &inactive_dirty_list &&
  7. maxscan-- > 0) {
  8. page = list_entry(page_lru, struct page, lru);
  9. ...
  10. if (PageDirty(page)) {
  11. int (*writepage)(struct page *) = page->mapping->a_ops->writepage;
  12. int result;
  13. if (!writepage)
  14. goto page_active;
  15. //在第二次进来的时候再向盘上写
  16. /* First time through? Move it to the back of the list */
  17. if (!launder_loop) {
  18. list_del(page_lru);
  19. list_add(page_lru, &inactive_dirty_list);
  20. UnlockPage(page);
  21. continue;
  22. }
  23. /* OK, do a physical asynchronous write to swap. */
  24. ClearPageDirty(page);
  25. page_cache_get(page);
  26. spin_unlock(&pagemap_lru_lock);
  27. //page->mapping->a_ops->writepage,如果是交换盘是swap_writepage,如果是mmap,则是ext2_writepage
  28. result = writepage(page);
  29. page_cache_release(page);
  30. /* And re-start the thing.. */
  31. spin_lock(&pagemap_lru_lock);
  32. //写完,在这里退出此次page的操作
  33. if (result != 1)
  34. continue;
  35. /* writepage refused to do anything */
  36. set_page_dirty(page);
  37. goto page_active;
  38. }
  39. ...
  40. else if (page->mapping && !PageDirty(page)) {
  41. /*
  42. * If a page had an extra reference in
  43. * deactivate_page(), we will find it here.
  44. * Now the page is really freeable, so we
  45. * move it to the inactive_clean list.
  46. */
  47. //从inactive_dirty_list移到inactive_clean_list
  48. del_page_from_inactive_dirty_list(page);
  49. add_page_to_inactive_clean_list(page);
  50. UnlockPage(page);
  51. cleaned_pages++;
  52. }
  53. ...
  54. }
  1. swap_writepage-->rw_swap_page-->rw_swap_page_base
  2. rw_swap_page_base
  3. {
  4. ...
  5. //从swap_info中获取要写的设备(dev)或者inode(swapf)
  6. get_swaphandle_info(entry, &offset, &dev, &swapf);
  7. //获取底层的参数dev block(物理)
  8. if (dev) {
  9. zones[0] = offset;
  10. zones_used = 1;
  11. block_size = PAGE_SIZE;
  12. } else if (swapf) {
  13. int i, j;
  14. unsigned int block = offset
  15. << (PAGE_SHIFT - swapf->i_sb->s_blocksize_bits);
  16. block_size = swapf->i_sb->s_blocksize;
  17. for (i=0, j=0; j< PAGE_SIZE ; i++, j += block_size)
  18. if (!(zones[i] = bmap(swapf,block++))) {
  19. printk("rw_swap_page: bad swap file\n");
  20. return 0;
  21. }
  22. zones_used = i;
  23. dev = swapf->i_dev;
  24. }
  25. ...
  26. //在其中提交申请submit_bh
  27. brw_page(rw, page, dev, zones, block_size);
  28. ..
  29. }

4.2高速缓存

        高速缓存不像普通内存换出那样在页面短缺的时候才同步,会周期性的(或者在balance_dirty发现dirty的bh过多时)通过bdflush去flush_dirty_buffers,以防止脏缓存的集中性写入。

  1. balance_dirty-->wakeup_bdflush-->flush_dirty_buffers
  2. bdflush-->flush_dirty_buffers
  3. flush_dirty_buffers
  4. {
  5. ...
  6. bh = lru_list[BUF_DIRTY];
  7. ...
  8. for (i = nr_buffers_type[BUF_DIRTY]; i-- > 0; bh = next) {
  9. next = bh->b_next_free;
  10. ...
  11. //提交申请submit_bh,写缓存
  12. ll_rw_block(WRITE, 1, &bh);
  13. ...
  14. }
  15. ...
  16. }

        另外因为内存短缺而执行page_launder,会将这些干净页面放入到inactive_clean_list中,如果遇到页面短缺程度比较大,也会在try_to_free_buffers中将一些老化但是没同步的内存同步到盘上,然后释放至inactive_clean_list。

  1. page_launder
  2. {
  3. ...
  4. //遍历整个inactive_dirty_listlist,注意这里如果不移到其他管理链表中,还在inactive_dirty_list中时,会将其插入到最后
  5. maxscan = nr_inactive_dirty_pages;
  6. while ((page_lru = inactive_dirty_list.prev) != &inactive_dirty_list &&
  7. maxscan-- > 0) {
  8. page = list_entry(page_lru, struct page, lru);
  9. ...
  10. if (page->buffers) {
  11. ...
  12. //从inactive_dirty_list移除
  13. del_page_from_inactive_dirty_list(page);
  14. ...
  15. //释放bh,如果短缺(wait)严重会同步缓存
  16. /* Try to free the page buffers. */
  17. clearedbuf = try_to_free_buffers(page, wait);
  18. ...
  19. if (!clearedbuf) {
  20. ...
  21. else/* page->mapping && page_count(page) == 2 */ {
  22. //添加至inactive_clean_list
  23. add_page_to_inactive_clean_list(page);
  24. cleaned_pages++;
  25. }
  26. ...
  27. }
  28. }
  1. try_to_free_buffers
  2. {
  3. ...
  4. do {
  5. struct buffer_head *p = tmp;
  6. tmp = tmp->b_this_page;
  7. //检查是否写到盘上
  8. if (buffer_busy(p))
  9. goto busy_buffer_page;
  10. } while (tmp != bh);
  11. ...
  12. //释放page对应的bh至unused_list,如果有足够的bh,则将其释放至slab
  13. if (p->b_dev != B_FREE) {
  14. remove_inode_queue(p);
  15. __remove_from_queues(p);
  16. } else
  17. __remove_from_free_list(p, index);
  18. __put_unused_buffer_head(p);
  19. //将bh对page的引用计数释放
  20. page->buffers = NULL;
  21. page_cache_release(page);
  22. ...
  23. busy_buffer_page:
  24. if (wait) {
  25. //向盘上同步
  26. sync_page_buffers(bh, wait);
  27. /* We waited synchronously, so we can free the buffers. */
  28. if (wait > 1 && !loop) {
  29. loop = 1;
  30. goto cleaned_buffers_try_again;
  31. }
  32. }
  33. return 0;
  34. }

注: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系统。

  1. kreclaimd
  2. {
  3. ...
  4. //pageaddress_space断开,并且将hash移除
  5. page = reclaim_page(zone);
  6. ...
  7. //释放给buddy
  8. __free_page(page);
  9. ...
  10. }
  11. reclaim_page
  12. {
  13. ...
  14. maxscan = zone->inactive_clean_pages;
  15. while ((page_lru = zone->inactive_clean_list.prev) !=
  16. &zone->inactive_clean_list && maxscan--) {
  17. page = list_entry(page_lru, struct page, lru);
  18. ...
  19. //这两个分别从swapper_space,和inode.i_mapping中移除,解除hash。
  20. /* OK, remove the page from the caches. */
  21. if (PageSwapCache(page)) {
  22. __delete_from_swap_cache(page);
  23. goto found_page;
  24. }
  25. if (page->mapping) {
  26. __remove_inode_page(page);
  27. goto found_page;
  28. }
  29. ...
  30. }
  31. ...
  32. found_page:
  33. //从inactive_clean_list中移除
  34. del_page_from_inactive_clean_list(page);
  35. ...
  36. }

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内核源代码情景分析》

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号