Linux内核并发控制
需求 | 建议加锁方法 |
---|---|
低开销加锁 | 优先使用spin_lock |
短期锁定 | 优先使用spin_lock |
长期加锁 | 优先使用mutex |
中断上下文中加锁 | 使用spin_lock |
持有者需要睡眠 | 使用mutex |
1.中断屏蔽
//关闭指定的中断,如果中断没有执行完,等待执行完在关闭,不能再中断中使用,否则自己关闭自己,引起内核崩溃
void disabled_irq(unsigned int irq)
//关闭中断,不等待中断执行完毕,可以在中断函数中执行
void disabled_irq_nosync(unsigned int irq)
//使能指定中断
void enabled_irq(unsigned int irq)
=============================================================================================
//禁止本CPU全部中断,并保存CPU状态信息,(现在很多芯片是多核CPU)这个函数需要和local_irq_restore函数配合使用
local_save_flags(flags)
//与local_save_flags(flags)功能函数相反,配对使用,用来使能由与local_save_flags禁止的中断
local_irq_restore(flags)
=============================================================================================
//禁止本CPU中断,不能保存当前CPU信息
local_irq_disable()
//使能由local_irq_disable禁止的中断,不能还原CPU信息
local_irq_enable()
示例
unsigned long flags;
local_save_flags(flags);
······
local_irq_restore(flags);
2.原子操作
原子操作可以保证对一个整形数据的修改是排他性的。
2.1 整型原子操作
//原子变量初始化
atomic_t a = ATOMIC_INIT(i);
//原子变量设置和读取
atomic_set(atomic_t *v, int i);
atomic_read(atomic_t *v);
//原子变量加减
atomic_add(int i, atomic_t *v);
atomic_sub(int i, atomic_t *v);
//原子变量自增自减
atomic_inc(atomic_t *v);
atomic_dec(atomic_t *v);
//原子变量操作并返回
atomic_add_return(int i, atomic_t *v);
atomic_sub_return(int i, atomic_t *v);
atomic_inc_return(atomic_t *v);
atomic_dec_return(atomic_t *v);
//原子变量加减并测试是否等于零
atomic_add_and_test(int i, atomic_t *v);
atomic_sub_and_test(int i, atomic_t *v);
atomic_inc_and_test(atomic_t *v);
atomic_dec_and_test(atomic_t *v);
atomic_add_negative(int i, atomic_t *v); // return true if the result is negative
//交换
atomic_xchg(v, new); // return the old value
atomic_cmpxchg( atomic_t *v, int old,int new); // return the old value
示例
atomic_t v = ATOMIC_INIT(3);
atomic_add(&v,1);
2.2 位原子操作
//设置位
void set_bit(int nr, volatile unsigned long *addr);
//清除位
void clear_bit(int nr, volatile unsigned long *addr)
//改变位
void change_bit(int nr, volatile unsigned long *addr)
//测试位
int test_bit(int nr, __const__ volatile unsigned long *addr);
//测试并操作位
int test_and_set_bit(int nr, volatile unsigned long *addr);
int test_and_clear_bit(int nr, volatile unsigned long *addr);
int test_and_change_bit(int nr, volatile unsigned long *addr);
2.3 非原子操作
当变量已经被锁保护的情况下,可以使用这些API
void __set_bit(int nr, volatile unsigned long *addr);
void __clear_bit(int nr, volatile unsigned long *addr);
void __change_bit(int nr, volatile unsigned long *addr);
int __test_and_set_bit(int nr, volatile unsigned long *addr);
int __test_and_clear_bit(int nr, volatile unsigned long *addr);
int __test_and_change_bit(int nr, volatile unsigned long *addr);
3.自旋锁
自旋锁是一种典型的对临界资源进行互斥访问的手段,其名称来源于他的工作方式。为了获得一个自旋锁,在某个cpu上运行代码先执行一个原子操作,该操作测试并设置某个内存变量。如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行,如果测试结果表明锁仍被占用,程序将在一个小循环内重复这个测试并设置操作,即进行所谓的自旋。
//初始化自旋锁,将自旋锁设置为1,表示有一个资源可用。
spin_lock_init(_lock)
//循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)
void spin_lock(spinlock_t *lock)
//如果自旋锁被置为1(未锁),返回0,否则返回1。
int spin_is_locked(spinlock_t *lock)
//尝试锁上自旋锁(置0),如果原来锁的值为1,返回1,否则返回0。
int spin_trylock(spinlock_t *lock)
//将自旋锁解锁(置为1)。
void spin_unlock(spinlock_t *lock)
//判断自旋锁是否能够被锁
int spin_can_lock(spinlock_t *lock)
//等待直到自旋锁解锁(为1),返回0;否则返回1。
void spin_unlock_wait(spinlock_t *lock)
//循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。阻止软中断的底半部的执行。
void spin_lock_bh(spinlock_t *lock)
//将自旋锁解锁(置为1)。开启底半部的执行。
void spin_unlock_bh(spinlock_t *lock)
//这些函数成功时返回非零( 获得了锁 ), 否则 0
int spin_trylock_bh(spinlock_t *lock)
spin_lock_nested(lock, subclass)
spin_lock_irqsave_nested(lock, flags, subclass)
spin_lock_nest_lock(lock, nest_lock)
//循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。关中断。
void spin_lock_irq(spinlock_t *lock)
int spin_trylock_irq(spinlock_t *lock)
//将自旋锁解锁(置为1)。开中断。
void spin_unlock_irq(spinlock_t *lock)
//循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。关中断,将状态寄存器值存入flags。
spin_lock_irqsave(lock, flags)
spin_trylock_irqsave(lock, flags)
//将自旋锁解锁(置为1)。开中断,将状态寄存器值从flags存入状态寄存器。
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)
int spin_is_contended(spinlock_t *lock)
示例
struct spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock);
...
spin_unlock(&lock);
4.读写自旋锁
当加读锁时多个进程可以同时访问该临界变量
//读写锁初始化
rwlock_init(lock)
//读锁操作
read_can_lock(rwlock)
read_lock(lock)
read_trylock(lock)
read_lock_irqsave(lock, flags)
read_lock_irqsave(lock, flags)
read_lock_irq(lock)
read_lock_bh(lock)
read_unlock(lock)
read_unlock_irq(lock)
read_unlock_irqrestore(lock, flags)
read_unlock_bh(lock)
//写锁操作
write_can_lock(rwlock)
write_lock(lock)
write_trylock(lock)
write_lock_irqsave(lock, flags)
write_lock_irqsave(lock, flags)
write_lock_irq(lock)
write_lock_bh(lock)
write_trylock_irqsave(lock, flags)
write_unlock(lock)
write_unlock_irq(lock)
write_unlock_irqrestore(lock, flags)
write_unlock_bh(lock)
示例
struct rwlock_t lock;
rwlock_init(&lock);
//读时锁
read_lock(&lock);
...
read_unlock(&lock);
//写时锁
write_lock_irqsave(&lock,flags);
...
write_unlock_irqrestore(&lock,flags);
5.顺序锁
当使用读/写锁时,读者必须等待写者完成时才能读,写者必须等待读者完成时才能写,两者的优先权是平等的。顺序锁是对读/写锁的优化,它允许读写同时进行,提高了并发性,读写操作同时进行的概率较小时,其性能很好。
5.1 顺序锁对读/写锁的优化
- 写者不会阻塞读者,即写操作时,读者仍可以进行读操作。
- 写者不需要等待所有读者完成读操作后才进行写操作。
- 写者与写者之间互斥,即如果有写者在写操作时,其他写者必须自旋等待。
- 如果在读者进行读操作期间,有写者进行写操作,那么读者必须重新读取数据,确保读取正确的数据。
- 要求临界区的共享资源不含指针,因为如果写者使指针失效,读者访问该指针,将导致崩溃。
//初始化顺序锁,将顺序计数器置0。
seqlock_init(x)
//初始化顺序号。
seqcount_init(x)
//返回顺序锁s1的当前顺序号,读者没有开锁和释放锁的开销。
unsigned read_seqbegin(const seqlock_t *sl)
//检查读操作期间是否有写者访问了共享资源,如果是,读者就需要重新进行读操作,否则,读者成功完成读操作。
unsigned read_seqretry(const seqlock_t *sl, unsigned start)
//读者在读操作前用此函数获取当前的顺序号。
unsigned read_seqcount_begin(const seqcount_t *s)
//写者在访问临界区前调用此函数将顺序号加1,以便读者检查是否在读期间有写者访问过。
void write_seqcount_begin(seqcount_t *s)
//写者写完成后调用此函数将顺序号加1,以便读者能检查出是否在读期间有写者访问过。
void write_seqcount_end(seqcount_t *s)
//加顺序锁,将顺序号加1。写者获取顺序锁s1访问临界区,它使用了函数spin_lock
void write_seqlock(seqlock_t *sl)
//解顺序锁,使用了函数spin_unlock,顺序号加1。
void write_sequnlock(seqlock_t *sl)
void write_seqlock_bh(seqlock_t *sl)
void write_sequnlock_bh(seqlock_t *sl)
void write_seqlock_irq(seqlock_t *sl)
void write_sequnlock_irq(seqlock_t *sl)
write_seqlock_irqsave(lock, flags)
void write_sequnlock_irqrestore(seqlock_t *sl, unsigned long flags)
示例
unsigned start;
struct seqlock_t my_seq_lock;
//初始化
seqlock_init(&my_seq_lock);
//写锁
write_seqlock(&my_seq_lock);
start++;
write_sequnlock(&my_seq_lock);
//读锁
do {
start = read_seqbegin(&my_seq_lock);
} while(read_seqretry(&my_seq_lock ,start ))
6.信号量
信号量是用于保护临界区的一种常用方法。它的使用和自旋锁类似。相同的是,只有得到信号量的进程才能执行临界区代码;不同的是,当获取不到信号量时,进程不会原地打转而是进入睡眠等待状态。
/* 定义并初始化信号量 */
DEFINE_SEMAPHORE(name)
//初始化信号量
void sema_init(struct semaphore *sem, int val)
/* 获取信号量 */
void down(struct semaphore *sem);
//会被信号打断,不能再中断上下文中使用,返回值非零应立即返回-ERESTARTSYS
int down_interruptible(struct semaphore *sem);
//可被kill信号打断
int down_killable(struct semaphore *sem);
//尝试获取信号量sem,如果能立即获得,它就获取该信号量并返回0,否则,返回非0。它不会导致调用者睡眠,可以在中断上下文使用
int down_trylock(struct semaphore *sem);
int down_timeout(struct semaphore *sem, long jiffies);
/* 释放信号量 */
void up(struct semaphore *sem);
示例
//DEFINE_SEMAPHORE(sem)//默认初始化一个信号量
struct semaphore sem;
sema_init(&sem,5); //初始化5个信号量
down(&sem);
...
up(&sem);
7.互斥锁
互斥锁主要用于实现内核中的互斥访问功能。内核互斥锁是在原子 API 之上实现的,但这对于内核用户是不可见的。对它的访问必须遵循一些规则:同一时间只能有一个任务持有互斥锁,而且只有这个任务可以对互斥锁进行解锁。互斥锁不能进行递归锁定或解锁。一个互斥锁对象必须通过其API初始化,一个任务在持有互斥锁的时候是不能结束的,互斥锁所使用的内存区域是不能被释放的,使用中的互斥锁是不能被重新初始化的,并且互斥锁不能用于中断上下文。互斥锁比内核信号量更快,并且更加紧凑。
/* 初始化 */
DEFINE_MUTEX(mutexname)
mutex_init(mutex)
/* 上锁 */
//无法获得锁时,睡眠等待,不会被信号中断
void mutex_lock(struct mutex *lock);
//和mutex_lock()一样,也是获取互斥锁。在获得了互斥锁或进入睡眠直到获得互斥锁之后会返回0。如果在等待获取锁的时候进入睡眠状态收到一个信号(被信号打断睡眠),则返回_EINIR
int mutex_lock_interruptible(struct mutex *lock);
//可被kill信号打断
int mutex_lock_killable(struct mutex *lock);
//此函数是 mutex_lock()的非阻塞版本,成功返回1,失败返回0
int mutex_trylock(struct mutex *lock);
/* 解锁 */
void mutex_unlock(struct mutex *lock);
int atomic_dec_and_mutex_lock(atomic_t *cnt, struct mutex *lock);
示例
struct mutex mymutex;
mutex_init(&mymutex);
mutex_lock(&mymutex);
...
mutex_unlock(&mymutex);
8.完成量
/* 初始化 */
//定义并初始化
DECLARE_COMPLETION(work)
//初始化
void init_completion(struct completion *x)
INIT_COMPLETION(x)
/* 等待完成量 */
void wait_for_completion(struct completion *);
void wait_for_completion_io(struct completion *);
//可被信号打断
int wait_for_completion_interruptible(struct completion *x);
//可被kill信号打断
int wait_for_completion_killable(struct completion *x);
unsigned long wait_for_completion_timeout(struct completion *x,unsigned long timeout);
unsigned long wait_for_completion_io_timeout(struct completion *x,unsigned long timeout);
long wait_for_completion_interruptible_timeout(struct completion *x, unsigned long timeout);
long wait_for_completion_killable_timeout(struct completion *x, unsigned long timeout);
bool try_wait_for_completion(struct completion *x);
//检查completion是否可用
bool completion_done(struct completion *x);
/* 唤醒完成量 */
void complete(struct completion *);
void complete_all(struct completion *);
示例
struct completion comp;
init_completion(&comp);
wait_for_completion(&comp);
...
complete(&comp);
9.RCU锁
RCU(Read-Copy Update)是数据同步的一种方式,在当前的Linux内核中发挥着重要的作用,可以看做时读写锁的高性能版本。RCU主要针对的数据对象是链表,目的是提高遍历读取数据的效率,为了达到目的使用RCU机制读取数据的时候不对链表进行耗时的加锁操作。这样在同一时间可以有多个线程同时读取该链表,并且允许一个线程对链表进行修改(修改的时候,需要加锁)。RCU适用于需要频繁的读取数据,而相应修改数据并不多的情景,例如在文件系统中,经常需要查找定位目录,而对目录的修改相对来说并不多,这就是RCU发挥作用的最佳场景。
(1)RCU只能保护动态分配的数据结构,并且必须是通过指针访问该数据结构
(2)受RCU保护的临界区内不能sleep(SRCU不是本文的内容)
(3)读写不对称,对writer的性能没有特别要求,但是reader性能要求极高。
(4)reader端对新旧数据不敏感。
/* 读锁定 */
void rcu_read_lock(void)
void rcu_read_lock_bh(void)
/* 读解锁 */
void rcu_read_unlock(void)
void rcu_read_unlock_bh(void)
/* 同步RCU*/
//阻塞写者直到所有读者执行完毕,实质通过完成量实现同步
void synchronize_rcu(void)
//用于等待所有CPU都处在可抢占状态,它能保证正在运行的中断处理函数处理完毕,但不能保证正在运行的softirq处理完毕
void synchronize_sched(void)
/* 挂接回调 */
//由RCU写执行单元调用,不会造成写执行单元阻塞,可以在中断上下文和软中断中使用
void call_rcu(struct rcu_head *head,void (*func)(struct rcu_head *head));
//可在中断上下文使用,而中断上下文能打断软中断的运行,故当call_rcu_bh在中断上下文中使用的时候,需确保软中断的能够顺利执行完毕
void call_rcu_bh(struct rcu_head *head,void (*func)(struct rcu_head *head));
//为RCU保护指针赋一个新值
rcu_assign_pointer(p, v)
//获取一个RCU保护指针
rcu_dereference(p)
/* 循环链表 */
//该函数把链表项new插入到RCU保护的链表head的开头。使用内存栅保证了在引用这个新插入的链表项之前,新链表项的链接指针的修改对所有读者是可见的。
void list_add_rcu(struct list_head *new, struct list_head *head)
//该函数类似于list_add_rcu,它将把新的链表项new添加到被RCU保护的链表的末尾。
void list_add_tail_rcu(struct list_head *new,
struct list_head *head)
//该函数从RCU保护的链表中移走指定的链表项entry,并且把entry的prev指针设置为LIST_POISON2,但是并没有把entry的next指针设置为LIST_POISON1,因为该指针可能仍然在被读者用于便利该链表。
void list_del_rcu(struct list_head *entry)
//该函数是RCU新添加的函数,并不存在非RCU版本。它使用新的链表项new取代旧的链表项old,内存栅保证在引用新的链表项之前,它的链接指针的修正对所有读者可见。
void list_replace_rcu(struct list_head *old, struct list_head *new)
void hlist_del_init_rcu(struct hlist_node *n)
void list_splice_init_rcu(struct list_head *list,struct list_head *head,void (*sync)(void))
list_entry_rcu(ptr, type, member)
list_first_or_null_rcu(ptr, type, member)
list_for_each_entry_rcu(pos, head, member)
list_for_each_entry_continue_rcu(pos, head, member)
//该宏用于遍历由RCU保护的链表head,只要在读端临界区使用该函数,它就可以安全地和其它_rcu链表操作函数(如list_add_rcu)并发运行。
list_for_each_rcu(pos, head)
//该宏类似于list_for_each_rcu,但不同之处在于它允许安全地删除当前链表项pos。
list_for_each_safe_rcu(pos, n, head)
//该宏类似于list_for_each_rcu,不同之处在于它用于遍历指定类型的数据结构链表,当前链表项pos为一包含struct list_head结构的特定的数据结构。
list_for_each_entry_rcu(pos, head, member)
//该宏用于在退出点之后继续遍历由RCU保护的链表head。
list_for_each_continue_rcu(pos, head)
/* 哈希链表 */
//它从由RCU保护的哈希链表中移走链表项n,并设置n的ppre指针为LIST_POISON2,但并没有设置next为LIST_POISON1,因为该指针可能被读者使用用于遍利链表。
void hlist_del_rcu(struct hlist_node *n)
//该函数用于把链表项n插入到被RCU保护的哈希链表的开头,但同时允许读者对该哈希链表的遍历。内存栅确保在引用新链表项之前,它的指针修正对所有读者可见。
void hlist_add_head_rcu(struct hlist_node *n,struct hlist_head *h)
void hlist_add_before_rcu(struct hlist_node *n,struct hlist_node *next)
void hlist_add_after_rcu(struct hlist_node *prev,struct hlist_node *n)
void hlist_replace_rcu(struct hlist_node *old,struct hlist_node *new)
//该宏用于遍历由RCU保护的哈希链表head,只要在读端临界区使用该函数,它就可以安全地和其它_rcu哈希链表操作函数(如hlist_add_rcu)并发运行。
__hlist_for_each_rcu(pos, head)
//类似于hlist_for_each_rcu,不同之处在于它用于遍历指定类型的数据结构哈希链表,当前链表项pos为一包含struct list_head结构的特定的数据结构。
hlist_for_each_entry_rcu(tpos, pos, head, member)
hlist_for_each_entry_rcu_notrace(pos, head, member)
hlist_for_each_entry_rcu_bh(pos, head, member)
hlist_for_each_entry_continue_rcu(pos, member)
hlist_for_each_entry_continue_rcu_bh(pos, member)
hlist_first_rcu(head)
hlist_next_rcu(node)
hlist_pprev_rcu(node)
示例
struct foo {
int a;
char b;
long c;
};
DEFINE_SPINLOCK(foo_mutex);
struct foo *gbl_foo;
void foo_update_a(int new_a)
{
struct foo *new_fp;
struct foo *old_fp;
new_fp = kmalloc(sizeof(*new_fp), GFP_KERNEL);
spin_lock(&foo_mutex);
old_fp = gbl_foo;
*new_fp = *old_fp;
new_fp->a = new_a;
rcu_assign_pointer(gbl_foo, new_fp);
spin_unlock(&foo_mutex);
synchronize_rcu();
kfree(old_fp);
}
int foo_get_a(void)
{
int retval;
rcu_read_lock();
retval = rcu_dereference(gbl_foo)->a;
rcu_read_unlock();
return retval;
}
struct el {
struct list_head list;
long key;
spinlock_t mutex;
int data;
/* Other data fields */
};
spinlock_t listmutex;
struct el head;
int search(long key, int *result)
{
struct list_head *lp;
struct el *p;
rcu_read_lock();
list_for_each_entry_rcu(p, head, lp)
{
if (p->key == key)
{
*result = p->data;
rcu_read_unlock();
return 1;
}
}
rcu_read_unlock();
return 0;
}
int delete(long key)
{
struct el *p;
spin_lock(&listmutex);
list_for_each_entry(p, head, lp)
{
if (p->key == key)
{
list_del_rcu(&p->list) or list_add_rcu(&p->list);
spin_unlock(&listmutex);
synchronize_rcu();
kfree(p);
return 1;
}
}
spin_unlock(&listmutex);
return 0;
}
[1] https://lwn.net/Articles/262464/
[2] https://www.kernel.org/doc/Documentation/RCU/checklist.txt
[3] https://www.kernel.org/doc/Documentation/RCU/whatisRCU.txt
[4] https://www.kernel.org/doc/Documentation/memory-barriers.txt
10.顺序和屏障
防止编译器优化我们的代码,让我们代码的执行顺序与我们所写的不同,就需要顺序和屏障。
方法 | 描述 |
---|---|
rmb | 阻止跨越屏障的载入动作发生重排序 |
read_barrier_depends() | 阻止跨越屏障的具有数据依赖关系的载入动作重排序 |
wmb() | 阻止跨越屏障的存储动作发生重排序 |
mb() | 阻止跨越屏障的载入和存储动作重新排序 |
smp_rmb() | 在SMP上提供rmb()功能,在UP上提供barrier()功能 |
smp_read_barrier_depends() | 在SMP上提供read_barrier_depends()功能,在UP上提供barrier()功能 |
smp_wmb() | 在SMP上提供wmb()功能,在UP上提供barrier()功能 |
smp_mb | 在SMP上提供mb()功能,在UP上提供barrier()功能 |
barrier | 阻止编译器跨越屏障对载入或存储操作进行优化 |
11.禁止抢占
自旋锁同时关闭中断和抢占,但有时后只需要关闭抢占
方法 | 描述 |
---|---|
preempt_disable() | 增加抢占计数值,从而禁止内核抢占 |
preempt_enable() | 减少抢占计算,并当该值将为0时检查和执行被挂起的需要调度的任务 |
preempt_enable_no_resched() | 激活内核抢占但不再检查任何被挂起的需调度的任务 |
preempt_count() | 返回抢占计数 |