基数树与RCU锁

基数树是一种用空间换时间的数据结构,通过空间的冗余减少时间上的消耗.radix tree很适合稀疏的结构!

自从把RCU机制引入到基树中来,这里就有了个协议叫做:lockless的page-cache协议!

一个小小的find_get_entry函数,里面处理了两种异常的情况,一是文件系统的page被free或者将要被free掉的情况,二是文件系统的page被remove掉的情况!第三种异常是slot中直接被标记无效的情况,还有就是slot中没有对应的page*的情况!但是这个槽为什么能搜到?是否和基树的结构有关呢?

tmpfs和procfs在内存中是个什么样子呢? 是不是也是这样一个superblock挂上一个pagecache树这样的情形?

RCU锁应该是内核中最不好想的锁了吧?

介于文件系统中大量用到这个锁:包括pagecache变成lockless的了,还有dentry的管理也是采用RCU的方式.

RCU的原理很简单:该机制记录了指向共享数据结构的指针的所有使用者.在该结构将要改变时,首先创建一个副本!在副本中修改!在所有进行读访问的使用者结束对旧副本的读取之后,指针可以替换为指向新的,修改之后的副本的指针.这种机制允许读写并发执行!

反引用指针并使用其结果的代码,需要用rcu_read_lock和rcu_read_unlock调用保护起来:

rcu_read_lock();

p = rcu_dereference(ptr);

if (p != NULL)

    do_something(p);

rcu_read_unlock();

如果必须要修改ptr指向的对象,则需要使用rcu_assign_pointer:

struct super_duper *new_ptr = kmalloc(...)

new_ptr->meaning = xyz;

new_ptr->of = 42;

new_ptr->life = 23;

rcu_assign_pointer(ptr, new_ptr);

根据RCU的术语,该操作公布了这个指针,后续的读操作将看到新的结构,而不是原来的.

如果更新操作来自于内核中的许多地方,那么必须使用普通的同步原语,防止并发的写操作,如自旋锁.尽管RCU能够保证读访问不受写访问的干扰,但它不对写访问之间的相互干扰提供保护.

新的值已经公布以后,就得结构实例会被释放!在所有的读访问完成以后,讷河可以释放该内存,但它需要知道何时释放内存是安全的.为此,RCU提供了另外两个函数:

synchronize_rcu()等待所有现存的读访问完成.在该函数返回之后,释放与原指针关联的内存是安全的!

2. call_rcu可用于注册一个函数,在所有针对共享资源的读访问完成之后调用.这要求将一个rcu_head实例嵌入(不能通过指针)到RCU保护的数据结构:

struct super_duper {
     struct rcu_head head;

     int meaning, of, life;

}

该回调函数可以通过参数访问对象的rcu_head成员,进而使用container_of机制访问对象本身.

其中super_duper是整个要释放的结构体

RCU机制中引入了垃圾回收器,真是个高大上的概念!

RCU不维护锁!

CU中有一篇文章把这个问题说得还不错, 但是仍然不知道call_rcu函数是怎么用的, 以及

http://blog.chinaunix.net/uid-23769728-id-3080134.html

http://blog.chinaunix.net/uid-20648784-id-1592810.html

--------------------

2016年11月5日

RCU writer在释放元素一般会有call_rcu, RCU writer在释放元素时的三种调用方式:调用synchronize_rcu,同步方式, 

list_del_rcu(p);

synchronize_rcu();

kfree(p);

另一种是异步方式,在iRQ中调用call_rcu,异步方式

list_del_rcu(p);

call_rcu(&p->rcu, p_call_back);

static void p_callback(stuct rcu_head *);

那么就从page_cache中调用call_rcu来看看一个radix_tree_node是如何被平稳释放的!

并且同步的释放节点和异步的释放节点会不会有什么不同呢?

异步释放与同步释放是不是为了解决不同的问题?

同步释放

为什么page-cache会用rcu机制? 其实并不是为了一个page, 或者说是slot的互斥, 而是radix-node粒度的互斥. 因为page粒度的互斥, lock_page已经做了!  所以不用担心此处, 所以page-cache算是巧用了rcu机制.

为什么, 因为rcu机制本质上是很简单地对一个共享区的互斥: 很简单的一个场景, 全局变量, 所有的rcu机制都围着这一个变量转! 但是page-cache里的rcu不同了[说到这里, 好像也没啥不同的......晕,只是10分钟前一直没有找到rcu要保护的对象而已......]

也就是说整个page-cache树中的结构有两个同步机制, 一是lock_page, 二是rcu锁. 

那么address_space也不难理解了,  tree_lock就是用来保护写了.

真是由于pagecache的RCU化,导致了每次lock_page成功之后, 都要判断page->mapping != mapping

=========================================

缓存是计算机中非常重要的概念, 其思想是数据访问的时间局部性,即当前被访问的数据后续很可能还会被访问到. 文件的访问是现代文件系统中pagecache是内核中非常重要的缓存, 

对pagecache的操作主要有:查找,插入,删除.

插入和删除操作都是写操作, 所以需要address_space->tree_lock来保证互斥,是整棵树层面的互斥,这里是典型的RCU的写使用场景.

查找操作不需要加锁

对查找的影响是什么?

1) 暂时还找不到一个page, 查找操作结束后page进来了, 某些gang操作, insert进来的page可能不被临幸;

2) 找到了一个page, 但是这个page后来被释放掉了(最典型的场景是删除了一个page);

这些都是由于RCU引起来的吗?

首先谈比较熟悉的2)即删除的场景: 函数pagecache_get_page中,首先通过 find_get_entry 得到一个page, 然后lock_page, 就在这个find_get_entry和lock_page的空挡处, 这个page可能就被truncate掉了! 被truncate掉的page有什么什么特征呢? 被truncate掉的page->mapping is null

所以在lock_page的时候,一定要注意这个page的的mapping是否还是原来的mapping, 只要是, 那么我们就可以断定这个page肯定是不可以被truncate掉的!

2)中的场景肯定是由RCU引起来的,因为这样可以保证最大限度的读并发效率; 那么1)的场景会导致这个问题吗?应该是不会的!要么能得到这个page,要么就得不到!

那么还有个问题,释放的page什么时候回收? delete部分是pagecache操作中比较有意思的一部分,首先是从page-cache中去掉这个page,然后是调用put_page, 如果此时普通_page后page_count仍然不是zero, 那么说明page还在用, 直至最后一个page的user释放了这个page, 那么page归还到buddy中去.

其实整个系统中涉及到插入删除的还真不是page, 而是radix_tree_node结构体, page-cache是基于radix-tree的, 所以radix-tree的插入删除算法中涉及到中间索引节点的变更,radix_tree_node的变更!

中间节点的变更其实只是page插入删除的一个副反应! 为什么这么说是因为page-cache的操作对象都是page,page通过中间的radix_tree_node索引

在删除radix_tree_node时, 必须要有page的locked状态! lockpage与RCU完美搭配,实现了tree_node顺利卸载!

那么问题又来了,如何rcu是如何保证读和写同时读到一个指针时读不会读到中间状态的?(和RCU机制相关,assigned)

入下面代码, 指针的赋值采用 rcu_assign_pointer 去做: #define rcu_assign_pointer(p, v) smp_store_release(&p, RCU_INITIALIZER(v))

 56 #define smp_store_release(p, v)                     
 57 do {                                    
 58     compiletime_assert_atomic_type(*p);             
 59     smp_mb();                           
 60     ACCESS_ONCE(*p) = (v);                      
 61 } while (0)
 62 

这里是一个原子的操作[后续可以研究下arm64中原子操作是怎么完成的], 后面的rcu reader就都读不到这个page了, 但和教科书中案例相比,本例有个非常非常重要的不同, 那就是指针指向的page是不需要调用free掉的,因为这棵树中的指针直接就是struct page结构体的地址, 有点探底了,不涉及到内存的释放,这里就是最彻底的释放了!

那么radix_tree_node的释放是怎么回事呢? 怎么就丝毫没有使用assign_pointer这样的赋值呢? 

是因为rcu的本质是去找page,所以slot中指向page的指针才是要保护的关键, 所以radix_tree_node就是可有可无的赋值了?

[page chache的命中率, extent_tree的命中率,对数字敏感!]

--------2016.11.07

pagecache的

http://lwn.net/Articles/291826/

原文地址:https://www.cnblogs.com/honpey/p/6002084.html