1.概述
当一个进程(线程)试图获取一个被别的执行单元占有的自旋锁时,只能在干等.这类似于"马桶".如果一个人占住了马桶,另一个想上厕所的人只能在门外干等.因此,试图占有自旋锁或已经占有自旋锁的执行单元,不能够睡眠.要不然,占着马桶睡觉去,你不把门外的那个谁给憋坏了...因此,"马桶"的占有者也必须占有时间短.
应用场景:
如果被保护的共享资源在中断上下文(包括底半部和上半部中断),只能用自旋锁;如果被保护的共享资源在进程上下文,使用信号量比较合适,因为进程上下文允许睡眠.
特点:
试图获取自旋锁的执行单元会一直忙等待.
实现策略:
由底层汇编实现.
2.自旋锁的内核API
2-1 定义:
spinlock_t lock;
2-2 初始化:
spin_lock_init(&lock);
自旋锁使用前必须先初始化.
2-3 获取自旋锁:
spin_lock(lock);
该宏用于获取自旋锁lock,如果能够立即获取的话,马上返回;否则,它将自旋在那里,直到该锁的占有者释放.
spin_trylock(&lock);
该宏尝试获取自旋锁lock,如果能获取立即获取,并返回真;否则立即返回假.它比上述的函数的好处是不需要原地等待目标自旋锁持有者释放自旋锁.
2-4 释放自旋锁
spin_unlock(&lock);
该函数释放自旋锁lock,它与spin_trylock()或spin_lock()配对使用.
2-5 自旋锁的使用示意代码
spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock);
your code need to be protected.
spin_unlock(&lock);
2-6 自旋锁的局限
自旋锁仅仅局限于保证临界区代码不被别的CPU和本CPU的内核抢占进程打扰,但是,像中断及底半部,还是中断持有自旋锁代码的执行的.因此,如果在中断或底半部中如果有对自旋锁保护的代码有访问是有效的.因此,实际编程中,我们应该把这种影响排除在外.内核专门为处理这种情况对自旋锁进行了衍生.自旋锁衍生的内核API.
如下:
spin_lock_irq() = spin_lock() + local_irq_disable();
spin_unlock_irq() = spin_unlock() + local_irq_enable();
spin_lock_irqsave() = spin_lock() + local_irq_save();
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore();
spin_lock_bh() = spin_lock() + local_bh_disable();
spin_unlock_bh() = spin_unlock() + local_bh_enable().
因此,spin_lock()/spin_unlock()是自旋锁这套机制的基础.
2-7 实际编程中需要的事项
2-7-1:占有自旋锁时间要短
自旋锁实际是忙等待,当锁不可用时,CPU一直循环执行"测试并设置"该锁直到可用而取得该锁,CPU在此期间不做任何有用的工作,仅仅是等待.因此,只有在占用锁的时间极短的情况下,使用自旋锁才是合理的.
2-7-2: 防止递归死锁
如果一个已经拥有某个自旋锁的CPU想第二次获得这个自旋锁,则该CPU将死锁.此外,如果进程获得自旋锁之后再阻塞,也有可能导致死锁的发生.copy_from_user()、copy_to_user()和kmalloc()等函数有可能引起阻塞,因此在自旋锁的占用时间不能调用这些函数.
2-8 实例
使用自锁锁使设备只能被一个进程打开
int xxx_count = 0;
static spinlock_t xxx_lock = SPIN_LOCK_UNLOCKED;static int xxx_open(struct inode *inode,struct file *filp)
{
spin_lock(&xxx_lock);
if(xxx_count)
{
spin_unlock(&xxx_lock);
return -EBUSY;
}
xxx_count++;
spin_unlock(&xxx_lock);
return 0;
}
static int xxx_release(struct inode *inode,struct file *filp)
{
spin_lock(&xxx_lock);
xxx_count--;
spin_unlock(xxx_lock);
return 0;
}
3.读写自旋锁
读写自旋锁其实算是自旋转的一种优化.自旋锁锁定的临界区不管是读或者写,它都一视同仁.即使多个执行单元同时读取临界资源也会被锁住.实际上,对共享资源并发访问时,多个执行单元同时读取它是不会有问题的,因此,读写自旋锁(rwlock)便应运而生,它支持多个读进程并发访问.
特点:
读并发.
实现策略:
在读写之间是互斥的、在写与写之间也是互斥的.但是读执行单元之间是并发是没问题的.
读写自锁琐在实际编程中需要注意:
1).最多只有一个写进程访问;
2).在读操作可以有多个读进程访问,但是读进程和写进程不能同时进行.
3-1 读写自旋锁的定义及初始化
rwlock_t my_rwlock = RW_UNLOCKED; //静态初始化
rwlock_t my_rwlock;
rwlock_init(&my_rwlock);
3-2 读锁定
void read_lock(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock,unsigned long flags);
void read_lock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);
3-3 读解锁
void read_unlock(rwlock_t *lock);
void read_unlock_irqrestore(rwlock_t *lock,unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);
3-4 写锁定
void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock,unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
void write_trylock(rwlock_t *lock);
3-5 写解锁
void write_unlock_(rwlock_t *lock);
void write_unlock_irqrestore(rwlock_t *lock,unsigned flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);
在对共享资源进行写之前,应该先调用写锁函数,完成之后应调用写解锁函数.和spin_trylock()一样,write_lock()也只是尝试获取读写自旋锁,不管成功失败,都会立即返回.
读写自旋锁使用示意代码如下:
rwlock_t lock;
rwlock_init(&lock);
read_lock(&lock);
your code need to be protect.
read_unlock(&lock);
write_lock_irqsave(&lock,flags);
your code need to be protect
write_unlock_irqrestore(&lock,flags);
4. 顺序锁
顺序锁(seqlock)是对读写锁的一种优化,优化表现在:顺序锁保护的共享资源进行着写操作时,读执行单元仍然有效;同样,顺序锁保护的共享资源进行读操作时,写执行单元仍然有效.
特点:
读执行单元和写执行单元可以同时执行.
实现策略:
按照上述的说法,对被顺序锁保护的资源,读写是可以同时进行的,那么,不会导致数据混乱?其实,顺序锁的实现是基于读写、是否同时进行的概率的基础上进行优化的.因为读写同时进行的可能性是很小的.
顺序锁的注意事项:
1).写执行单元与写执行单元之间仍然是互斥的;
2).如果读执行单元执行期间发生了写操作,读执行单元必须重新读取数据,以确保数据的完整性;
3).被保护的共享资源不能有指针,因为写执行单元可能使指针失效,如果读执行单元正要访问该指针,将导致oops.
4-1 写执行单元的顺序操作API
void write_seqlock(seqlock_t *sl)
int write_tryseqlock(seqlock_t *sl)
write_seqlock_irqsave(lock,flags);
write_seqlock_irq(lock);
write_seqlock_bh(lock);
其中,
write_seqlock_irsave() = local_irq_save() + write_seqlock()
write_seqlock_irq() = local_irq_disable() + write_seqlock()
write_seqlock_bh() = local_bh_disable() + write_seqlock()
可见,write_seqlock()是写执行单元的顺序锁的基本操作.
4-2 释放顺序锁
void write_sequnlock(seqlock_t *sl)
write_sequnlock_irqrestore(lock,flags)
write_sequnlock_irq(lock)
write_seqlock_bh(lock)
其中,
write_sequnlock_irqrestore() = write_sequnlock() + local_irq_restore()
write_sequnlock_irq() = write_sequnlock() + local_irq_enable()
write_sequnlock_bh() = write_sequnlock() + local_bh_enable()
可见,write_sequnlock()是写顺序锁执行的最基本单元.
写执行单元使用顺序锁的模式如下:
write_seqlock(&seqlock_a);
...
write_sequnlock(&seqlock_a);
4-3 读执行单元的顺序锁操作API
unsigned read_seqbegin(const seqlock_t *s1);
read_seqbegin_irqsave(lock,flags);
读执行单元在对被顺序锁s1保护的共享资源进行访问前需要调用该函数,该函数仅返回顺序锁s1的当前顺序号.其中,
read_seqbegin_irqsave() = local_irq_save() + read_seqbegin()
当读执行单元在访问完被顺序锁s1保护的共享资源时,需要检查一下是否在读执行期间发生了写操作,如果有写操作,必须重新执行读操作以确保数据的完整性、准确性.
int read_seqretry(const seqlock_t *s1,unsigned iv);
read_seqretry_irqrestore(lock,iv,flags);
其中,
read_seqretry_irqrestore() = read_seqretry() + local_irq_restore()
读执行单元使用顺序的模式如下:
do{
seqnum = read_seqbegin(&seqlock_a);
}while(read_seqretry(&seqlock_a,seqnum));
1.RCU简介:
1-1.RCU的优势:
1).不使用原子操作,不需要内存栅,不会导致锁竞争、内存延迟及流水线停滞,不需要锁,因此不用考虑死锁的问题;
2).可以允许并发的读写操作;
1-2.RCU的劣势:
1).这种锁机制的使用范围比较窄,它只适用于读多写少的情况,如网络路由表的查询、设备状态表的维护、数据结构的延迟释放以及多径I/O设备的维护等;
2).写者之间的同步开销比较大,它需要延迟数据结构的释放,复制被修改的数据结构.
2.RCU的实现思想:
2-1.宏观思想:
上述RCU的优势已经讲述了,RCU锁机制允许多个读写并发操作,但是它是如何防止读写并发操作而带来的数据混乱呢?最重要的思想是对RCU锁机制保护的临界的资源,RCU会实现一个对原数据的"副本"数据备份.后续的读写操作都是面对"副本"这个对象操作的,然后再会在适当的时机把"副本"数据更新到"原本".
2-1-1.读读并发:
多个读进程并发访问RCU的临界资源,本来就允许的,因为数据并不在读的过程中发生改变.
2-1-2.读写并发:
每一次对RCU保护的临界资源进行读操作后,为了保证数据的完整性、准确性,必须在读数据后进行侦听读数据期间是否有写动作访问RCU保护的临界资源.如果有写动作发生在读数据过程中,必须再进行读动作.
2-1-3.写写并发:
对RCU保护的临界资源的多个写并发之间必须是互斥的.
2-2.微观源码:
对RCU保护的临界资源,读执行单元是不需要考虑同步问题,这是合情合理的.最主要的矛盾是解决读写数据的准确性.
2-2-1.读执行单元:
读执行单元必须提供一个信号给写者以便写者能够确定数据可以被安全地释放或修改的时机.这些信号都会被一个内核的垃圾收集器来收集.一旦所有的读执行单元都发出信号
告知它们都不再使用被RCU保护的数据结构,垃圾收集器就调用回调函数完成最后的数据数据释放或修改操作.读执行单元完成后,会进行读执行期间是否有写执行动作.如果有写执行动作,则进行重读动作.
2-2-2.写执行单元:
写者修改数据前首先拷贝一个被修改元素的副本,然后在副本上进行修改,修改完毕后它向垃圾回收器注册一个回调函数以便在适当的时机执行真正修改操作.这个适当的时机
被称为grace period,而CPU发生了上下文切换称为经历了一个quiescent state,grace period就是所有CPU都经历一次quiescent state所需要的等待时间.垃圾收集器就是在grace
period之后调用写者注册的回调函数来完成真正的数据修改或数据释放.
以下以链表元素删除为例详细说明这一过程.
写者要从链表中删除元素 B,它首先遍历该链表得到指向元素 B 的指针,然后修改元素 B 的前一个元素的 next 指针指向元素 B 的 next 指针指向的元素C,修改元素 B 的 next 指针指向的元素 C 的 prep 指针指向元素 B 的 prep指针指向的元素 A,在这期间可能有读者访问该链表,修改指针指向的操作是原子的,所以不需要同步,而元素 B 的指针并没有去修改,因为读者可能正在使用 B 元素来得到下一个或前一个元素。写者完成这些操作后注册一个回调函数以便在 grace period 之后删除元素 B,然后就认为已经完成删除操作。垃圾收集器在检测到所有的CPU不在引用该链表后,即所有的 CPU 已经经历了 quiescent state,grace period 已经过去后,就调用刚才写者注册的回调函数删除了元素 B。
上述大致流程如下:
读执行单元都完成了RCU临界资源的访问后,发信号给垃圾收集器;垃圾收集器接收到这一信息后,便调用写执行单元在垃圾收集器注册的函数.
源码实现细则:
1).注册回调函数:
写操作往垃圾收集器注册是通过函数call_rcu()及call_rcu_bh()函数实现的.相应地维护了两个数据链表:rcu_data,rcu_bh_data.这两个数据链表分别保存call_rcu()、call_rcu_bh()函数注册的回调函数,先注册的排在前头,后注册的排在末尾;
2).适当的时机:
当CPU发生进程上下文切换时,函数rcu_qsctr_inc()将被调用以标记该CPU经历了一个quiescent state.该函数也会被时钟中断触发调用.在时钟中断触发垃圾收集器的运行.垃圾收集器便调用相应的回调函数.
3.内核API:
3-1.保护临界资源
Rcu_read_lock()
读者在读取由RCU保护的共享数据时使用该函数标记它进入读端临界区.
Rcu_read_unlock()
读者在退出读端临界区调用它.
夹在上述两函数之间的代码就是RCU保护的临界资源.RCU可以嵌套.
3-2.同步:
RCU的同步主要用于写者啥时候允许把其"副本"数据回写到原本数据上必须进行同步.
Synchronize_rcu()
该函数由RCU写端调用,它将阻塞写者,直到经过grace period后,即所有读者已经完成读端临界区,写者才可以下一步操作.如果有多个RCU写端调用该函数,他们将在一个grace period之后全部被唤醒.
Synchronize_sched()
该函数用于等待所有CPU都处于可抢占状态,它能保证正在运行的中断处理函数处理完毕,但是不能保证正在运行的softirq处理完毕.
3-3.挂载回调函数
void fastcall call_rcu(struct rcu_head *head,void (*func)(struct rcu_head *rcu))
该函数由RCU写端调用,它不会使写执行单元阻塞,可以在中断上下文或软中断使用.该函数将把函数 func 挂接到 RCU回调函数链rcu_data上,然后立即返回.
void fastcall call_rcu_bh(struct rcu_head *head,void (*func)(struct rcu_head *rcu))
该函数和上述的call_rcu()完全相同,唯一的区别是它把软中断的完成也当作经历了一个quiescent state,因此,如果写执行单元使用了该函数,在进程上下文的读执行单元必须使用rcu_read_lock_bh().
3-4.RCU版本的链表操作API
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)
该函数把链表元素new插入到RCU保护的链表head的末尾.
Static inline void list_del_rcu(struct list_head *entry);
该函数从RCU保护的链表中删除指定的链表元素entry.
Static inline void list_replace_rcu(struct list_head *old,struct list_head *new);
该函数是RCU新添加的函数,它使用新的链表元素new取代旧的链表元素old.
List_for_each_rcu(pos,head)
遍历由RCU保护的链表head.
List_for_each_safe_rcu(pos,n,head)
遍历由RCU保护的链表head,并允许安全删除当前链表元素pos.
List_for_each_entry_rcu(pos,head,member)
该宏遍历由RCU保护的链表head,通过它可以找到特定的数据结构.其中,pos为一个包含struct list_head结构的特定的数据结构.
Static inline void hlist_del_rcu(struct hlist_node *n)
它从由RCU保护的哈希链表中移走链表元素n.
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.
Hlist_for_each_entry_rcu(tpos,pos,head,member)
遍历指定类型的数据结构哈希链表,并找到特定的数据结构pos.pos为包含struct list_head结构的特定数据结构.
4.内核实例:
表 1 rcu_read_lock 的使用情况统计
表 2 rcu_read_unlock 的使用情况统计
表 3 rcu_read_lock_bh 的使用情况统计
表 4 rcu_read_unlock_bh 的使用情况统计
表 5 call_rcu 的使用情况统计
表 6 call_rcu_bh 的使用情况统计
表 7 list API 的使用情况统计
表 8 synchronize_rcu 的使用情况统计
表 9 rcu_dereferance 的使用情况统计
表 10 所有RCU API使用情况总汇