在分析内核内存回收源码时,page引用计数并不显眼,但是page引用计数对page的内存回收至关重要。本文基于linux-4.18.0-240版本内核源码,总结下文件页page的引用计数的相关细节。首先是get_page()和put_page()函数,分别令page引用计数加1和减1.
- //page引用计数加1
- static inline void get_page(struct page *page)
- {
- page_ref_inc(page);
- }
- //page引用计数减1
- static inline void put_page(struct page *page)
- {
- if (put_page_testzero(page))
- __put_page(page);
- }
以read系统调用读文件为例,最后执行到generic_file_buffered_read函数,先page_cache_alloc()分配一个文件页page,此时的page引用计数是0。
- static ssize_t generic_file_buffered_read(struct kiocb *iocb,
- struct iov_iter *iter, ssize_t written)
- {
- page = page_cache_alloc(mapping);
- error = add_to_page_cache_lru(page, mapping, index,mapping_gfp_constraint(mapping, GFP_KERNEL));
- }
然后执行add_to_page_cache_lru函数把page添加到radix/xrray tree,接着把page添加到lru缓存和lru链表
- int add_to_page_cache_lru(struct page *page, struct address_space *mapping,
- pgoff_t offset, gfp_t gfp_mask)
- {
- //把page添加到radix/xrray tree时令page引用计数加1
- ret = __add_to_page_cache_locked(page, mapping, offset,gfp_mask, &shadow);
- //page添加到lru缓存时令page引用计数加1,把page从lru缓存移动到lru链表时再令page引用计数减1
- lru_cache_add(page);
- }
在把page添加到radix/xrray tree时令page引用计数加1
- static int __add_to_page_cache_locked(struct page *page,
- struct address_space *mapping,
- pgoff_t offset, gfp_t gfp_mask,
- void **shadowp)
- {
- XA_STATE(xas, &mapping->i_pages, offset);
- .........
- //page引用计数加1
- get_page(page);
- page->mapping = mapping;
- page->index = offset;
- .........
- old = xas_load(&xas);
- xas_store(&xas, page);
- mapping->nrpages++;
- .........
- }
然后执行lru_cache_add函数把page添加到lru缓存时令page引用计数加1,把page从lru缓存移动到lru链表时再令page引用计数减1,函数流程是lru_cache_add->__lru_cache_add->__pagevec_lru_add->release_pages,关键函数如下:
把page添加到lru缓存时令page引用计数加1
- static void __lru_cache_add(struct page *page)
- {
- struct pagevec *pvec = &get_cpu_var(lru_add_pvec);
- //page引用计数加1
- get_page(page);
- if (!pagevec_add(pvec, page) || PageCompound(page))
- __pagevec_lru_add(pvec);
- put_cpu_var(lru_add_pvec);
- }
- //把page添加到lru缓存
- static inline unsigned pagevec_add(struct pagevec *pvec, struct page *page)
- {
- pvec->pages[pvec->nr++] = page;
- return pagevec_space(pvec);
- }
接着把page添加到lru链表时令page引用计数减1
- void __pagevec_lru_add(struct pagevec *pvec)
- {
- pagevec_lru_move_fn(pvec, __pagevec_lru_add_fn, NULL);
- }
- static void __pagevec_lru_add_fn(struct page *page, struct lruvec *lruvec,
- void *arg)
- {
- SetPageLRU(page);
- //把page添加到lru链表
- add_page_to_lru_list(page, lruvec, lru);
- }
- void release_pages(struct page **pages, int nr)
- {
- for (i = 0; i < nr; i++) {
- struct page *page = pages[i];
- //令page引用计数减1。如果之后page引用计数是0说明没有进程使用该page了,然后执行free_unref_page_list()把page释放回伙伴系统。
- if (!put_page_testzero(page))
- continue;
- .........
- list_add(&page->lru, &pages_to_free);
- }
- free_unref_page_list(&pages_to_free);
- }
如果是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,
- static inline void attach_page_buffers(struct page *page,
- struct buffer_head *head)
- {
- //令page引用计数加1
- get_page(page);
- //标记page的private属性
- SetPagePrivate(page);
- //建立page与bh的联系,本质是page->private=bh
- set_page_private(page, (unsigned long)head);
- }
OK,此时page的引用计数是2。接着来到page的内存回收,执行shrink_inactive_list()函数,这里把该函数的关键源码列下:
- static unsigned long shrink_inactive_list(unsigned long nr_to_scan, struct lruvec *lruvec,
- struct scan_control *sc, enum lru_list lru)
- {
- spin_lock_irq(&pgdat->lru_lock);
- //根据nr_to_scan数目从inactive lru链表隔离page符合条件的page到page_list链表,同时都令这些page的引用计数加1
- nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &page_list,&nr_scanned, sc, isolate_mode, lru);
- spin_unlock_irq(&pgdat->lru_lock);
- ..........
- nr_reclaimed = shrink_page_list(&page_list, pgdat, sc, 0,&stat, false);
- ..........
- spin_lock_irq(&pgdat->lru_lock);
- //没有成功内存回收的page再移动回 active/inactive lru链表,page引用计数减1。如果page引用计数是0说明没人用了,再移动回page_list
- putback_inactive_pages(lruvec, &page_list);
- spin_unlock_irq(&pgdat->lru_lock);
- ..........
- //释放page_list上引用计数是0的page
- free_unref_page_list(&page_list);
- }
看下隔离page执行的isolate_lru_pages函数,主要是令page引用计数加1
- static unsigned long isolate_lru_pages(unsigned long nr_to_scan,
- struct lruvec *lruvec, struct list_head *dst,
- unsigned long *nr_scanned, struct scan_control *sc,
- isolate_mode_t mode, enum lru_list lru)
- {
- //page符合内存回收条件则清理page的PageLRU属性,并令page引用计数加1,返回0,否则返回负数
- switch (__isolate_lru_page(page, mode)) {
- case 0:
- ........
- //把符合内存回收条件的page从lru链表移动到dst临时链表
- list_move(&page->lru, dst);
- break;
- case -EBUSY:
- list_move(&page->lru, src);
- continue;
- }
- }
- int __isolate_lru_page(struct page *page, isolate_mode_t mode)
- {
- int ret = -EINVAL;
- //关键点,如果page已经从lru链条剔除,page隔离失败
- if (!PageLRU(page))
- return ret;
- ret = -EBUSY;
- //page引用计数不是0则加1并返回true。否则说明page应用计数是0,返回false,这种page已经没进程在使用了,已经不在LRU链表了
- if (likely(get_page_unless_zero(page))){
- //page将要从active或inactive lru链表移除,于是清理page的PageLRU属性
- ClearPageLRU(page);
- ret = 0;
- }
- return ret;
- }
注意,隔离page时,在对spin_lock对lru_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进行真正的内存回收。
- static unsigned long shrink_page_list(struct list_head *page_list,
- struct pglist_data *pgdat,
- struct scan_control *sc,
- enum ttu_flags ttu_flags,
- struct reclaim_stat *stat,
- bool force_reclaim)
- {
- while (!list_empty(page_list)) {
- ............
- //page有映射的bh
- if (page_has_private(page)) {
- //page和bh解除联系,并且令page引用计数减1
- if (!try_to_release_page(page, sc->gfp_mask))
- goto activate_locked;
- }
- ...........
- //把page从radix tree、address_space 剔除,如果page引用计数是2则清0,返回1,page可以释放。否则page还再被其他进程使用,返回0,不能释放
- else if (!mapping || !__remove_mapping(mapping, page, true))
- goto keep_locked;
- free_it:
- nr_reclaimed++;
- list_add(&page->lru, &free_pages);
- continue;
- activate_locked:
- //重新设置page active
- SetPageActive(page);
- keep_locked:
- unlock_page(page);
- keep:
- //到这里,page本轮不能回收,暂存ret_pages链表然后再移回active或inactive lru链表
- list_add(&page->lru, &ret_pages);
- }
- //释放free_pages上的page到伙伴系统
- free_unref_page_list(&free_pages);
- ...............
- }
如果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,
- static void __clear_page_buffers(struct page *page)
- {
- //解除page和bh的联系
- ClearPagePrivate(page);
- set_page_private(page, 0);
- //令page引用计数减1
- put_page(page);
- }
此时page的引用计数是2,然后执行到__remove_mapping()函数。
- static int __remove_mapping(struct address_space *mapping, struct page *page,
- bool reclaimed)
- {
- refcount = 2;
- //page引用计数是2则对page引用计数清0,并返回true,这个page可以释放了。否则page引用计数不是2则保持引用计数并返回false,这个page不能释放
- if (!page_ref_freeze(page, refcount))
- goto cannot_free;
- .............
- //把page从radix tree 剔除
- __delete_from_page_cache(page, shadow);
- return 1;
- cannot_free:
- return 0;
- }
主要作用是:把page从radix tree、address_space 剔除,如果page引用计数是2则清0,返回1,page可以释放。否则page还再被其他进程使用,返回0,不能释放
好的,正常情况page引用计数此时就是0了,然后就可以释放掉这个page了。如果page因为是脏页、writeback页等导致page回收失败,page就要暂存在page_list链表。shrink_page_list()函数执行后,再执行putback_inactive_pages()函数把page移动回lru链表,源码如下:
- static void putback_inactive_pages(struct lruvec *lruvec, struct list_head *page_list)
- {
- struct zone_reclaim_stat *reclaim_stat = &lruvec->reclaim_stat;
- struct pglist_data *pgdat = lruvec_pgdat(lruvec);
- LIST_HEAD(pages_to_free);
- while (!list_empty(page_list)) {
- struct page *page = lru_to_page(page_list);
- int lru;
- lruvec = mem_cgroup_page_lruvec(page, pgdat);
- //page要添加到inactive lru,设置LRU属性
- SetPageLRU(page);
- lru = page_lru(page);
- //把page添加到lru链表,并增加lru链表page数
- add_page_to_lru_list(page, lruvec, lru);
- //page引用计数减1,减1后如果是0就说明page没人用了,可以释放了
- if (put_page_testzero(page)) {
- //清理page的lRU和active属性
- __ClearPageLRU(page);
- __ClearPageActive(page);
- //把page从lru链表剔除,并减少lru链表的page数
- del_page_from_lru_list(page, lruvec, lru);
- //把page再移动到pages_to_free链表,之后就直接释放掉
- list_add(&page->lru, &pages_to_free);
- }
- list_splice(&pages_to_free, page_list);
- }
这里对page引用计数减1,因为之前隔离收该page时令page引用计数加1了,二者对冲掉。
OK,本文到这里基本就结束了。page引用计数可能有点复杂,简单说,当page要启用一个新功能时,就要对page引用计数加1,而回收page时要一一对应对page引用计数减1。水平有限,如有错误请指出。