龙空技术网

风险指针(Hazard Pointer)

k24u 83

前言:

现时各位老铁们对“判断是否为空指针的方法”可能比较关怀,我们都想要剖析一些“判断是否为空指针的方法”的相关文章。那么小编在网摘上收集了一些有关“判断是否为空指针的方法””的相关知识,希望朋友们能喜欢,兄弟们快快来学习一下吧!

Hazard Pointer,有时候翻译为冒险指针,有时候翻译为风险指针,不过不影响本文的探讨,本文以风险指针来描述。

为方便探讨和理解该问题,我们以单链表操作为例子。单链表一般有插入,删除,查询操作:

插入操作:

new->next = current->next;

current->next = new;

即先把自己的next指向current的之前next,然后把current的next指向自己,这里面涉及两个操作,另外因为插入操作,是先改变自己的next,然后再自己入链表,这样操作的结果是,其它线程遍历的时候,在第二步未完成的情况下,它看不到new成员。

删除操作:

current->next = new->next;

new->next = null;

删除操作,则直接一步完成操作,它对查询的影响是,查询可能正在使用new成员,这里面有并发问题,如果直接释放,会导致指针异常。

对于插入和删除操作,因为涉及到并发问题,我们往往会使用锁,来防止并发,但是对于查询操作,查询因为不会涉及到修改链表,所以我们会想,有没有办法不加锁的情况下进行查询遍历?

假设不加锁的情况下,进行遍历,我们使用如下的思路进行:

1,插入操作,因为会导致有些线程看到new成员,有些看不到,那就是说会出现不同线程看到的数据不一致问题,这是典型的延迟处理问题,RCU算法也会面临这个问题,但如果程序逻辑不介意这个,那就没问题,比如路由表,比如文件系统。

2,对于删除操作,我们不再直接释放,而是先标记待删除,然后看看有没有人在使用,如果没有人使用,则释放,如果有人使用,则在使用完成后,再释放。

针对2,最先想到的办法是使用引用计数,在查询的时候,先增加引用计数,然后再使用,释放的时候,通过判断引用计数是否为0,决定是否释放。对于引用计数方式,可能面临下面的问题:

1,引用计数放在哪里?如果是放在new成员里,那会涉及一个矛盾问题,你想获取引用计数,那必须先获取new,这是矛盾的,因为new是否可用,取决于引用计数,而引用计数的多少,又需要先获取new。如果引用计数不跟new放在一起,那需要单独维护,这是可行的。

2,引用计数需要全局同步,这个开销比较大

引用计数之所以有这种问题,在于它不是直接表明new的情况,相当于是程序员强行把引用计数和new成员逻辑上绑定在一起,这两个成员又是单独维护的,那必然面临逻辑一致性问题,因为这是两个动作。

那是否有办法,直接体现new成员的情况呢?结合上面的分析,我们可以重点关注一下删除操作和查询操作之间的并发,对于删除操作,如果脱链之后,我们把new->next赋值为一个特殊值,表明该节点已经脱链,待释放,而对于查询操作,如果在脱链之前,查询已经在使用new,这样的话,为避免跟引用计数一样的问题,我们通过引用current->next,来获取new的情况,而为了获取第一个成员的next,我们需要一个一直存在的头指针,这个就是风险指针的思路。

下面,我们以Pre-BSD路由表的源代码作为风险指针的应用案例进行源代码分析:

(注:代码来源于perfbook-9.3章节)

如上代码,route_entry为路由表的存储结构,re_next作为链表的next成员使用,可以看出:

1,在添加和删除操作时,使用了自旋锁进行保护,对于添加操作,它始终作用于表头,可考虑使用无锁方式,而对于删除操作,它先查找,然后再删除的操作,它不同于队列,队列始终作用于表头

2,对于删除操作,在找到之后,先进行脱链(*repp = rep->next),然后标记待删除(rep->re_next = (struct route_entry *)HAZPTR_POISON;),此时已经解锁,然后发起风险指针延迟删除处理,在hazptr_free_later中,先进行登记,然后在合适的时机,发起扫描处理,扫描的逻辑是,把待删除成员和风险指针列表里记录的指针进行比对,如果风险指针列表里存在,说明还有引用,否则可以直接删除,注意这里有一个前提,就是必须先脱链,再扫描,而且扫描前需要使用smp_mb进行数据同步,以此看到全局的风险指针列表。这样处理的好处是,同步完成后,后续再查询引用的时候,该成员已经不可见,同时确保在该时机,可以看到全局的引用情况。

3,对于查询操作,它并没有加锁,而是先从表头开始遍历,注意表头一直是存在的,因此它的re_next一直存在,在hp_try_record时,它使用了&route_list.re_next,即双重指针,之所以这样,是因为它现在要进行风险指针登记的,其实是re_next成员,并不是自己,而re_next成员是个指针,为传递该指针成员,需要使用双重指针。

可以看出,每次遍历前,先登记re_next成员为风险指针,如果登记成功,则返回re_next成员,否则如果跟删除产生并发操作,即看到HAZPTR_POISON,则认为该成员已经脱链,那重新再遍历一次,而重新遍历时,该成员已经不可见。对于已经登记后,再删除的情况,则需要通过扫描的方式,来比对处理。

从这里可以看到,风险指针其实是通过外部的双重指针来引用自己的,并不是通过引用计数,也就是要引用自己之前,它通过前一个对象的re_next成员来访问自己,先尝试登记自己为风险指针,如果登记成功,则可以正常访问,然后继续迭代到自己,去登记自己的re_next,这样处理的好处是,访问下一个成员之前,自己处于已登记状态,是安全的,如果下一个成员也登记成功,那风险指针的登记就传递给下一个成员了,此时可以安全访问下一个成员了。

上述是风险指针登记的处理,可以看到hp_try_record的处理,它会先判断是不是空指针或者已经是脱链状态,如果是,则返回失败,否则直接登记,然后smp_mb同步处理,最后同步完成后,再check一次是不是真的登记成功了,如果是,那可以安全返回了,否则返回外层,继续尝试进行登记,此时*p已经发生了变化,因为如果读取到HAZPTR_POISON时,该成员实际已经脱链,那意味着通过上一个成员的re_next双重指针访问时,它应该已经指向下一个成员了(参考删除代码),这样登记的结果会是自己的下一个成员,而不是自己。

上述是延迟扫描删除的代码,读者可自行分析。需要说明的是,rlist是per-thread变量,记录本线程想删除的成员,gplist是全局变量,记录所有当前引用的风险指针。

结尾

对于风险指针来说,它优化了读的性能,对于频繁读,偶尔增删的情况,读性能上是有提升的,但是它还是避免不了多线程之间数据的频繁同步,也就是会频繁使用smp_mb,因为它还是基于数据本身来做无锁化处理的,这不可避免,这对性能有比较大的影响,我们后续聊一聊RCU锁,它利用了系统事件来识别可删除时机,尽量摒弃了基于数据本身的逻辑,RCU锁因为强依赖系统事件,所以要求对linux系统的整体运行有充分的熟悉,如dyntick,RMI,scheduler,中断,软中断,cpu hotplug,pm事件等等,RCU锁算是内核比较复杂的一个模块了。

标签: #判断是否为空指针的方法