级别: 初级
杨 燚 ([email protected]), 计算机科学硕士
2005 年 8 月 15 日
这是本系列文章的第二部分,它详细地介绍了Linux内核中的同步机制:大内核锁、读写锁、大读者锁、RCU和顺序锁的API,使用要求以及一些典型示例。本系列文章的第一部分则详细地介绍了 Linux 内核中的其它一些同步机制,包括原子操作、信号量、读写信号量和自旋锁的API,使用要求以及一些典型示例。
六、大内核锁(BKL--Big Kernel Lock)
大内核锁本质上也是自旋锁,但是它又不同于自旋锁,自旋锁是不可以递归获得锁的,因为那样会导致死锁。但大内核锁可以递归获得锁。大内核锁用于保护整个内核,而自旋锁用于保护非常特定的某一共享资源。进程保持大内核锁时可以发生调度,具体实现是:在执行schedule时,schedule将检查进程是否拥有大内核锁,如果有,它将被释放,以致于其它的进程能够获得该锁,而当轮到该进程运行时,再让它重新获得大内核锁。注意在保持自旋锁期间是不运行发生调度的。
需要特别指出,整个内核只有一个大内核锁,其实不难理解,内核只有一个,而大内核锁是保护整个内核的,当然有且只有一个就足够了。
还需要特别指出的是,大内核锁是历史遗留,内核中用的非常少,一般保持该锁的时间较长,因此不提倡使用它。从2.6.11内核起,大内核锁可以通过配置内核使其变得可抢占(自旋锁是不可抢占的),这时它实质上是一个互斥锁,使用信号量实现。
大内核锁的API包括:
void lock_kernel(void);
该函数用于得到大内核锁。它可以递归调用而不会导致死锁。
void unlock_kernel(void);
该函数用于释放大内核锁。当然必须与lock_kernel配对使用,调用了多少次lock_kernel,就需要调用多少次unlock_kernel。
大内核锁的API使用非常简单,按照以下方式使用就可以了:
lock_kernel();
//对被保护的共享资源的访问
…
unlock_kernel();
七、读写锁(rwlock)
读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。
在读写锁保持期间也是抢占失效的。
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。
读写锁的API看上去与自旋锁很象,只是读者和写者需要不同的获得和释放锁的API。下面是读写锁API清单:
rwlock_init(x)
该宏用于动态初始化读写锁x。
DEFINE_RWLOCK(x)
该宏声明一个读写锁并对其进行初始化。它用于静态初始化。
RW_LOCK_UNLOCKED
它用于静态初始化一个读写锁。
DEFINE_RWLOCK(x)等同于rwlock_t x = RW_LOCK_UNLOCKED
read_trylock(lock)
读者用它来尽力获得读写锁lock,如果能够立即获得读写锁,它就获得锁并返回真,否则不能获得锁,返回假。无论是否能够获得锁,它都将立即返回,绝不自旋在那里。
write_trylock(lock)
写者用它来尽力获得读写锁lock,如果能够立即获得读写锁,它就获得锁并返回真,否则不能获得锁,返回假。无论是否能够获得锁,它都将立即返回,绝不自旋在那里。
read_lock(lock)
读者要访问被读写锁lock保护的共享资源,需要使用该宏来得到读写锁lock。如果能够立即获得,它将立即获得读写锁并返回,否则,将自旋在那里,直到获得该读写锁。
write_lock(lock)
写者要想访问被读写锁lock保护的共享资源,需要使用该宏来得到读写锁lock。如果能够立即获得,它将立即获得读写锁并返回,否则,将自旋在那里,直到获得该读写锁。
read_lock_irqsave(lock, flags)
读者也可以使用该宏来获得读写锁,与read_lock不同的是,该宏还同时把标志寄存器的值保存到了变量flags中,并失效了本地中断。
write_lock_irqsave(lock, flags)
写者可以用它来获得读写锁,与write_lock不同的是,该宏还同时把标志寄存器的值保存到了变量flags中,并失效了本地中断。
read_lock_irq(lock)
读者也可以用它来获得读写锁,与read_lock不同的是,该宏还同时失效了本地中断。该宏与read_lock_irqsave的不同之处是,它没有保存标志寄存器。
write_lock_irq(lock)
写者也可以用它来获得锁,与write_lock不同的是,该宏还同时失效了本地中断。该宏与write_lock_irqsave的不同之处是,它没有保存标志寄存器。
read_lock_bh(lock)
读者也可以用它来获得读写锁,与与read_lock不同的是,该宏还同时失效了本地的软中断。
write_lock_bh(lock)
写者也可以用它来获得读写锁,与write_lock不同的是,该宏还同时失效了本地的软中断。
read_unlock(lock)
读者使用该宏来释放读写锁lock。它必须与read_lock配对使用。
write_unlock(lock)
写者使用该宏来释放读写锁lock。它必须与write_lock配对使用。
read_unlock_irqrestore(lock, flags)
读者也可以使用该宏来释放读写锁,与read_unlock不同的是,该宏还同时把标志寄存器的值恢复为变量flags的值。它必须与read_lock_irqsave配对使用。
write_unlock_irqrestore(lock, flags)
写者也可以使用该宏来释放读写锁,与write_unlock不同的是,该宏还同时把标志寄存器的值恢复为变量flags的值,并使能本地中断。它必须与write_lock_irqsave配对使用。
read_unlock_irq(lock)
读者也可以使用该宏来释放读写锁,与read_unlock不同的是,该宏还同时使能本地中断。它必须与read_lock_irq配对使用。
write_unlock_irq(lock)
写者也可以使用该宏来释放读写锁,与write_unlock不同的是,该宏还同时使能本地中断。它必须与write_lock_irq配对使用。
read_unlock_bh(lock)
读者也可以使用该宏来释放读写锁,与read_unlock不同的是,该宏还同时使能本地软中断。它必须与read_lock_bh配对使用。
write_unlock_bh(lock)
写者也可以使用该宏来释放读写锁,与write_unlock不同的是,该宏还同时使能本地软中断。它必须与write_lock_bh配对使用。
读写锁的获得和释放锁的方法也有许多版本,具体用哪个与自旋锁一样,因此参考自旋锁部分就可以了。只是需要区分读者与写者,读者要用读者版本,而写者必须用写者版本。
八、大读者锁(brlock-Big Reader Lock)
大读者锁是读写锁的高性能版,读者可以非常快地获得锁,但写者获得锁的开销比较大。大读者锁只存在于2.4内核中,在2.6中已经没有这种锁(提醒读者特别注意)。它们的使用与读写锁的使用类似,只是所有的大读者锁都是事先已经定义好的。这种锁适合于读多写少的情况,它在这种情况下远好于读写锁。
大读者锁的实现机制是:每一个大读者锁在所有CPU上都有一个本地读者写者锁,一个读者仅需要获得本地CPU的读者锁,而写者必须获得所有CPU上的锁。
大读者锁的API非常类似于读写锁,只是锁变量为预定义的锁ID。
void br_read_lock (enum brlock_indices idx);
读者使用该函数来获得大读者锁idx,在2.4内核中,预定义的idx允许的值有两个:BR_GLOBALIRQ_LOCK和BR_NETPROTO_LOCK,当然,用户可以添加自己定义的大读者锁ID 。
void br_read_unlock (enum brlock_indices idx);
读者使用该函数释放大读者锁idx。
void br_write_lock (enum brlock_indices idx);
写者使用它来获得大读者锁idx。
void br_write_unlock (enum brlock_indices idx);
写者使用它来释放大读者锁idx。
br_read_lock_irqsave(idx, flags)
读者也可以使用该宏来获得大读者锁idx,与br_read_lock不同的是,该宏还同时把寄存器的值保存到变量flags中,并且失效本地中断。
br_read_lock_irq(idx)
读者也可以使用该宏来获得大读者锁idx,与br_read_lock不同的是,该宏还同时失效本地中断。与br_read_lock_irqsave不同的是,该宏不保存标志寄存器。
br_read_lock_bh(idx)
读者也可以使用该宏来获得大读者锁idx,与br_read_lock不同的是,该宏还同时失效本地软中断。
br_write_lock_irqsave(idx, flags)
写者也可以使用该宏来获得大读者锁idx,与br_write_lock不同的是,该宏还同时把寄存器的值保存到变量flags中,并且失效本地中断。
br_write_lock_irq(idx)
写者也可以使用该宏来获得大读者锁idx,与br_write_lock不同的是,该宏还同时失效本地中断。与br_write_lock_irqsave不同的是,该宏不保存标志寄存器。
br_write_lock_bh(idx)
写者也可以使用该宏来获得大读者锁idx,与br_write_lock不同的是,该宏还同时失效本地软中断。
br_read_unlock_irqrestore(idx, flags)
读者也使用该宏来释放大读者锁idx,它与br_read_unlock不同之处是,该宏还同时把变量flags的值恢复到标志寄存器。
br_read_unlock_irq(idx)
读者也使用该宏来释放大读者锁idx,它与br_read_unlock不同之处是,该宏还同时使能本地中断。
br_read_unlock_bh(idx)
读者也使用该宏来释放大读者锁idx,它与br_read_unlock不同之处是,该宏还同时使能本地软中断。
br_write_unlock_irqrestore(idx, flags)
写者也使用该宏来释放大读者锁idx,它与br_write_unlock不同之处是,该宏还同时把变量flags的值恢复到标志寄存器。
br_write_unlock_irq(idx)
写者也使用该宏来释放大读者锁idx,它与br_write_unlock不同之处是,该宏还同时使能本地中断。
br_write_unlock_bh(idx)
写者也使用该宏来释放大读者锁idx,它与br_write_unlock不同之处是,该宏还同时使能本地软中断。
这些API的使用与读写锁完全一致。
九、RCU(Read-Copy Update)
RCU(Read-Copy Update),顾名思义就是读-拷贝修改,它是基于其原理命名的。对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它,但写者在访问它时首先拷贝一个副本,然后对副本进行修改,最后使用一个回调(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。这个时机就是所有引用该数据的CPU都退出对共享数据的操作。
RCU也是读写锁的高性能版本,但是它比大读者锁具有更好的扩展性和性能。 RCU既允许多个读者同时访问被保护的数据,又允许多个读者和多个写者同时访问被保护的数据(注意:是否可以有多个写者并行访问取决于写者之间使用的同步机制),读者没有任何同步开销,而写者的同步开销则取决于使用的写者间同步机制。但RCU不能替代读写锁,因为如果写比较多时,对读者的性能提高不能弥补写者导致的损失。
RCU的API如下;
rcu_read_lock()
读者在读取由RCU保护的共享数据时使用该函数标记它进入读端临界区。
rcu_read_unlock()
该函数与rcu_read_lock配对使用,用以标记读者退出读端临界区。
synchronize_rcu()
该函数由RCU写端调用,它将阻塞写者,直到经过grace period后,即所有的读者已经完成读端临界区,写者才可以继续下一步操作。如果有多个RCU写端调用该函数,他们将在一个grace period之后全部被唤醒。
synchronize_kernel()
其他非RCU的内核代码使用该函数来等待所有CPU处在可抢占状态,目前功能等同于synchronize_rcu,但现在已经不建议使用,而使用synchronize_sched。
synchronize_sched()
该函数用于等待所有CPU都处在可抢占状态,它能保证正在运行的中断处理函数处理完毕,但不能保证正在运行的softirq处理完毕。注意,synchronize_rcu只保证所有CPU都处理完正在运行的读端临界区。
void fastcall call_rcu(struct rcu_head *head,
void (*func)(struct rcu_head *rcu))
struct rcu_head {
struct rcu_head *next;
void (*func)(struct rcu_head *head);
};
函数call_rcu也由RCU写端调用,它不会使写者阻塞,因而可以在中断上下文或softirq使用。该函数将把函数func挂接到RCU回调函数链上,然后立即返回。一旦所有的CPU都已经完成端临界区操作,该函数将被调用来释放删除的将绝不在被应用的数据。参数head用于记录回调函数func,一般该结构会作为被RCU保护的数据结构的一个字段,以便省去单独为该结构分配内存的操作。需要指出的是,函数synchronize_rcu的实现实际上使用函数call_rcu。
void fastcall call_rcu_bh(struct rcu_head *head,
void (*func)(struct rcu_head *rcu))
函数call_ruc_bh功能几乎与call_rcu完全相同,唯一差别就是它把softirq的完成也当作经历一个quiescent state,因此如果写端使用了该函数,在进程上下文的读端必须使用rcu_read_lock_bh。
#define rcu_dereference(p) ({ \
typeof(p) _________p1 = p; \
smp_read_barrier_depends(); \
(_________p1); \
}
该宏用于在RCU读端临界区获得一个RCU保护的指针,该指针可以在以后安全地引用,内存栅只在alpha架构上才使用。
除了这些API,RCU还增加了链表操作的RCU版本,因为对于RCU,对共享数据的操作必须保证能够被没有使用同步机制的读者看到,所以内存栅是非常必要的。
static inline void list_add_rcu(struct list_head *new, struct list_head *head)
该函数把链表项new插入到RCU保护的链表head的开头。使用内存栅保证了在引用这个新插入的链表项之前,新链表项的链接指针的修改对所有读者是可见的。
static inline void list_add_tail_rcu(struct list_head *new,
struct list_head *head)
该函数类似于list_add_rcu,它将把新的链表项new添加到被RCU保护的链表的末尾。
static inline void list_del_rcu(struct list_head *entry)
该函数从RCU保护的链表中移走指定的链表项entry,并且把entry的prev指针设置为LIST_POISON2,但是并没有把entry的next指针设置为LIST_POISON1,因为该指针可能仍然在被读者用于便利该链表。
static inline void list_replace_rcu(struct list_head *old, struct list_head *new)
该函数是RCU新添加的函数,并不存在非RCU版本。它使用新的链表项new取代旧的链表项old,内存栅保证在引用新的链表项之前,它的链接指针的修正对所有读者可见。
list_for_each_rcu(pos, head)
该宏用于遍历由RCU保护的链表head,只要在读端临界区使用该函数,它就可以安全地和其它_rcu链表操作函数(如list_add_rcu)并发运行。
list_for_each_safe_rcu(pos, n, head)
该宏类似于list_for_each_rcu,但不同之处在于它允许安全地删除当前链表项pos。
list_for_each_entry_rcu(pos, head, member)
该宏类似于list_for_each_rcu,不同之处在于它用于遍历指定类型的数据结构链表,当前链表项pos为一包含struct list_head结构的特定的数据结构。
list_for_each_continue_rcu(pos, head)
该宏用于在退出点之后继续遍历由RCU保护的链表head。
static inline void hlist_del_rcu(struct hlist_node *n)
它从由RCU保护的哈希链表中移走链表项n,并设置n的ppre指针为LIST_POISON2,但并没有设置next为LIST_POISON1,因为该指针可能被读者使用用于遍利链表。
static inline void hlist_add_head_rcu(struct hlist_node *n,
struct hlist_head *h)
该函数用于把链表项n插入到被RCU保护的哈希链表的开头,但同时允许读者对该哈希链表的遍历。内存栅确保在引用新链表项之前,它的指针修正对所有读者可见。
hlist_for_each_rcu(pos, head)
该宏用于遍历由RCU保护的哈希链表head,只要在读端临界区使用该函数,它就可以安全地和其它_rcu哈希链表操作函数(如hlist_add_rcu)并发运行。
hlist_for_each_entry_rcu(tpos, pos, head, member)
类似于hlist_for_each_rcu,不同之处在于它用于遍历指定类型的数据结构哈希链表,当前链表项pos为一包含struct list_head结构的特定的数据结构。
对于RCU更详细的原理、实现机制以及应用请参看作者专门针对RCU发表的一篇文章,"Linux 2.6内核中新的锁机制--RCU(Read-Copy Update)"。
十、顺序锁(seqlock)
顺序锁也是对读写锁的一种优化,对于顺序锁,读者绝不会被写者阻塞,也就说,读者可以在写者对被顺序锁保护的共享资源进行写操作时仍然可以继续读,而不必等待写者完成写操作,写者也不需要等待所有读者完成读操作才去进行写操作。但是,写者与写者之间仍然是互斥的,即如果有写者在进行写操作,其他写者必须自旋在那里,直到写者释放了顺序锁。
这种锁有一个限制,它必须要求被保护的共享资源不含有指针,因为写者可能使得指针失效,但读者如果正要访问该指针,将导致OOPs。
如果读者在读操作期间,写者已经发生了写操作,那么,读者必须重新读取数据,以便确保得到的数据是完整的。
这种锁对于读写同时进行的概率比较小的情况,性能是非常好的,而且它允许读写同时进行,因而更大地提高了并发性。
顺序锁的API如下:
void write_seqlock(seqlock_t *sl);
写者在访问被顺序锁s1保护的共享资源前需要调用该函数来获得顺序锁s1。它实际功能上等同于spin_lock,只是增加了一个对顺序锁顺序号的加1操作,以便读者能够检查出是否在读期间有写者访问过。
void write_sequnlock(seqlock_t *sl);
写者在访问完被顺序锁s1保护的共享资源后需要调用该函数来释放顺序锁s1。它实际功能上等同于spin_unlock,只是增加了一个对顺序锁顺序号的加1操作,以便读者能够检查出是否在读期间有写者访问过。
写者使用顺序锁的模式如下:
write_seqlock(&seqlock_a);
//写操作代码块
…
write_sequnlock(&seqlock_a);
因此,对写者而言,它的使用与spinlock相同。
int write_tryseqlock(seqlock_t *sl);
写者在访问被顺序锁s1保护的共享资源前也可以调用该函数来获得顺序锁s1。它实际功能上等同于spin_trylock,只是如果成功获得锁后,该函数增加了一个对顺序锁顺序号的加1操作,以便读者能够检查出是否在读期间有写者访问过。
unsigned read_seqbegin(const seqlock_t *sl);
读者在对被顺序锁s1保护的共享资源进行访问前需要调用该函数。读者实际没有任何得到锁和释放锁的开销,该函数只是返回顺序锁s1的当前顺序号。
int read_seqretry(const seqlock_t *sl, unsigned iv);
读者在访问完被顺序锁s1保护的共享资源后需要调用该函数来检查,在读访问期间是否有写者访问了该共享资源,如果是,读者就需要重新进行读操作,否则,读者成功完成了读操作。
因此,读者使用顺序锁的模式如下:
do {
seqnum = read_seqbegin(&seqlock_a);
//读操作代码块
...
} while (read_seqretry(&seqlock_a, seqnum));
write_seqlock_irqsave(lock, flags)
写者也可以用该宏来获得顺序锁lock,与write_seqlock不同的是,该宏同时还把标志寄存器的值保存到变量flags中,并且失效了本地中断。
write_seqlock_irq(lock)
写者也可以用该宏来获得顺序锁lock,与write_seqlock不同的是,该宏同时还失效了本地中断。与write_seqlock_irqsave不同的是,该宏不保存标志寄存器。
write_seqlock_bh(lock)
写者也可以用该宏来获得顺序锁lock,与write_seqlock不同的是,该宏同时还失效了本地软中断。
write_sequnlock_irqrestore(lock, flags)
写者也可以用该宏来释放顺序锁lock,与write_sequnlock不同的是,该宏同时还把标志寄存器的值恢复为变量flags的值。它必须与write_seqlock_irqsave配对使用。
write_sequnlock_irq(lock)
写者也可以用该宏来释放顺序锁lock,与write_sequnlock不同的是,该宏同时还使能本地中断。它必须与write_seqlock_irq配对使用。
write_sequnlock_bh(lock)
写者也可以用该宏来释放顺序锁lock,与write_sequnlock不同的是,该宏同时还使能本地软中断。它必须与write_seqlock_bh配对使用。
read_seqbegin_irqsave(lock, flags)
读者在对被顺序锁lock保护的共享资源进行访问前也可以使用该宏来获得顺序锁lock的当前顺序号,与read_seqbegin不同的是,它同时还把标志寄存器的值保存到变量flags中,并且失效了本地中断。注意,它必须与read_seqretry_irqrestore配对使用。
read_seqretry_irqrestore(lock, iv, flags)
读者在访问完被顺序锁lock保护的共享资源进行访问后也可以使用该宏来检查,在读访问期间是否有写者访问了该共享资源,如果是,读者就需要重新进行读操作,否则,读者成功完成了读操作。它与read_seqretry不同的是,该宏同时还把标志寄存器的值恢复为变量flags的值。注意,它必须与read_seqbegin_irqsave配对使用。
因此,读者使用顺序锁的模式也可以为:
do {
seqnum = read_seqbegin_irqsave(&seqlock_a, flags);
//读操作代码块
...
} while (read_seqretry_irqrestore(&seqlock_a, seqnum, flags));
读者和写者所使用的API的几个版本应该如何使用与自旋锁的类似。
如果写者在操作被顺序锁保护的共享资源时已经保持了互斥锁保护对共享数据的写操作,即写者与写者之间已经是互斥的,但读者仍然可以与写者同时访问,那么这种情况仅需要使用顺序计数(seqcount),而不必要spinlock。
顺序计数的API如下:
unsigned read_seqcount_begin(const seqcount_t *s);
读者在对被顺序计数保护的共享资源进行读访问前需要使用该函数来获得当前的顺序号。
int read_seqcount_retry(const seqcount_t *s, unsigned iv);
读者在访问完被顺序计数s保护的共享资源后需要调用该函数来检查,在读访问期间是否有写者访问了该共享资源,如果是,读者就需要重新进行读操作,否则,读者成功完成了读操作。
因此,读者使用顺序计数的模式如下:
do {
seqnum = read_seqbegin_count(&seqcount_a);
//读操作代码块
...
} while (read_seqretry(&seqcount_a, seqnum));
void write_seqcount_begin(seqcount_t *s);
写者在访问被顺序计数保护的共享资源前需要调用该函数来对顺序计数的顺序号加1,以便读者能够检查出是否在读期间有写者访问过。
void write_seqcount_end(seqcount_t *s);
写者在访问完被顺序计数保护的共享资源后需要调用该函数来对顺序计数的顺序号加1,以便读者能够检查出是否在读期间有写者访问过。
写者使用顺序计数的模式为:
write_seqcount_begin(&seqcount_a);
//写操作代码块
…
write_seqcount_end(&seqcount_a);
需要特别提醒,顺序计数的使用必须非常谨慎,只有确定在访问共享数据时已经保持了互斥锁才可以使用。
小结
自linux 2.4以来,内核对SMP的支持越来越好,很大程度上,对SMP的支持,这些锁机制是非常必要和重要的。基本上,内核开发者在开发中都会需要使用一些同步机制,本文通过详细地讲解内核中所有的同步机制,使得读者能够对内核锁机制有全面的了解和把握。
参考资料
Kernel Locking Techniques,http://www.linuxjournal.com/article/5833
Redhat 9.0 kernel source tree
kernel.org 2.6.12 source tree
Linux 2.6内核中新的锁机制--RCU(Read-Copy Update), http://www.ibm.com/developerworks/cn/linux/l-rcu/
Unreliable Guide To Locking.
关于作者
杨燚,计算机科学硕士,毕业于中科院计算技术研究所,有4年的Linux内核编程经验,目前从事嵌入式实时Linux的开发与性能测试。您可以通过[email protected]或[email protected]与作者联系。