
在一次偶然的机会,在研究如何降低pagecache占用的过程中,走查了 invalidate_mapping_pages的代码:
通过调用 __pagevec_lookup 在radix树中收集一部分page,然后尝试调用 invalidate_inode_page 来释放这个page。
我主要看__pagevec_lookup 对引用计数的修改:
__pagevec_lookup -- >__find_get_pages -->page_cache_get_speculative
static inline int page_cache_get_speculative(struct page *page)
{/*³¢ÊÔÔö¼ÓÒýÓüÆÊý£¬Èç¹ûpage²»ÎªfreeµÄ»°*/
VM_BUG_ON(in_interrupt()); #ifdef CONFIG_TINY_RCU
# ifdef CONFIG_PREEMPT_COUNT
VM_BUG_ON(!in_atomic());
# endif
/*
* Preempt must be disabled here - we rely on rcu_read_lock doing
* this for us.
*
* Pagecache won't be truncated from interrupt context, so if we have
* found a page in the radix tree here, we have pinned its refcount by
* disabling preempt, and hence no need for the "speculative get" that
* SMP requires.
*/
VM_BUG_ON_PAGE(page_count(page) == , page);
page_ref_inc(page); #else
if (unlikely(!get_page_unless_zero(page))) {-------------走这个分支
/*
* Either the page has been freed, or will be freed.
* In either case, retry here and the caller should
* do the right thing (see comments above).
*/
return ;
}
#endif
VM_BUG_ON_PAGE(PageTail(page), page); return ;
}
static inline int get_page_unless_zero(struct page *page)
{
return page_ref_add_unless(page, 1, 0);
}
static inline int page_ref_add_unless(struct page *page, int nr, int u)
{
return atomic_add_unless(&page->_count, nr, u);
}
/**
* atomic_add_unless - add unless the number is already a given value
* @v: pointer of type atomic_t
* @a: the amount to add to v...
* @u: ...unless v is equal to u.
*
* Atomically adds @a to @v, so long as @v was not already @u.
* Returns non-zero if @v was not @u, and zero otherwise.
*/
static inline int atomic_add_unless(atomic_t *v, int a, int u)
{
return __atomic_add_unless(v, a, u) != u;
}
最后一个函数注释很明白,除非 page->_count 的计数为1,否则不增加引用计数,也就是说,当原值 page->_count 为1的时候,增加到2,然后返回。
然后看释放过程:
if (!trylock_page(page))
continue;
WARN_ON(page->index != index);
ret = invalidate_inode_page(page);
unlock_page(page);
主要的调用链为:
invalidate_inode_page--> invalidate_complete_page --> remove_mapping --> __remove_mapping
/*
* Same as remove_mapping, but if the page is removed from the mapping, it
* gets returned with a refcount of 0.
*/
static int __remove_mapping(struct address_space *mapping, struct page *page,
bool reclaimed)
{
BUG_ON(!PageLocked(page));
BUG_ON(mapping != page_mapping(page)); spin_lock_irq(&mapping->tree_lock);
/*
* The non racy check for a busy page.
*
* Must be careful with the order of the tests. When someone has
* a ref to the page, it may be possible that they dirty it then
* drop the reference. So if PageDirty is tested before page_count
* here, then the following race may occur:
*
* get_user_pages(&page);
* [user mapping goes away]
* write_to(page);
* !PageDirty(page) [good]
* SetPageDirty(page);
* put_page(page);
* !page_count(page) [good, discard it]
*
* [oops, our write_to data is lost]
*
* Reversing the order of the tests ensures such a situation cannot
* escape unnoticed. The smp_rmb is needed to ensure the page->flags
* load is not satisfied before that of page->_count.
*
* Note that if SetPageDirty is always performed via set_page_dirty,
* and thus under tree_lock, then this ordering is not required.
*/
if (!page_ref_freeze(page, ))
goto cannot_free;
/* note: atomic_cmpxchg in page_freeze_refs provides the smp_rmb */
if (unlikely(PageDirty(page))) {
page_ref_unfreeze(page, );
goto cannot_free;
} if (PageSwapCache(page)) {
swp_entry_t swap = { .val = page_private(page) };
__delete_from_swap_cache(page);
spin_unlock_irq(&mapping->tree_lock);
swapcache_free(swap, page);
} else {
void (*freepage)(struct page *);
void *shadow = NULL; freepage = mapping->a_ops->freepage;
/*
* Remember a shadow entry for reclaimed file cache in
* order to detect refaults, thus thrashing, later on.
*
* But don't store shadows in an address space that is
* already exiting. This is not just an optizimation,
* inode reclaim needs to empty out the radix tree or
* the nodes are lost. Don't plant shadows behind its
* back.
*
* We also don't store shadows for DAX mappings because the
* only page cache pages found in these are zero pages
* covering holes, and because we don't want to mix DAX
* exceptional entries and shadow exceptional entries in the
* same page_tree.
*/
if (reclaimed && page_is_file_cache(page) &&
!mapping_exiting(mapping) && !dax_mapping(mapping))
shadow = workingset_eviction(mapping, page);
__delete_from_page_cache(page, shadow);
spin_unlock_irq(&mapping->tree_lock);
mem_cgroup_uncharge_cache_page(page); if (freepage != NULL)
freepage(page);
} return ; cannot_free:
spin_unlock_irq(&mapping->tree_lock);
return ;
}
看第一行的注释,我们可以知道,remove_mapping 是__remove_mapping的包裹函数, __remove_mapping函数如果将页面从page cache中移除成功,则会将page的引用计数 返回0.
我们关注最关键的一段:
if (!page_ref_freeze(page, ))
goto cannot_free; static inline int page_ref_freeze(struct page *page, int count)
{
return likely(atomic_cmpxchg(&page->_count, count, ) == count);
}
atomic_cmpxchg函数实现了一个比较+交换的原子操作(原子就是说cpu要不就不
做,要做就一定要做完,不会存在中间状态,对应这里就是比较和交换要一次过做完).
关于atomic 和比较交换的一些函数,网上资料较多,在此不赘述,总体的意思就是,当page的count为2,则交换为0,并且返回旧值2,
也就是 page->_count 为2的话,则可以释放,否则 会走 cannot_free 的分支。因为之前 __pagevec_lookup 执行后,page->_count肯定为2,所以能最终free。
回到 包裹函数 remove_mapping
/*
* Attempt to detach a locked page from its ->mapping. If it is dirty or if
* someone else has a ref on the page, abort and return 0. If it was
* successfully detached, return 1. Assumes the caller has a single ref on
* this page.
*/
int remove_mapping(struct address_space *mapping, struct page *page)
{
if (__remove_mapping(mapping, page, false)) {
/*
* Unfreezing the refcount with 1 rather than 2 effectively
* drops the pagecache ref for us without requiring another
* atomic operation.
*/
page_ref_unfreeze(page, );
return ;
}
return ;
}
因为在lookup阶段,将原有的因为计数page->_count为1的,增加到2,然后通过一个交换判断,如果是2的,则交换为0,然后最终调用 page_ref_unfreeze 将引用计数设置为1,
释放成功。有兴趣的同学可以继续一下,因为在pagecache中的页,默认是加入到lru的,为了防止频繁地加入到lru,又设计了一个pagevec数组,当数组满了或者主动调用drain函数来将
数组中缓存的page 刷入到lru链表中,为了区分这两种状态,加入到lru的pagevec数组的page,计数要加1,当真正加入到lru中的时候,计数又减1,恢复到之前的计数值。所以lru并不占用计数。
我们知道,页面从freelist中分配出来的时候,引用计数是需要加1的。
get_page_from_freelist->buffered_rmqueue->prep_new_page->set_page_refcounted,由此函数完成+1.
写本文的原因是,我原来以为的加入到lru,加入到radix树,都需要增加引用计数的,在加入radix树,确实是加1了,但一般出来之后就会-1,所以在radix树的时候,计数增加只是临时行为,lru也是如此,因为加入到lru,我只看到了加入到lru的pagevec数组,这个时候确实是+1了,但是
当真正加入到lru链表的时候,又减了1,也就是page真正加入到lru链表,会保持计数不变,当然PG_Lru肯定是要置位的。
同理,我在看函数 add_to_page_cache_lru 的时候,确实对加入的page 增加了引用计数,所以一直认为pagecache中的页的引用计数至少是2, 调用 __pagevec_lookup 后应该为3,
当我看到 如下代码之后,就死活不理解。
if (!page_ref_freeze(page, 2))
goto cannot_free;
然后回过头再去看单凡是调用add_to_page_cache_lru 之后,都会调用put_page,不管成功失败,如果成功,相当于加入pagecache成功,对应的put_page就是减去对加入的时候的page的引用计数,那么此时计数为1,如果加入失败,那么对应的put_page就是释放内存,因为此时计数为0.
回到文章的开头,如果一个页面,没人访问,在pagecache中,当然也在lru链表中的时候,引用计数为1,而这个1,还是从freelist中摘除的时候来增加的。也有可能为2,此时说明没有在lru中,只在pagevec中,
如果一个页面,没人访问,在pagecache中,但是处于lru的lruvec中,此时的引用计数应该为2,所以才会有在调用fadvise64_64 的时候,
case POSIX_FADV_DONTNEED:
if (!bdi_write_congested(mapping->backing_dev_info))
__filemap_fdatawrite_range(mapping, offset, endbyte,
WB_SYNC_NONE); /* First and last FULL page! */
start_index = (offset+(PAGE_CACHE_SIZE-)) >> PAGE_CACHE_SHIFT;
end_index = (endbyte >> PAGE_CACHE_SHIFT); if (end_index >= start_index) {
unsigned long count = invalidate_mapping_pages(mapping,
start_index, end_index); /*
* If fewer pages were invalidated than expected then
* it is possible that some of the pages were on
* a per-cpu pagevec for a remote CPU. Drain all
* pagevecs and try again.
*/
if (count < (end_index - start_index + )) {
lru_add_drain_all();
invalidate_mapping_pages(mapping, start_index,
end_index);
}
}
break;
当发现释放的页面小于请求的页面数,会调用 lru_add_drain_all ,如果不调用这个,则有可能因为处于lru的pagevec的页无法释放,其实有大概率是能够释放的。
当时的stap记录如下:
调用 page_cache_get_speculative 之前的计数,为1,此时就是出于pagecache中的页的原本计数,(排除其他正在使用的页)
enter 1147=page=0xffffea000e955080,flags=0x6fffff00020068,mapcount=-1,_count=1===
0xffffffff81183381 : __find_get_pages+0x81/0x170 [kernel]
0xffffffff8118ff2e : __pagevec_lookup+0x1e/0x30 [kernel]
0xffffffff81191243 : invalidate_mapping_pages+0x93/0x1f0 [kernel]
0xffffffff811850b4 : SyS_fadvise64_64+0x1a4/0x290 [kernel]
0xffffffff811851ae : SyS_fadvise64+0xe/0x10 [kernel]
0xffffffff81698b09 : system_call_fastpath+0x16/0x1b [kernel]
调用 page_cache_get_speculative之后的计数,为2,
enter 1163=page=0xffffea000e955080,flags=0x6fffff00020068,mapcount=-1,_count=2===
0xffffffff811833b5 : __find_get_pages+0xb5/0x170 [kernel]
0xffffffff8118ff2e : __pagevec_lookup+0x1e/0x30 [kernel]
0xffffffff81191243 : invalidate_mapping_pages+0x93/0x1f0 [kernel]
0xffffffff811850b4 : SyS_fadvise64_64+0x1a4/0x290 [kernel]
0xffffffff811851ae : SyS_fadvise64+0xe/0x10 [kernel]
0xffffffff81698b09 : system_call_fastpath+0x16/0x1b [kernel]
有一个同事问到,在__generic_file_splice_read 函数中,有一个while循环
while (spd.nr_pages < nr_pages) {
/*
* Page could be there, find_get_pages_contig() breaks on
* the first hole.
*/
page = find_get_page(mapping, index);//找具体的page,之前连续的时候没找到的
if (!page) {//经过预读仍然没找到
/*
* page didn't exist, allocate one.
*/
page = page_cache_alloc_cold(mapping);//分配页面
if (!page)
break;
//加入到radix树,主要有修改page的mapping等
error = add_to_page_cache_lru(page, mapping, index,
GFP_KERNEL);
if (unlikely(error)) {
page_cache_release(page);
if (error == -EEXIST)
continue;
break;
}
/*
* add_to_page_cache() locks the page, unlock it
* to avoid convoluting the logic below even more.
*/
unlock_page(page);
} spd.pages[spd.nr_pages++] = page;//将找到的或者分配的页面加入到spd
index++;
}
经过page_cache_alloc_cold 再加入到 add_to_page_cache_lru 的页面,并没有-1啊,岂不是跟之前的描述矛盾,这个地方没有减1,其实是因为这个计数本就应该为2,因为这个page加入到了spd中,计数必须增加,既然-1又需要加1,干脆就不动,前面通过 find_get_pages_contig 加入到spd中的页,此时计数应该也是2,(排查并发操作的情况,否则就是>2).
经过 find_get_pages_contig 加入到spd,或者通过 spd.pages[spd.nr_pages++] = page;//将找到的或者分配的页面加入到spd
保证了此时在spd中的page的计数都至少为2.这个在spd进行release的时候,统一进行-1,计数又恢复了。
所以说,pagecache中的且位于lru链表的page,在没有读写,也没有kswap正在对该page进行老化的情况下,引用计数就是1。