现在回到读端临界区的问题上来 。多个读端临界区不互斥,即多个读者可同时处于读端临界区中,但一块内存数据一旦能够在读端临界区内被获取到指针引用,这块内存块数据的释放必须等到读端临界区结束,等待读端临界区结束的 Linux kernel API 是synchronize_rcu() 。读端临界区的检查是全局的,系统中有任何的代码处于读端临界区,synchronize_rcu() 都会阻塞,知道所有读端临界区结束才会返回 。为了直观理解这个问题,举以下的代码实例:
/* `p` 指向一块受 RCU 保护的共享数据 *//* reader */rcu_read_lock();p1 = rcu_dereference(p);if (p1 != NULL) {printk("%dn", p1->field);}rcu_read_unlock();/* free the memory */p2 = p;if (p2 != NULL) {p = NULL;synchronize_rcu();kfree(p2);}
用以下图示来表示多个读者与内存释放线程的时序关系:
文章插图
上图中,每个读者的方块表示获得 p 的引用(第5行代码)到读端临界区结束的时间周期;t1 表示 p = NULL 的时间;t2 表示 synchronize_rcu() 调用开始的时间;t3 表示 synchronize_rcu() 返回的时间 。我们先看 Reader1,2,3,虽然这 3 个读者的结束时间不一样,但都在 t1 前获得了 p 地址的引用 。t2 时调用 synchronize_rcu(),这时 Reader1 的读端临界区已结束,但 Reader2,3 还处于读端临界区,因此必须等到 Reader2,3 的读端临界区都结束,也就是 t3,t3 之后,就可以执行 kfree(p2) 释放内存 。synchronize_rcu() 阻塞的这一段时间,有个名字,叫做 Grace period 。而 Reader4,5,6,无论与 Grace period 的时间关系如何,由于获取引用的时间在 t1 之后,都无法获得 p 指针的引用,因此不会进入 p1 != NULL 的分支 。
删除链表项知道了前边说的 Grace period,理解链表项的删除就很容易了 。常见的代码模式是:
p = seach_the_entry_to_delete();list_del_rcu(p->list);synchronize_rcu();kfree(p);其中 list_del_rcu() 的源码如下,把某一项移出链表:/* list.h */static inline void __list_del(struct list_head * prev, struct list_head * next){next->prev = prev;prev->next = next;}/* rculist.h */static inline void list_del_rcu(struct list_head *entry){__list_del(entry->prev, entry->next);entry->prev = LIST_POISON2;}
根据上一节“访问链表项”的实例,假如一个读者能够从链表中获得我们正打算删除的链表项,则肯定在 synchronize_rcu() 之前进入了读端临界区,synchronize_rcu() 就会保证读端临界区结束时才会真正释放链表项的内存,而不会释放读者正在访问的链表项 。更新链表项前文提到,RCU 的更新机制是 “Copy Update”,RCU 链表项的更新也是这种机制,典型代码模式是:
p = search_the_entry_to_update();q = kmalloc(sizeof(*p), GFP_KERNEL);*q = *p;q->field = new_value;list_replace_rcu(&p->list, &q->list);synchronize_rcu();kfree(p);
其中第 3,4 行就是复制一份副本,并在副本上完成更新,然后调用 list_replace_rcu() 用新节点替换掉旧节点 。源码如下:其中第 3,4 行就是复制一份副本,并在副本上完成更新,然后调用 list_replace_rcu() 用新节点替换掉旧节点,最后释放旧节点内存 。list_replace_rcu() 源码如下:
static inline void list_replace_rcu(struct list_head *old,struct list_head *new){new->next = old->next;new->prev = old->prev;rcu_assign_pointer(list_next_rcu(new->prev), new);new->next->prev = new;old->prev = LIST_POISON2;}
推荐阅读
- 深入设计原则-SOLID
- Linux 日志分析实战
- Linux驱动基础篇:LED驱动
- 你知道Linux中用户们的密码藏在哪儿吗?
- Linux文件系统EXT2,EXT3,ReiserFS详解
- 深入理解 HttpSecurity
- 安装 Linux,只需三步
- linux中tar命令的用法
- linux中tar命令实测
- Bash 脚本实现每次登录到 Shell 时可以查看 Linux 系统信息