并发与竞争

原子操作

原子操作只能对整形变量或者位进行保护

原子整形操作

// include/linux/types.h

// 如果使用 32位的 SOC
typedef struct {
	int counter;
} atomic_t;


// 如果使用 64位的 SOC
typedef struct { 
	long long counter; 
} atomic64_t;
// 下面所有介绍都基于32位系统说明

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

基本API

函数 功能
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,如果结果为负就返回真,否则返回假

原子位操作

基本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位原来的值。

原子变量实验

参见代码【07.atomic】工程,该工程在【05.led_pinctrl_gpio】工程上只修改入口参数和open函数

入口函数新增原子变量的初始化

static int __init led_init(void)
{
	/* 
	 * 初始化原子变量 初始值为1
	 */
	atomic_set(&g_led_dev.lock, 1);

    /* 
     * 3:注册自己的 platform_driver 到平台的总线驱动链表里面
     *   注册的过程中会根据匹配规则   到【总线平台设备链表】里面找,
     *   如果匹配成功会调用驱动里面的platform_driver下的probe函数  
     */
	printk("%s %s line %d:insmod !\n", __FILE__, __FUNCTION__, __LINE__);
	return platform_driver_register(&led_driver);
}

open函数新增原子变量来防止重复open的问题

static int led_drv_open (struct inode *node, struct file *file)
{
	/* 
	* 如果dec后  通过判断原子变量的值来检查LED有没有被别的应用使用 
	* 1:lock初始化为1
	* 2:第一次open后,lock=0  atomic_dec_and_test(&g_led_dev.lock) 为真,执行下面的hw_led_init
	* 3:如果第二次open的时候,atomic_dec_and_test(&g_led_dev.lock)为假(lock=-1),  故而执行atomic_inc(&g_led_dev.lock),把lock=0;且直接返回,提示用户Dev busy
	* 4:如果再有应用open的话,和3条一样
	*/
	if (!atomic_dec_and_test(&g_led_dev.lock)) {
		atomic_inc(&g_led_dev.lock);				/* 小于0的话就加1,使其原子变量等于0 */
		printk("%s %s line %d:Dev busy locking\n", __FILE__, __FUNCTION__, __LINE__);
		return -EBUSY;								/* LED被使用,返回忙 */
	}


    hw_led_init(g_led_dev.nd);
	file->private_data = &g_led_dev;
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

	return 0;
}

自旋锁

概念:对于自旋锁而言,如果自旋锁正在被线程 A持有,线程 B想要获取自旋锁,那么线程 B就会处于忙循环-旋转-等待状态,线程B不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里“转圈圈”的等待线程A释放锁。

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

注意☀️:自旋锁加锁后。不要调用引起系统睡眠和阻塞的API的函数,否则会可能导致锁死现象 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;

自旋锁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

和中断有关的自旋锁

函数 功能
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/spin_unlock 应用举例

DEFINE_SPINLOCK(lock) /* 定义并初始化一个锁 */ 
/* 线程A */ 
void functionA ()
{ 
	unsigned long flags; 					/* 中断状态 */ 
	spin_lock_irqsave(&lock, flags) 		/* 获取锁 */ 
	/* 临界区 */ 
	spin_unlock_irqrestore(&lock, flags) 	/* 释放锁 */ 
} 
 /* 中断服务函数 */ 
void irq() 
{ 
	spin_lock(&lock) /* 获取锁 */
	/* 临界区 */ 
	spin_unlock(&lock) /* 释放锁 */ 
}

下半部 (BH)也会竞争共享资源,有些资料也会将下半部叫做底半部。如果要在下半部里面使用自旋锁, API函数:

函数 功能
void spin_lock_bh(spinlock_t *lock) 关闭下半部,并获取自旋锁。
void spin_unlock_bh(spinlock_t *lock) 打开下半部,并释放自旋锁

自旋锁实验

参见代码【08.spinlock】工程,该工程在【07.atomic】工程上只修改入口函数、open和close函数

  1. 入口函数

    static int __init led_init(void)
    {
    	/* 
    	 * 初始化自旋锁
    	 */
    	spin_lock_init(&g_led_dev.lock);
    	g_led_dev.dev_stats = 0;
    
        /* 
         * 3:注册自己的 platform_driver 到平台的总线驱动链表里面
         *   注册的过程中会根据匹配规则   到【总线平台设备链表】里面找,
         *   如果匹配成功会调用驱动里面的platform_driver下的probe函数  
         */
    	printk("%s %s line %d:insmod !\n", __FILE__, __FUNCTION__, __LINE__);
    	return platform_driver_register(&led_driver);
    }
    
  2. open函数

    static int led_drv_open (struct inode *node, struct file *file)
    {
    	unsigned long flags;
    	spin_lock_irqsave(&g_led_dev.lock, flags);	/* 上锁 */
    	if (g_led_dev.dev_stats) {					/* 如果设备被使用了 */
    		printk("%s %s line %d:dev is busy\n", __FILE__, __FUNCTION__, __LINE__);
    		spin_unlock_irqrestore(&g_led_dev.lock, flags);/* 解锁 */
    		return -EBUSY;
    	}
    
    
        hw_led_init(g_led_dev.nd);
    	file->private_data = &g_led_dev;
    
    	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    
    	return 0;
    }
    
  3. close函数

    static int led_drv_close (struct inode *node, struct file *file)
    {
    	unsigned long flags;
    	struct led_char_dev *dev = file->private_data;
    
    	/* 关闭驱动文件的时候将dev_stats减1 */
    	spin_lock_irqsave(&dev->lock, flags);	/* 上锁 */
    	if (dev->dev_stats) {
    		dev->dev_stats--;
    	}
    	spin_unlock_irqrestore(&dev->lock, flags);/* 解锁 */
    
    
    	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    	hw_led_reinit();
    	return 0;
    }
    

读写自旋锁

在自旋锁的基础上衍生而来 特点:允许读并发

typedef struct { 
	arch_rwlock_t raw_lock; 
} rwlock_t;

读写锁API

  • 创建和初始化

函数 功能
DEFINE_RWLOCK(rwlock_t lock) 定义并初始化读写锁
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) 打开下半部,并释放读锁。

顺序锁

在读写锁的基础上衍生而来允许读写并发,但是在读操作的时候同时进行了写操作,最好重新读取,保证读取的数据正确 最好重新进行读取,保证数据完整性。顺序锁保护的资源不能是指针,因为如果在写操作的时候可能会导致指针无效,而这个时候恰巧有读操作访问指针的话就可能导致意外发生,比如读取野指针导致系统崩溃。 Linux 关于顺序锁的定义

typedef struct { 
	struct seqcount seqcount; 
	spinlock_t lock; 
} seqlock_t;
  • 顺序锁定义并初始化

函数 功能
DEFINE_SEQLOCK(seqlock_t sl) 定义并初始化顺序锁
void seqlock_ini seqlock_t *sl) 初始化顺序锁
  • 顺序锁写操作

函数 功能
void write_seqlock(seqlock_t *sl) 获取写顺序锁。
void write_sequnlock(seqlock_t *sl) 释放写顺序锁。
void write_seqlock_irq(seqlock_t *sl) 禁止本地中断,并且获取写顺序锁
void write_sequnlock_irq(seqlock_t *sl) 打开本地中断,并且释放写顺序锁。
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) 读结束以后调用此函数检查在读的过程中有没有对资源进行写操作,如果有的话就要重读

自旋锁使用注意事项

  • 因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要 短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处 理方式,比如稍后要讲的信号量和互斥体。

  • 自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API函数,否则的话可能 导致 死锁。

  • 不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就 必须“自旋”, 等待锁被释放,然而你正处于“自旋”状态,根本没法释放锁。结果就是自己 把自己锁死了!

  • 在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管你用的是单核的还 是多核的 SOC,都将其当做多核 SOC来编写驱动程序。

信号量

相比于自旋锁,信号量可以使线程进入休眠状态,但是,信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态 以后会切换线程,切换线程就会有开销。总结一下信号量的特点:

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

  • 因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠;

  • 如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。

数型信号量,计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源。线程申请一次信号量就会自减一次知道为0;如果要互斥的访问共享资源那么信号量的值就不能大于1,此时的信号量就是一个二值信号量。 Linux内核使用 semaphore结构体表示信号量,结构体内容如下所示:

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

信号量的API函数

函数 功能
DEFINE_SEAMPHORE(name) 定义一个信号量,并且设置信号量的值为
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) 释放信号量

应用举例


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

信号量实验

参见代码【09.semaphone】工程,入口函数、open和close函数

  1. 入口函数

    
    static int __init led_init(void)
    {
    	/* 
    	 * 初始化信号量
    	 */
    	sema_init(&g_led_dev.sem, 1);
    
    
        /* 
         * 3:注册自己的 platform_driver 到平台的总线驱动链表里面
         *   注册的过程中会根据匹配规则   到【总线平台设备链表】里面找,
         *   如果匹配成功会调用驱动里面的platform_driver下的probe函数  
         */
    	printk("%s %s line %d:insmod !\n", __FILE__, __FUNCTION__, __LINE__);
    	return platform_driver_register(&led_driver);
    }
    
  2. open函数

    static int led_drv_open (struct inode *node, struct file *file)
    {
    			/* 获取信号量 */
    	if (down_interruptible(&g_led_dev.sem)) { /* 获取信号量,进入休眠状态的进程可以被信号打断 */
    	    printk("%s %s line %d:sem get err ,dev busy\n", __FILE__, __FUNCTION__, __LINE__);
    		return -ERESTARTSYS;
    	}
    	#if 0
    		down(&gpioled.sem);		/* 不能被信号打断 */
    	#endif
    
    
        hw_led_init(g_led_dev.nd);
    	file->private_data = &g_led_dev;
    
    	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    
    	return 0;
    }
    
  3. close函数

    
    static int led_drv_close (struct inode *node, struct file *file)
    {
    
    	struct led_char_dev *dev = file->private_data;
    
    	up(&dev->sem);		/* 释放信号量,信号量值加1 */
    
    	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    	hw_led_reinit();
    	return 0;
    }
    

互斥体

将信号量的值设置为1就可以使用信号量进行互斥访问了,虽然可以通过信号量实现互斥,但是 Linux提供了一个比信号量更专业的机制来进行 互斥,它就是互斥体 mutex。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申 请互斥体。 在我们编写 Linux驱动的时候遇到需要互斥访问的地方建议使用 mutex LINUX关于互斥体的定义如下

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

在使用 mutex之前要先定义一个 mutex变量。在使用 mutex的时候要注 意如下几点:

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

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

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

关于互斥体的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) 使用此函数获取信号量失败进入休眠以后可以被信号打断。

应用举例

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

互斥体实验

参见工程【10.mutex】

  1. 入口函数

    static int __init led_init(void)
    {
    	/* 	
    	 * 初始化互斥变量
    	 */
    	mutex_init(&g_led_dev.lock);
    
    
        /* 
         * 3:注册自己的 platform_driver 到平台的总线驱动链表里面
         *   注册的过程中会根据匹配规则   到【总线平台设备链表】里面找,
         *   如果匹配成功会调用驱动里面的platform_driver下的probe函数  
         */
    	printk("%s %s line %d:insmod !\n", __FILE__, __FUNCTION__, __LINE__);
    	return platform_driver_register(&led_driver);
    }
    
  2. open函数

    static int led_drv_open (struct inode *node, struct file *file)
    {
    	/* 获取互斥体,可以被信号打断 */
    	if (mutex_lock_interruptible(&g_led_dev.lock)) {
    		printk("%s %s line %d:mutex get err dev busy\n", __FILE__, __FUNCTION__, __LINE__);
    		return -ERESTARTSYS;
    	}
    #if 0
    	mutex_lock(&g_led_dev.lock);	/* 不能被信号打断 */
    #endif
    
    
        hw_led_init(g_led_dev.nd);
    	file->private_data = &g_led_dev;
    
    	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    
    	return 0;
    }
    
  3. close函数

    static int led_drv_close (struct inode *node, struct file *file)
    {
    	struct led_char_dev *dev = file->private_data;
    
    		/* 释放互斥锁 */
    	mutex_unlock(&dev->lock);
    
    	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    	hw_led_reinit();
    	return 0;
    }