linux设备驱动基础01之并发与竞态(代码片段)

PanGC2014 PanGC2014     2022-12-04     688

关键词:

一、基础概念

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设备驱动的并发控制学习笔记(代码片段)

文章目录并发和竞态编译乱序和执行乱序并发控制机制中断屏蔽原子操作整型原子操作位原子操作自旋锁自旋锁的使用读写自旋锁顺序锁读-复制-更新信号量互斥体完成量并发和竞态并发:多个执行单元同时、并行被执行。... 查看详情

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

linux设备驱动归纳总结(四):5.多处理器下的竞态和并发xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx这节将在上一节的基础上介绍支持多处理器和内核抢占的内核如何避免并发。除了内核抢占和... 查看详情

:并发与竞态

...是,只要可能,就应该避免资源共享。如果没有并发的访问,也就不会有竟态的产生。因此,仔细编写的内核代码应该具有最少的共享。这种思想的最明显应用就是避免使用全局变量。如果我们将资源放在一个多... 查看详情

linux设备驱动的并发控(代码片段)

文章目录并发和竞态编译乱序和执行乱序并发控制机制中断屏蔽local_irq_disable原子操作atomic整型原子操作位原子操作自旋锁spin_lock自旋锁的使用读写自旋锁rwlock顺序锁seqlock读-复制-更新RCU信号量Semaphore互斥体mutex完成量Completion并... 查看详情

linux设备驱动的并发控(代码片段)

文章目录并发和竞态编译乱序和执行乱序并发控制机制中断屏蔽local_irq_disable原子操作atomic整型原子操作位原子操作自旋锁spin_lock自旋锁的使用读写自旋锁rwlock顺序锁seqlock读-复制-更新RCU信号量Semaphore互斥体mutex完成量Completion并... 查看详情

juc并发编程--避免临界区的竞态条件之synchronized解决方案(同步代码块)(代码片段)

1.synchronized解决方案应用之互斥为了避免临界区的竞态条件发生,有多种手段可以达到目的。阻塞式的解决方案:synchronized,Lock非阻塞式的解决方案:原子变量1.1synchronized介绍synchronized,即俗称的【对象锁】&#... 查看详情

并发控制(代码片段)

 一、并发与竞态并发是指一段时间内有多个程序执行,但任一个时刻点上只有一个程序在运行并发就会导致一个问题:假设程序A对一个文件写入3000个字符“a”,而另一个程序B对这个文件写入3000个“b”,第三... 查看详情

[java并发编程实战]基础知识(代码片段)

什么是线程安全性?定义线程安全性:当多个线程访问某个类时,这个类始终都能表现正确的行为,那么就称这个类是线程安全的。单线程:所见及所知(weknowitwhenweseeit)竞态条件当某个计算的正确性... 查看详情

linux设备驱动基础04之异步通知(代码片段)

一、基础简介使用阻塞/非阻塞方式读写设备驱动时,都需要应用程序主动发起,对于非阻塞方式来说还需通过poll函数不断轮询。这种情况下更好的解决方案是,设备驱动主动向应用程序发出通知,报告自己可以... 查看详情

linux驱动程序中的并发控制(自旋锁)-44(代码片段)

自旋锁(spinlock)简介原子锁和自旋锁的使用范围原子操作是一种很好的避免竞态的方式,使用非常简单。但在某些方面却显得过于简单。例如,有很多数据需要被格式化,被添加到某些数据结构中,然后... 查看详情

juc并发编程之completablefuture基础用法(代码片段)

目录实现多线程的四种方式方式一:继承Thread类方式二:实现Runnable接口方式三:实现Callable接口方式四:线程池创建异步对象回调方法handle方法 线程串行化 任务组合组合任务单任务完成及执行实现多线程的四... 查看详情

linux操作系统|并发竞态互斥锁自旋锁信号量都是什么鬼?(代码片段)

1.锁的由来?学习linux的时候,肯定会遇到各种和锁相关的知识,有时候自己学好了一点,感觉半桶水的自己已经可以华山论剑了,又突然冒出一个新的知识点,我看到新知识点的时候,有时间也是一脸... 查看详情

linux驱动程序中的并发控制-2(自旋锁)-44(代码片段)

自旋锁(spinlock)简介原子锁和自旋锁的使用范围原子操作是一种很好的避免竞态的方式,使用非常简单。但在某些方面却显得过于简单。例如,有很多数据需要被格式化,被添加到某些数据结构中,然后... 查看详情

15同步于互斥并发竞态和编译乱序执行乱序(代码片段)

1并发和竞态1.1简介设备在运行的过程中存在多个进程对资源的并发访问多个执行单元同时,并行的执行。并发事件对共享资源的访问(硬件资源,全局变量,静态变量等),很容易导致设备出现竞态。竞态的出现会导致设备出... 查看详情

linux设备驱动基础03之阻塞与非阻塞io(代码片段)

...定要考虑到阻塞和非阻塞。默认情况下,应用程序对设备驱动的读取方式时阻塞式的。IO是指Input/Output,即应用程序对驱动设备的输入/输出操作。当应用程序对设备驱动进行操作的时候&#x 查看详情

嵌入式linux驱动开发01:基础开发与使用(代码片段)

文章目录目的基础说明驱动测试应用程序基础开发与使用驱动模块入口与出口驱动模块安装与卸载字符设备注册与注销设备开关与读写自动创建与销毁设备节点使用VSCode进行开发总结目的驱动开发是嵌入式Linux中工作比重比较大... 查看详情

golangdatarace竞态条件(代码片段)

...件dataraceraceconditiongolangracedetectorgolang的协程机制使得编写并发代码变得非常容易,但是多线程(协程)编程离不开的一个话题就是线程(协程)安全,也就是并发环境下是不是存在竞态条件。这里有两个概念首先需要区分清楚&... 查看详情

juc并发编程之completablefuture基础用法(代码片段)

目录实现多线程的四种方式方式一:继承Thread类方式二:实现Runnable接口方式三:实现Callable接口方式四:线程池创建异步对象回调方法handle方法 线程串行化 任务组合组合任务单任务完成及执行实现多线程的四... 查看详情