Linux内核并发控制

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() 返回抢占计数

猜你喜欢

转载自blog.csdn.net/wyy626562203/article/details/81334082