lock & lock-free
Lock | Description |
---|---|
Mutex | 只允许一个线程同时获取,也就是同时只允许一个线程进入临界区,其他线程会挂起,等待holder释放Mutex |
Recursive lock | Recursive lock是Mutex的变种,允许一个线程在释放之前多次获取,其他线程阻塞直到此线程释放同样的次数,可用于递归迭代,也可以用于多个函数分别获取锁的情况 |
Read-write lock | shared-exclusive lock,适用于读多写少的情况。没有写者时,读者可以同时执行读操作;写者需要阻塞等待所有的读者释放锁;读者需要阻塞等待所有写者释放锁 |
Distributed lock | 分布式锁进程级别的互斥访问。不同于mutex,分布式锁不阻塞进程。在无法获取锁时由进程决定是否继续。 |
Spin lock | spin lock在无法获取锁时会重试直到获取锁,适用于等待锁的时间较小的多处理器系统,在这种系统中,重试比阻塞线程更有效,因为阻塞线程会导致上下文切换和线程数据结构的更新 |
Double-checked lock | Double-checked lock是为了解决获取锁前需要检测获取条件的损耗,但是这种锁不是很安全,尽量不要使用 |
RCU | 读几乎无代价,写会延迟,在写很少且不要求数据一致的情况下高效 |
题外话:futex减少非竞争状态下的系统调用
RCU
每种锁都有特点,决定了适用的场景,RCU适用于写操作很少(注意,不是写者少)、不要求数据一致的情况下(如果写者很多会造成不一致的情况,一般情况下写者多于一个,要求一致性的场景下RCU不适用)
RCU特点
RCU机制是在02年加入内核的,特点就是允许同时读写
类型 | 特点 |
---|---|
互斥锁 | 读者写者一视同仁,互斥访问 |
读写锁 | 没有写的情况下,可以同时读 |
RCU | 一写多读同时 |
写者在更新之前会检测在此之前的读操作都完成了,否则写者就等待,此时写者持有最新版本,正在读的读者会得到旧的版本。写者更新完后还需要释放旧版本数据占用的空间。
RCU三机制
- 发布-订阅机制(用于插入)
- 等待已经存在的读者完成读取操作(用于删除)
- 维护最新数据的多个版本(对于读者)
发布-订阅
1 struct foo {
2 int a;
3 int b;
4 int c;
5 };
6 struct foo *gp = NULL;
7
8 /* . . . */
9
10 p = kmalloc(sizeof(*p), GFP_KERNEL);
11 p->a = 1;
12 p->b = 2;
13 p->c = 3;
14 gp = p;
10-14可能被编译器优化或CPU乱序执行,比如11、12完成,然后执行了14,那么新的读者就会读取到未完全初始化的数据(p->c没有初始化)
需要用内存屏障以保证CPU执行完11、12、13以后再执行14.
1 p->a = 1;
2 p->b = 2;
3 p->c = 3;
4 rcu_assign_pointer(gp, p);
用rcu_assign_pointer发布新数据,强制编译器和CPU在初始化p的成员以后执行发布操作。
读者也需要,
1 p = gp;
2 if (p != NULL) {
3 do_something_with(p->a, p->b, p->c);
4 }
这段代码初看可能觉得不会导致乱序问题,眼见不一定为实,the DEC Alpha CPU [PDF] and value-speculation compiler optimizations可能会在取p之前,取到p->a,p->b,p->c。
1 rcu_read_lock();
2 p = rcu_dereference(gp);
3 if (p != NULL) {
4 do_something_with(p->a, p->b, p->c);
5 }
6 rcu_read_unlock();
rcu_dereference保证在执行4时可以读取到初始化过的值。
rcu_read_lock和rcu_read_unlock定义的read-size section,目的是。不会循坏或阻塞。
理论上,这两个操作可以用于RCU保护的数据结构。内核里用在了struct list_head和struct hlist_head/struct hlist_node上
1 struct foo {
2 struct list_head list;
3 int a;
4 int b;
5 int c;
6 };
7 LIST_HEAD(head);
8
9 /* . . . */
10
11 p = kmalloc(sizeof(*p), GFP_KERNEL);
12 p->a = 1;
13 p->b = 2;
14 p->c = 3;
15 list_add_rcu(&p->list, &head);
15行需要同步机制的保护,防止同时添加,可以加锁,这个锁只会使写者互斥,不会导致RCU读者阻塞。
1 rcu_read_lock();
2 list_for_each_entry_rcu(p, head, list) {
3 do_something_with(p->a, p->b, p->c);
4 }
5 rcu_read_unlock();
为何list_add_rcu的同时list_for_each_entry_rcu时不会导致segfault
单向循环链表
struct foo {
struct hlist_node *list;
int a;
int b;
int c;
};
HLIST_HEAD(head);
/* . . . */
p = kmalloc(sizeof(*p), GFP_KERNEL);
p->a = 1;
p->b = 2;
p->c = 3;
hlist_add_head_rcu(&p->list, &head);
rcu_read_lock();
hlist_for_each_entry_rcu(p, q, head, list) {
do_something_with(p->a, p->b, p->c);
}
rcu_read_unlock();
为何需要传递两个指针
Category Publish Retract Subscribe Pointers rcu_assign_pointer() rcu_assign_pointer(…, NULL) rcu_dereference() Lists list_add_rcu() list_add_tail_rcu() list_replace_rcu() list_del_rcu() list_for_each_entry_rcu() Hlists hlist_add_after_rcu() hlist_add_before_rcu() hlist_add_head_rcu() hlist_replace_rcu() hlist_del_rcu() hlist_for_each_entry_rcu()
何时释放已经被删除或替换的数据元素,如何知道所有对此元素的读者都退出了
等待已有读者完成
等待事情完成有很多机制,引用计数、读写锁、事件,RCU的优势在于可以等待20000个不同的事情,无需track每个事情,无需担心性能损失、扩展性限制、死锁、内存泄露,这些在track的策略中会出现。
RCU read-side critical section 保护任意非阻塞或sleep的代码段
(看图)
等待临界区(包括内存操作)完成。如图在Grace Period之后进入的reader可能会跨越Grace Period。
等待reader:
- 修改,例如替换链表中的元素
- 等待已经进入的RCU read-side临界区完成,例如,用synchronize_rcu()原子操作)。后进入的RCU read-size临界区无法获取到已经被移除的元素指针(在Grace Period之前的可以获得)
- 清空旧的元素
可以对照下面的代码
1 struct foo {
2 struct list_head list;
3 int a;
4 int b;
5 int c;
6 };
7 LIST_HEAD(head);
8
9 /* . . . */
10
11 p = search(head, key);
12 if (p == NULL) {
13 /* Take appropriate action, unlock, and return. */
14 }
15 q = kmalloc(sizeof(*p), GFP_KERNEL);
16 *q = *p;
17 q->b = 2;
18 q->c = 3;
19 list_replace_rcu(&p->list, &q->list);
20 synchronize_rcu();
21 kfree(p);
19、20、21就是三个步骤,16-19就是read-copy-update,允许同时读取的。
对于synchronize_rcu(),需要等待RCU read-side临界区都完成,但是rcu_read_lock和rcu_read_unlock在未配置PREEMPT的内核里不会有任何代码产生,那如何识别
trick:read-side临界区不可block或sleep
所以CPU进行上下文切换时,可以保证read-side临界区已经完成,CPU只要执行一次上下文切换,那么synchronize_rcu可以返回
1 for_each_online_cpu(cpu)
2 run_on(cpu);
用上面代码表现synchronize_rcu,对每个cpu进行上下文切换,保证read-side临界区都完成。对于non-CONFIG_PREEMPT 和 CONFIG_PREEMPT 内核,已经禁止临界区抢占,所以没什么问题。
对于CONFIG_PREEMPT_RT 实时 (-rt) 内核不奏效。 这种情况下不能使用禁止抢占的策略,需要用其他策略以保证实时性。
内核里实际代码很复杂,需要处理中断、NMI、CPU热插拔等,同时要保证性能和扩展性。
如何保持更新的多版本
deletion
1 struct foo {
2 struct list_head list;
3 int a;
4 int b;
5 int c;
6 };
7 LIST_HEAD(head);
8
9 /* . . . */
1 p = search(head, key);
2 if (p != NULL) {
3 list_del_rcu(&p->list);
4 synchronize_rcu();
5 kfree(p);
6 }
(图)
图中没有画tail->head的指针
replacement