Linux内核的并发与竞争管理

一、背景介绍

为了解决多任务或CPU并发访问同一个内存资源的问题,Linux内核提供了一系列共享资源管理方法。

/

二、原子操作

1、基本概念:

        就是CPU在执行此程序语句时,不能再拆分的语句;如果可以把非原子操作的程序捆绑为原子操作,那么程序将不会被其他进程打断,也就避免了内存资源的混乱。

/

2、Linux提供的原子操作方法:

① 结构体atomic_t定义一个原子整数:

#include <linux/types.h>

typedef struct {

     int counter;

}atomic_t;

② 使用宏ATOMIC_INIT(value)初始化原子变量:

#include <asm/atomic.h>

#define ATOMIC_INIT(i)  { (i) }

/

3、Linux提供的原子整数操作函数

#include <asm/atomic.h>

① #define atomic_read(v)     ACCESS_ONCE((v)->counter)  //读取原子整数的值并返回

② #define atomic_set(v,i)     ((v)->counter = (i))                      //给原子整数变量赋值

③ #define atomic_add(i.v)    (void)atomic_add_return(i,v)       //给原子整数值加i

④ #define atomic_sub(i,v)    (void)atomic_sub_return(i,v)       //将原子整数的值减i

⑤ #define atomic_dec(v)      atomic_sub(1,v)                           //原子整数减1

⑥ #define atomic_inc(v)       atomic_add(1,v)                          //原子整数加1

⑦ #define atomic_dec_return(v) atomic_sub_return(1,v)        //原子整数减1,并返回当前的值

⑧ #define atomic_inc_return(v)  atomic_add_return(1,v)        //原子整数加1,并返回当前的值

⑨ #define atomic_sub_and_test(i,v)  (atomic_sub_return((i),(v)) ==0)   //原子整数减i,如果结果为0返回真,否则返回假

⑩ #define atomic_dec_and_test(v)  (atomic_sub_return(1,(v)) == 0)     //原子整数减1,如果结果为0返回真,否则返回假

11 #define atomic_inc_and_test(v)   (atomic_add_return(1,(v)) == 0)     //原子整数加1,如果结果为0返回真,否则返回假

12 #define atomic_add_negative(a,v)  (atomic_add_return((a),(v))<0)      //原子整数加a,如果结果为负值返回真

/

4、Linux提供的原子位操作函数(原子位操作是直接对内存操作)

#include <asm/bitops.h>

①  void set_bit(unsigned long nr, volatile void *addr);     //将addr地址的nr位置1

② void clear_bit(unsigned long nr,volatile void *addr);   //将addr地址的nr位清0

③ int test_bit(int nr, const volatile void *addr);              //返回addr地址的第nr位的值

④ int test_and_set_bit(unsigned long nr, volatile void *addr);  //返回addr地址第nr位的值;并置nr位为1

⑤ int test_and_clear_bit(unsigned long nr, volatile void *addr);  //返回addr地址第nr位的值;并清0位nr

⑥ int test_and_change_bit(unsigned long nr,volatile void *addr);  //返回addr地址第nr位的值;并对nr位进行翻转

 /

三、自旋锁

1、基本概念:

         对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线

程 B 不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里“转圈圈”的等待锁可用;类似于应用层的互斥锁。

         Linux内核使用结构体spinlock_t来描述一个自旋锁,原型如下:#include <linux/spinlock_types.h>

typedef struct spinlock {

     union {

         struct raw_spinlock rlock;   //最核心的锁!!

#ifdef CONFIG_DEBUG_LOCK_ALLOC

# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))

         struct {

             u8 __padding[LOCK_PADSIZE];

             struct lockdep_map dep_map;

         };

#endif

     };

} spinlock_t;

在使用自旋锁之前,需要先用结构体申明一个出来。

        自旋锁API适用于单CPU线程之间的资源抢占;且被自旋锁保护的临界区一定不能调用任何可能会引起睡眠或阻塞的API函数,否则的话就有可能会导致死锁的发生;自旋锁会自动禁止抢占,也就是说线程A获得自旋锁后,内核就会被线程A独占了;如果线程A在获得锁之后又进入了休眠状态,意味着线程A放弃了CPU的使用权,这时候如果线程B要获得锁来访问临界区资源,由于锁被线程A占用了,而线程A又休眠了,内核被锁禁止抢占,线程B无法退出,死锁发生!

        线程和中断访问同一个共享资源的情况。Linux内核是允许中断函数使用自旋锁的,但需要注意防止死锁的发生;为了防止普通线程在获得自旋锁后,中断插一脚也要访问临界资源导致死锁发生的情况,Linux内核提供了关闭本地中断的解决方案(具体使用方法在后面介绍)。

//

2、基本的自旋锁API函数接口

#include <linux/spinlock.h>

① #define DEFINE_SPINLOCK(spinlock_t lock)   //定义并初始化一个自旋锁

② int spin_lock_init(spinlock_t *lock);                       //初始化自旋锁,将自旋锁结构体加入内核有关的链表中

③ void spin_lock(spinlock_t *lock);                           //阻塞获取目标自旋锁,也就是加锁

④ void spin_unlock(spinlock_t *lock);                       //释放指定的自旋锁

⑤ int spin_trylock(spinlock_t *lock);                         //不阻塞尝试获取目标自旋锁,成功返回1,失败返回0

⑥ int spin_is_locked(spinlock_t *lock);                     //检查目标自旋锁是否被使用,未被使用则返回非0

//

3、禁止本地中断的自旋锁API函数接口

#include <linux/spinlock.h>

① void spin_lock_irq(spinlock_t *lock);                                                   //禁止本地中断,并阻塞获取目标自旋锁

② void spin_unlock_irq(spinlock_t *lock);                                               //禁止本地中断,并释放目标自旋锁

③ void spin_lock_irqsave(spinlock_t *lock,unsigned long flags);             //保持中断状态,并禁止本地中断,阻塞获取目标自旋锁

④ void spin_unlock_irqrestore(spinlock_t *lock,unsigned long flags);     //解锁目标自旋锁,并恢复本地中断状态

具体使用注意事项:

         一般在线程中,我们使用spin_lock_irqsave/spin_unlock_irqrestore来使用自旋锁;因为可以自动保存中断的状态。在中断中,我们不必去保存和恢复本地中断,所以可以使用spin_lock_irq/spin_unlock_irq。

//

4、中断底半部使用的自旋锁API函数接口

#include <linux/spinlock.h>

① void spin_lock_bh(spinlock_t *lock);                                                 //关闭下半部,并获取自旋锁

② void spin_unlock_bh(spinlock_t *lock);                                             //启动下半部,并释放自旋锁

///

读写自旋锁(自旋锁的衍生)

1、功能介绍

        读写自旋锁为读和写操作提供了不同的锁。一次只能允许一个写数据操作,且写锁一但被任意线程锁定,其他线程不可读写数据;但当没有线程锁定写锁的时候,则允许多个线程同时读数据。

///

2、Linux内核的读写锁结构体

#include <linux/rwlock_types.h>

结构体原型:

typedef struct {

     arch_rwlock_t raw_lock;            //这个是读写锁的核心,其实是一个u32

#ifdef CONFIG_GENERIC_LOCKBREAK

     unsigned int break_lock;

#endif

#ifdef CONFIG_DEBUG_SPINLOCK

     unsigned int magic, owner_cpu;         

     void *owner;

#endif

#ifdef CONFIG_DEBUG_LOCK_ALLOC

     struct lockdep_map dep_map;

#endif

} rwlock_t;

3、Linux提供的读写锁API函数接口:

#include <linux/rwlock.h>

公共接口:

① DEFINE_RWLOCK(rwlock_t lock)           //宏函数定义并初始化一个读写锁,其实是=0

② void rwlock_init(rwlock_t *lock);                 //初始化目标读写自旋锁,其实是个宏定义函数

读锁相关:

① void read_lock(rwlock_t *lock);                   //获取读锁,如果有线程获取了写锁,则会阻塞等待释放

② void read_unlock(rwlock_t *lock);               //释放读锁

③ void read_lock_irq(rwlock_t *lock);             //关闭本地中断,并获取读锁

④ void read_unlock_irq(rwlock_t *lock);         //释放读锁,然后启动本地中断

⑤ void read_lock_irqsave(rwlock_t *lock,unsigned long flags);  //保存本地中断状态后禁止本地中断,并获取读锁

⑥ void read_unlock_irqrestore(rwlock_t *lock,unsigned long flags);  //先释放读锁,然后启动本地中断后恢复本地中断状态

⑦ void read_lock_bh(rwlock_t *lock);             //关闭底半部,并获取读锁

⑧ void read_unlock_bh(rwlock_t *lock);         //释放读锁,然后启动底半部

写锁相关:

① void write_lock(rwlock_t *lock);                  //阻塞获取写锁

② void write_unlock(rwlock_t *lock);              //释放写锁

③ void write_lock_irq(rwlock_t *lock);           //禁止本地中断,并获取写锁

④ void write_unlock_irq(rwlock_t *lock);       //释放写锁,启动本地中断

⑤ void write_lock_irqsave(rwlock_t *lock,unsigned long flags);  //先保存本地中断状态并禁止,然后获取写锁

⑥ void write_unlock_irqrestore(rwlock_t *lock,unsigned long flags);  //释放写锁,然后启动本地中断后恢复本地中断状态

⑦ void write_lock_bh(rwlock_t *lock);           //关闭底本部,并获取写锁

⑧ void write_unlock_bh(rwlock_t *lock);       //释放写锁,并启动底半部

顺序锁(读写旋转锁的衍生)

1、功能介绍:

       顺序锁是有前面读写锁进一步衍生来的。读写锁在写数据时,不允许读;而顺序锁的读和写操作是允许同时进行的,也就是说即使有线程获取的写锁,其他线程仍然可以获取读锁并读取数据;但这样需要特别小心,在读数据期间,如果有写操作发生,则最好停止读,在写操作完成后再重新读取。

///

2、Linux内核的顺序锁描述结构体:

#include <linux/seqlock.h>

结构体原型

typedef struct {

     struct seqcount seqcount;           //主要提供了一个u32变量

     spinlock_t lock;                          //内部包含了一个标准的自旋锁

}seqlock_t;

///

3、Linux提供的顺序锁使用API函数:

#include <linux/seqlock.h>

公共函数:

① #define DEFINE_SEQLOCK(seqlock_t sl)        //宏函数申明并初始化一个顺序锁,就是将u32=0且初始化自旋锁

② void seqlock_init(seqlock_t *sl);                            //初始化顺序锁,也初始化了里面的自旋锁

写操作相关:

① void write_seqlock(seqlock_t *sl);                        //阻塞获取顺序锁的写锁,这里也阻塞获取里面的自旋锁

② void write_sequnlock(seqlock_t *sl);                    //释放顺序锁,也释放里面的自旋锁

③ void write_seqlock_irq(seqlock_t *sl);                  //禁止本地中断,阻塞获取顺序锁的写锁;其实调用了自旋锁的spin_lock_irq

④ void write_sequnlock_irq(seqlock_t *sl);              //释放顺序锁的写锁,然后启动本地中断;其实调用了自旋锁的spin_unlock_irq

⑤ void write_seqlock_irqsave(seqlock_t *sl,unsigned long flags);     //保存本地中断的状态并禁止本地中断,然后阻塞获取写锁

⑥ void write_sequnlock_irqrestore(seqlock_t *sl,unsigned long flags);   //释放写锁,然后启动本地中断后恢复中断状态

⑦ void write_seqlock_bh(seqlock_t *sl);                  //关闭底半部,阻塞获取写锁

⑧ void write_sequnlock_bh(seqlock_t *sl);              //释放写锁,启动底半部

读操作相关:

① unsigned read_seqbegin(const seqlock_t *sl);                //读取共享单元前,调用此函数;此函数返回顺序锁的顺序号(可以看出被多少线程使用过)

② unsigned read_seqretry(const seqlock_t *sl,unsigned start);   //用于在读共享资源结束后,判断是否读期间有线程写了资源 1=有 0=无

参数解释:start = 读共享资源前,获取到的顺序号!!

/

四、信号量

1、功能介绍:

        信号量有一个信号量初始值,假设这个值为10;因此,可以通过信号量来控制访问共享资源的访问数量。当有一个线程使用到共享资源时,这个信号量值就会减1;一直到这个值为0时,这个时候不允许任何线程去访问资源了。相当于信号量控制的是,同时访问共享资源的线程数。

 信号量有以下几点特性:

① 因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。

② 信号量不能用于中断中,因为信号量会引起休眠发生。

③ 如果共享资源持有的时间比较短,那就不适合使用信号量了,因为频繁的休眠和线程切换所引起的资源开销远大于好处。

///

2、Linux内核中用于描述信号量的结构体:

#include <linux/semaphore.h>

struct semaphore {

     raw_spinlock_t      lock;          //这里旋转锁的作用是:防止读写count是,其他线程也操作count

     unsigned int        count;          //这个值就是该信号量的初始值

     struct list_head    wait_list;     //其实是链表的指针(双向链表),应该是内核用于管理信号量用的

};

///

3、Linux内核提供的操作使用信号量的API函数接口:

#include <linux/semaphore.h>

① #define DEFINE_SEMAPHORE(name)               //初始化一个信号量,信号个数为1,并返回结构体对象

② void sema_init(struct semaphore *sem, int val);       //初始化配置目标信号量,并设置信号的信号个数

③ void down(struct semaaphore *sem);                       //获取信号量,如果获取不到,线程将休眠

④ int down_trylock(struct semaphore *sem);              //尝试获取信号量,如果获取成功则返回0,获取失败则返回1;不会休眠

⑤ int down_interruptible(struct semaphore *sem);     //尝试获取信号量,如果成功返回0;失败则进入休眠,但可以被信号打断返回负数

⑥ void up(struct semaphore *sem);                            //释放信号量

///

 /

五、互斥体

1、功能介绍:

        互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体。在我们编写 Linux 驱动的时候遇到需要互斥访问的地方建议使用 mutex。

        使用mutex注意事项:

① mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。

② 和信号量一样,mutex 保护的临界区可以调用引起阻塞的 API 函数。

③ 因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁。

//

2、Linux内核中描述mutex的结构体:

#include <linux/mutex.h>

struct mutex {

     /* 1: unlocked, 0: locked, negative: locked, possible waiters */

     atomic_t        count;           //核心变量

     spinlock_t      wait_lock;

     struct list_head    wait_list;

 #if defined(CONFIG_DEBUG_MUTEXES) || defined(CONFIG_MUTEX_SPIN_ON_OWNER)

     struct task_struct  *owner;

 #endif

 #ifdef CONFIG_MUTEX_SPIN_ON_OWNER

    struct optimistic_spin_queue osq; /* Spinner MCS lock */

 #endif

 #ifdef CONFIG_DEBUG_MUTEXES

     void            *magic;

 #endif

 #ifdef CONFIG_DEBUG_LOCK_ALLOC

     struct lockdep_map  dep_map;

#endif

};

//

3、Linux内核提供的操作mutex的API函数:

#include <linux/mutex.h>

① #define DEFINE_MUTEX(mutexname)     //宏定义函数,定义并初始化一个mutex

② void mutex_init(struct mutex *lock);             //初始化互斥锁

③ void mutex_lock(struct mutex *lock);           //获取互斥锁,如果失败则线程休眠

④ void mutex_unlock(struct mutex *lock);       //释放互斥锁

⑤ int mutex_trylock(struct mutex *lock);         //尝试获取互斥锁,成功返回1;失败返回0

⑥ int mutex_is_locked(struct mutex *lock)      //判断互斥锁使用情况,如果已经被使用,则返回1

⑦ int mutex_lock_interruptible(struct mutex *lock);     //获取互斥锁,如果成功返回0,如果失败则线程休眠,但可以被中断信号唤醒

上一篇:Java面试题:n=2\n1*2*5*6\n--3*4\n\nn=3\n1*2*3*10*11*12\n--4*5*8*9\n----6*7\n如何实现如上结构的数据


下一篇:String 类中的几个练习--获取指定字符串中,大写字母、小写字母、数字的个数||获取一个字符串中,另一个字符串出现的次数