Bootstrap

内核内存回收关键隐藏变量之page引用计数

在分析内核内存回收源码时,page引用计数并不显眼,但是page引用计数对page的内存回收至关重要。本文基于linux-4.18.0-240版本内核源码,总结下文件页page的引用计数的相关细节。首先是get_page()和put_page()函数,分别令page引用计数加1和减1.

  1. //page引用计数加1
  2. static inline void get_page(struct page *page)
  3. {
  4.     page_ref_inc(page);
  5. }
  6. //page引用计数减1
  7. static inline void put_page(struct page *page)
  8. {
  9.     if (put_page_testzero(page))
  10.         __put_page(page);
  11. }

以read系统调用读文件为例,最后执行到generic_file_buffered_read函数,先page_cache_alloc()分配一个文件页page,此时的page引用计数是0。

  1. static ssize_t generic_file_buffered_read(struct kiocb *iocb,
  2.         struct iov_iter *iter, ssize_t written)
  3. {
  4.     page = page_cache_alloc(mapping);
  5.     error = add_to_page_cache_lru(page, mapping, index,mapping_gfp_constraint(mapping, GFP_KERNEL));
  6. }

然后执行add_to_page_cache_lru函数把page添加到radix/xrray tree,接着把page添加到lru缓存和lru链表

  1. int add_to_page_cache_lru(struct page *page, struct address_space *mapping,
  2.                 pgoff_t offset, gfp_t gfp_mask)
  3. {
  4.     //page添加到radix/xrray tree时令page引用计数加1
  5.     ret = __add_to_page_cache_locked(page, mapping, offset,gfp_mask, &shadow);
  6.     //page添加到lru缓存时令page引用计数加1,把pagelru缓存移动到lru链表时再令page引用计数减1
  7.     lru_cache_add(page);
  8. }

在把page添加到radix/xrray tree时令page引用计数加1

  1. static int __add_to_page_cache_locked(struct page *page,
  2.                       struct address_space *mapping,
  3.                       pgoff_t offset, gfp_t gfp_mask,
  4.                       void **shadowp)
  5. {
  6.     XA_STATE(xas, &mapping->i_pages, offset);
  7.     .........
  8.     //page引用计数加1
  9.     get_page(page);
  10.     page->mapping = mapping;
  11.     page->index = offset;
  12.     .........
  13.     old = xas_load(&xas);
  14.     xas_store(&xas, page);
  15.     mapping->nrpages++;
  16.     .........
  17. }

然后执行lru_cache_add函数page添加到lru缓存时令page引用计数加1pagelru缓存移动到lru链表时再令page引用计数减1,函数流程是lru_cache_add->__lru_cache_add->__pagevec_lru_add->release_pages,关键函数如下:

把page添加到lru缓存时令page引用计数加1

  1. static void __lru_cache_add(struct page *page)
  2. {
  3.     struct pagevec *pvec = &get_cpu_var(lru_add_pvec);
  4.     //page引用计数加1
  5.     get_page(page);
  6.     if (!pagevec_add(pvec, page) || PageCompound(page))
  7.         __pagevec_lru_add(pvec);
  8.     put_cpu_var(lru_add_pvec);
  9. }
  10. //page添加到lru缓存
  11. static inline unsigned pagevec_add(struct pagevec *pvec, struct page *page)
  12. {
  13.     pvec->pages[pvec->nr++] = page;
  14.     return pagevec_space(pvec);
  15. }

接着把page添加到lru链表时令page引用计数减1

  1. void __pagevec_lru_add(struct pagevec *pvec)
  2. {
  3.     pagevec_lru_move_fn(pvec, __pagevec_lru_add_fn, NULL);
  4. }
  5. static void __pagevec_lru_add_fn(struct page *page, struct lruvec *lruvec,
  6.                  void *arg)
  7. {
  8.     SetPageLRU(page);
  9.     //page添加到lru链表
  10.     add_page_to_lru_list(page, lruvec, lru);
  11. }
  12. void release_pages(struct page **pages, int nr)
  13. {
  14.     for (i = 0; i < nr; i++) {
  15.         struct page *page = pages[i];
  16.         //page引用计数减1。如果之后page引用计数是0说明没有进程使用该page了,然后执行free_unref_page_list()page释放回伙伴系统。
  17.         if (!put_page_testzero(page))
  18.             continue;
  19.         .........  
  20.         list_add(&page->lru, &pages_to_free);
  21.     }
  22.     free_unref_page_list(&pages_to_free);
  23. }

如果是write系统调用对文件页page有写操作,则还要为page分配buffer_head(即bh),然后建立文件页page和bh的联系,令page引用计数加1。源码流程如下(以ext4文件系统为例):vfs_write->new_sync_write->ext4_file_write_iter->__generic_file_write_iter->generic_perform_write->ext4_da_write_begin->__block_write_begin_int->create_page_buffers->create_empty_buffers->attach_page_buffers,

  1. static inline void attach_page_buffers(struct page *page,
  2.         struct buffer_head *head)
  3. {
  4.     //page引用计数加1
  5.     get_page(page);
  6.     //标记pageprivate属性
  7.     SetPagePrivate(page);
  8.     //建立pagebh的联系,本质是page->private=bh
  9.     set_page_private(page, (unsigned long)head);
  10. }

OK,此时page的引用计数是2。接着来到page的内存回收,执行shrink_inactive_list()函数,这里把该函数的关键源码列下:

  1. static unsigned long shrink_inactive_list(unsigned long nr_to_scan, struct lruvec *lruvec,
  2.              struct scan_control *sc, enum lru_list lru)
  3. {
  4.     spin_lock_irq(&pgdat->lru_lock);
  5.     //根据nr_to_scan数目从inactive lru链表隔离page符合条件的pagepage_list链表,同时都令这些page的引用计数加1
  6.     nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &page_list,&nr_scanned, sc, isolate_mode, lru);
  7.     spin_unlock_irq(&pgdat->lru_lock);
  8.     ..........
  9.     nr_reclaimed = shrink_page_list(&page_list, pgdat, sc, 0,&stat, false);
  10.     ..........
  11.     spin_lock_irq(&pgdat->lru_lock);
  12.     //没有成功内存回收的page再移动回 active/inactive lru链表,page引用计数减1。如果page引用计数是0说明没人用了,再移动回page_list
  13.     putback_inactive_pages(lruvec, &page_list);
  14.     spin_unlock_irq(&pgdat->lru_lock);
  15.     ..........
  16.     //释放page_list上引用计数是0page
  17.     free_unref_page_list(&page_list);
  18. }

看下隔离page执行的isolate_lru_pages函数,主要是page引用计数加1

  1. static unsigned long isolate_lru_pages(unsigned long nr_to_scan,
  2.         struct lruvec *lruvec, struct list_head *dst,
  3.         unsigned long *nr_scanned, struct scan_control *sc,
  4.         isolate_mode_t mode, enum lru_list lru)
  5. {
  6.         //page符合内存回收条件则清理pagePageLRU属性,并令page引用计数加1,返回0,否则返回负数
  7.         switch (__isolate_lru_page(page, mode)) {
  8.         case 0:
  9.             ........
  10.             //把符合内存回收条件的pagelru链表移动到dst临时链表
  11.             list_move(&page->lru, dst);
  12.             break;
  13.         case -EBUSY:
  14.             list_move(&page->lru, src);
  15.             continue;
  16.         }
  17. }
  18. int __isolate_lru_page(struct page *page, isolate_mode_t mode)
  19. {
  20.     int ret = -EINVAL;
  21.     //关键点,如果page已经从lru链条剔除,page隔离失败
  22.     if (!PageLRU(page))
  23.         return ret;
  24.     ret = -EBUSY;
  25.     //page引用计数不是0则加1并返回true。否则说明page应用计数是0,返回false,这种page已经没进程在使用了,已经不在LRU链表了
  26.     if (likely(get_page_unless_zero(page))){
  27.         //page将要从activeinactive lru链表移除,于是清理pagePageLRU属性
  28.         ClearPageLRU(page);
  29.         ret = 0;
  30.     }
  31.     return ret;
  32. }

注意,隔离page时,在对spin_locklru_lock加锁后,要令page引用计数加1,这个非常重要。此时其他进程就无法释放这个page了!如果在隔离page前,这个page可能被其他进程释放回伙伴系统,那page将没有LRU属性,此时__isolate_lru_page函数里的if (!PageLRU(page))将起到作用,导致隔离page失败。如果隔离page时没有对page引用计数加1,那page将可能并发被其他进程释放回伙伴系统,或者被释放回伙伴系统并且被新的进程分配并加入新的lru链表。这种情况下,page->mapping将发生变化,与原始的mapping就不一样了,可以据此判断出这种异常。

好的,page引用计数此时是3,接着来到shrink_page_list()函数对page进行真正的内存回收。

  1. static unsigned long shrink_page_list(struct list_head *page_list,
  2.                       struct pglist_data *pgdat,
  3.                       struct scan_control *sc,
  4.                       enum ttu_flags ttu_flags,
  5.                       struct reclaim_stat *stat,
  6.                       bool force_reclaim)
  7. {
  8.     while (!list_empty(page_list)) {
  9.         ............
  10.         //page有映射的bh
  11.         if (page_has_private(page)) {
  12.             //pagebh解除联系,并且令page引用计数减1
  13.             if (!try_to_release_page(page, sc->gfp_mask))
  14.                 goto activate_locked;
  15.         }
  16.         ...........
  17.         //pageradix treeaddress_space 剔除,如果page引用计数是2则清0,返回1page可以释放。否则page还再被其他进程使用,返回0,不能释放
  18.         else if (!mapping || !__remove_mapping(mapping, page, true))
  19.             goto keep_locked;
  20. free_it:
  21.         nr_reclaimed++;
  22.         list_add(&page->lru, &free_pages);
  23.         continue;
  24. activate_locked:
  25.         //重新设置page active
  26.         SetPageActive(page);
  27. keep_locked:
  28.         unlock_page(page);
  29. keep:
  30.         //到这里,page本轮不能回收,暂存ret_pages链表然后再移回activeinactive lru链表
  31.         list_add(&page->lru, &ret_pages);
  32.     }
  33.     //释放free_pages上的page到伙伴系统
  34.     free_unref_page_list(&free_pages);
  35.     ...............
  36. }

如果page有bh则if (page_has_private(page))成立,然后执行try_to_release_page解除page和bh的联系,并page引用计数减1,源码流程是try_to_release_page->ext4_releasepage->try_to_free_buffers->drop_buffers->__clear_page_buffers,

  1. static void __clear_page_buffers(struct page *page)
  2. {
  3.     //解除pagebh的联系
  4.     ClearPagePrivate(page);
  5.     set_page_private(page, 0);
  6.     //page引用计数减1
  7.     put_page(page);
  8. }

此时page的引用计数是2,然后执行到__remove_mapping()函数。

  1. static int __remove_mapping(struct address_space *mapping, struct page *page,
  2.                 bool reclaimed)
  3. {
  4.     refcount = 2;
  5.     //page引用计数是2则对page引用计数清0,并返回true,这个page可以释放了。否则page引用计数不是2则保持引用计数并返回false,这个page不能释放
  6.     if (!page_ref_freeze(page, refcount))
  7.         goto cannot_free;
  8.     .............
  9.     //pageradix tree 剔除
  10.     __delete_from_page_cache(page, shadow);
  11.     return 1;
  12. cannot_free:
  13.     return 0;  
  14. }

主要作用是:把page从radix tree、address_space 剔除,如果page引用计数是2则清0,返回1page可以释放。否则page还再被其他进程使用,返回0,不能释放

好的,正常情况page引用计数此时就是0了,然后就可以释放掉这个page了。如果page因为是脏页、writeback页等导致page回收失败,page就要暂存在page_list链表。shrink_page_list()函数执行后,再执行putback_inactive_pages()函数把page移动回lru链表,源码如下:

  1. static void putback_inactive_pages(struct lruvec *lruvec, struct list_head *page_list)
  2. {
  3.     struct zone_reclaim_stat *reclaim_stat = &lruvec->reclaim_stat;
  4.     struct pglist_data *pgdat = lruvec_pgdat(lruvec);
  5.     LIST_HEAD(pages_to_free);
  6.     while (!list_empty(page_list)) {
  7.         struct page *page = lru_to_page(page_list);
  8.         int lru;
  9.         lruvec = mem_cgroup_page_lruvec(page, pgdat);
  10.         //page要添加到inactive lru,设置LRU属性
  11.         SetPageLRU(page);
  12.         lru = page_lru(page);
  13.         //page添加到lru链表,并增加lru链表page
  14.         add_page_to_lru_list(page, lruvec, lru);
  15.         //page引用计数减1,减1后如果是0就说明page没人用了,可以释放了
  16.         if (put_page_testzero(page)) {
  17.             //清理pagelRUactive属性
  18.             __ClearPageLRU(page);
  19.             __ClearPageActive(page);
  20.             //pagelru链表剔除,并减少lru链表的page
  21.             del_page_from_lru_list(page, lruvec, lru);
  22.             //page再移动到pages_to_free链表,之后就直接释放掉
  23.             list_add(&page->lru, &pages_to_free);
  24.     }
  25.     list_splice(&pages_to_free, page_list);
  26. }

这里对page引用计数减1,因为之前隔离收该page时令page引用计数加1了,二者对冲掉。

OK,本文到这里基本就结束了。page引用计数可能有点复杂,简单说,当page要启用一个新功能时,就要对page引用计数加1,而回收page时要一一对应对page引用计数减1。水平有限,如有错误请指出。

;