Linux设备驱动基础01之并发与竞态

Posted PanGC2014

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux设备驱动基础01之并发与竞态相关的知识,希望对你有一定的参考价值。

一、基础概念

Linux是个多任务操作系统,存在多个任务同时访问同一片内存区域的情况,可能会相互覆盖这段内存中的数据,最终造成内存数据混乱,严重的话会导致系统崩溃。驱动开发中要注意对共享资源的保护,需要管理对共享资源的并发访问。Linux系统产生并发访问的几个主要原因:

(1)、多线程并发访问, Linux 是多任务系统,在应用程序中多线程访问是最基本的原因。

(2)、抢占式并发访问,从2.6版本开始, Linux内核支持抢占,也就是说调度程序可以在任意时刻调度正在运行的线程,从而运行其他的线程。

(3)、中断程序并发访问,硬件中断的权重很大,可以中断正在运行的线程并对共享资源进行访问。

(4)、SMP(多核)核间并发访问,现在ARM架构的多核CPU很常见,存在核间并发访问。 并发访问带来的问题就是竞争,所谓的临界区就是共享数据段,对于临界区必须保证一次只有一个线程访问,即对临界区的访问是原子式的,不可再拆分。

二、原子操作

1、基本概念

原子操作就是指不能再进一步分割的操作,一般原子操作用于变量或者位操作。Linux内核提供两组原子操作API:一组是对整形变量进行操作,另一组是对位进行操作。

2、原子整型操作

(1)、结构体atomic_t:在32位SoC中,该结构体被用来表示原子整型变量,用来代替整型变量,定义如下:

typedef struct 
    int counter;
 atomic_t;

(2)、结构体atomic64_t:在64位SoC中,就要用到64位的原子变量,定义如下:

typedef struct 
    long long counter;
 atomic64_t;

(3)原子整型操作相关API(32位SoC):

ATOMIC_INIT(int i); 						//定义原子变量时对其初始化
int atomic_read(atomic_t *v); 				//读取v的值,并且返回
void atomic_set(atomic_t *v, int i); 		//向v写入i值
void atomic_add(int i, atomic_t *v); 		//给v加上i值
void atomic_sub(int i, atomic_t *v); 		//从v减去i值
void atomic_inc(atomic_t *v); 				//给v加 1,也就是自增
void atomic_dec(atomic_t *v); 				//从v减 1,也就是自减
int atomic_dec_return(atomic_t *v); 		//从v减1,并且返回v的值
int atomic_inc_return(atomic_t *v); 		//给v加1,并且返回v的值
int atomic_sub_and_test(int i, atomic_t *v);//从v减i,如果结果为0就返回真,否则返回假
int atomic_dec_and_test(atomic_t *v); 		//从v减1,如果结果为0就返回真,否则返回假
int atomic_inc_and_test(atomic_t *v); 		//给v加1,如果结果为0就返回真,否则返回假
int atomic_add_negative(int i, atomic_t *v);//给v加i,如果结果为负就返回真,否则返回假

注意:在64位SoC中,相关操作API,只是将“atomic”前缀换为“atomic64_”,将int换为long long。

(4)原子整型操作示例

void function(void)

    atomic_t v = ATOMIC_INIT(0); /* 定义并初始化原子变零v=0 */
    atomic_set(10); /* 设置v=10 */
    atomic_read(&v); /* 读取v的值,肯定是10 */
    atomic_inc(&v); /* v的值加1,v=11 */

3、原子位操作

原子位操作不像原子整型变量那样有个atomic_t的数据结构,原子位操作是直接对内存进行操作,相关API如下:

void set_bit(int nr, void *p); 			    //将p地址的第nr位置1
void clear_bit(int nr,void *p); 			//将p地址的第nr位清零
void change_bit(int nr, void *p); 		    //将p地址的第nr位进行翻转
int test_bit(int nr, void *p); 				//获取p地址的第nr位的值
int test_and_set_bit(int nr, void *p); 		//将p地址的第nr位置1,并且返回nr位原来的值
int test_and_clear_bit(int nr, void *p); 	//将p地址的第nr位清零,并且返回nr位原来的值
int test_and_change_bit(int nr, void *p);   //将p地址的第nr位翻转,并且返回nr位原来的值

三、自旋锁

1、基本概念

当一个线程要访问某个共享资源的时候首先要先获取相应的锁,锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取该锁。对于自旋锁而言,如果自旋锁正在被线程A持有,线程B想要获取自旋锁,那么线程B就会处于“忙循环-旋转-等待“状态,线程B不会进入休眠状态,而是会在本地“转圈圈”,一直等待锁可用。

可以看到自旋锁的一个缺点:等待自旋锁的线程会一直处于自旋状态,这样会浪费CPU时间,降低系统性能。所以自旋锁的持有时间不能太长,适用于短时期的轻量级加锁,如果遇到需要长时间持有锁的场景那就需要换其他的方法。

2、自旋锁操作

(1)、结构体spinlock_t:Linux内核中使用该结构体表示自旋锁,定义如下:

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;

(2)、自旋锁相关API

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);		//尝试获取指定的自旋锁,如果没有获取到就返回0
int spin_is_locked(spinlock_t *lock);	//检查指定的自旋锁是否被获取,如果没有被获取就返回非0,否则返回0

上面的API适用于SMP或支持抢占的单CPU下线程之间的并发访问,即用于线程与线程之间,自旋锁会自动禁止抢占;也就说当线程A得到锁后会暂时禁止内核抢占。在持有自旋锁期间,线程不能进入睡眠状态,否则会造成死锁。

中断里面可以使用自旋锁,但是在中断内获取锁之前需要先禁止本地中断,即本CPU中断,对于多核SoC会有多个CPU,否则可能会导致死锁现象。最好的解决办法是获取锁前关闭本地中断,使用如下API:

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_irq/spin_unlock_irq时需要用户能够确定加锁之前的中断状态,但实际上内核过于庞大,运行也是“千变万化”,用户是很难确定某个时刻的中断状态,因此不推荐使用spin_lock_irq/spin_unlock_irq。建议使用spin_lock_irqsave/spin_unlock_irqrestore,因为这组函数会保存中断状态,并在释放锁时恢复中断状态。

注意:一般在线程中使用spin_lock_irqsave/spin_unlock_irqrestore,在中断中使用spin_lock/spin_unlock,这样做的目的是为了防止线程和中断对共享资源访问时出现死锁。

(3)、中断下半部 在中断下半部中也会访问共享组员,在下半部中使用自旋锁,需要如下API:

void spin_lock_bh(spinlock_t *lock); 	//关闭下半部,并获取自旋锁
void spin_unlock_bh(spinlock_t *lock); 	//打开下半部,并释放自旋锁

3、自旋锁使用示例

DEFINE_SPINLOCK(lock) /* 定义并初始化一个锁 */

/* 线程处理函数 */
void function()

    unsigned long flags; /* 中断状态 */
    spin_lock_irqsave(&lock, flags) /* 获取锁 */
    /* 临界区 */
    spin_unlock_irqrestore(&lock, flags) /* 释放锁 */


/* 中断服务函数 */
void irq_handler() 

    spin_lock(&lock) /* 获取锁 */
    /* 临界区 */
    spin_unlock(&lock) /* 释放锁 */

4、自旋锁总结

(1)、自旋锁持有时间要短,否则会降低系统性能。如果临界区较大,运行时间较长,要选择信号量或互斥体。

(2)、自旋锁保护的临界区中不能调用任何导致线程睡眠的函数,否则会造成死锁。

(3)、不能递归调用自旋锁,会造成自己把自己锁死。

四、信号量

1、基本概念

信号量常用于控制对共享资源的访问,可以使线程进入休眠状态。比如A正在临界区,B想要进入临界区,但是此时不能访问,于是告诉A,让A出来后通知他一下,然后B继续回房间睡觉,这就是信号量。使用信号量会提高处理器的使用效率,但是会增大系统开销,因为信号量使线程进入休眠后会切换线程,切换线程就会有开销。信号量的特点如下:

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

(2)、中断中不能使用信号量,因为信号量会引起休眠,中断不能休眠。

信号量有一个信号量值,相当于一个房子有10把钥匙,这10把钥匙就相当于信号量值为10。因此,可通过信号量来控制访问共享资源的访问数量,如果要想进房间,那就要先获取一把钥匙,信号量值减1,直到10把钥匙都被拿走,信号量值为0,这个时候就不允许任何人进入房间。

初始化信号量时将其值设置大于1,那么这个信号量就是计数型信号量,计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源。如果要互斥的访问共享资源,那么信号量的值就不能大于1,此时的信号量就是一个二值信号量。

2、信号量操作

(1)、结构体semaphore:Linux内核中用改结构体表示信号量,定义如下:

struct semaphore 
    raw_spinlock_t lock;
    unsigned int count;
    struct list_head wait_list;
;

(2)、信号量操作API

DEFINE_SEAMPHORE(name); //定义一个信号量,并且设置信号量的值为 1
void sema_init(struct semaphore *sem, int val); //初始化信号量sem,设置信号量值为val
void down(struct semaphore *sem); //获取信号量,因为会导致休眠,因此不能在中断中使用
int down_trylock(struct semaphore *sem); //尝试获取信号量,如果能获取到信号量就获取,并且返回0;如果不能就返回非0,并且不会进入休眠
int down_interruptible(struct semaphore *sem); //获取信号量,和down类似,只是使用down进入休眠状态的线程不能被信号打断。而使用此函数进入休眠以后是可以被信号打断的
void up(struct semaphore *sem); //释放信号量

3、信号量使用示例

void function(void)

    struct semaphore sem; /* 定义信号量 */
    sema_init(&sem, 1); /* 初始化信号量 */
    down(&sem); /* 申请信号量 */
    /* 临界区 */
    up(&sem); /* 释放信号量 */

五、互斥体

1、基本概念

将信号量的值设置为1就可以使用信号量进行互斥访问,但是Linux提供一个比信号量更专业的机制来进行互斥,即互斥体mutex。互斥访问表示一次只有一个线程可以访问临界区,不能递归申请互斥体。Linux驱动中需要互斥访问的地方建议使用mutex,使用mutex时要注意如下几点:

(1)、中断中不能使用mutex,因为mutex可以导致休眠,中断中只能使用自旋锁。

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

(3)、必须由mutex的持有者释放mutex,并且mutex不能递归上锁和解锁。

2、互斥体操作

(1)、结构体mutex:Linux 中使用该结构体表示互斥体,定义如下:

struct mutex 
    /* 1: unlocked, 0: locked, negative: locked, possible waiters */
    atomic_t count;
    spinlock_t wait_lock;
;

(2)、互斥体操作API

DEFINE_MUTEX(name); 					//定义并初始化一个mutex变量
void mutex_init(mutex *lock); 			//初始化mutex
void mutex_lock(struct mutex *lock); 	//获取mutex,也就是给mutex上锁。如果获取不到就进休眠
void mutex_unlock(struct mutex *lock); 	//释放mutex,也就给mutex解锁
int mutex_trylock(struct mutex *lock); 	//尝试获取mutex,如果成功就返回1,如果失败就返回0
int mutex_is_locked(struct mutex *lock); //判断mutex是否被获取,如果是的话就返回1,否则返回0
int mutex_lock_interruptible(struct mutex *lock); //使用此函数获取信号量失败进入休眠以后可以被信号打断

3、互斥体使用示例

void function(void)

    struct mutex lock; /* 定义一个互斥体 */
    mutex_init(&lock); /* 初始化互斥体 */
    mutex_lock(&lock); /* 上锁 */
    /* 临界区 */
    mutex_unlock(&lock); /* 解锁 */

 

 

以上是关于Linux设备驱动基础01之并发与竞态的主要内容,如果未能解决你的问题,请参考以下文章

Linux设备驱动基础01之并发与竞态

:并发与竞态

并发与竞态(原子操作)

linux并行与竞态

linux设备驱动归纳总结:5.SMP下的竞态和并发

linux设备驱动归纳总结:5.SMP下的竞态和并发