驱动开发学习笔记---并发与竞争
一、并发与竞争简介
并发:多个“用户”同时访问一个共享的内存。
竞争:多个“用户”同时访问一段共享的内存并对其修改,就会造成数据混乱,甚至程序崩溃,这就是竞争。
二、造成并发与竞争的原因
1、多线程并发访问, Linux 是多任务(线程)的系统,所以多线程访问是最基本的原因。
2、抢占式并发访问, Linux 内核支持抢占,也就是说调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。
3、中断程序并发访问,这个无需多说,学过 STM32 的同学应该知道,硬件中断的权利可是很大的。
4、 SMP(多核)核间并发访问,现在 ARM 架构的多核 SOC 很常见,多核 CPU 存在核间并发访问。
三、临界区(共享资源)
在多任务的工作模式下有多个执行单位,它们通常可以拥有一段共享空间(比如全局变量、共享
四、避免并发与竞争的操作:
1、原子操作
原子操作就是指不能再进一步分割的操作,一般原子操作用于变量或者位操作。
对于基本的赋值操作比如a=1;在内核里面的步骤为:
ldr r0, =0X30000000 // 变量 a 地址 ldr r1, = 3 // 要写入的值 str r1, [r0] // 将 3 写入到 a 变量中
因为一个简单的赋值就有三条语句,然后在多进程里面就可以能出现竞争现象
在编写驱动时,对全局变量要求保证原子操作可以用 atomic_t 结构体:
typedef struct { int counter; } atomic_t;
通过原子整形API库函数进行整形操作和位操作。
2、自旋锁
原子操作可以用来解决变量访问的问题,我们在实际应用中有时候需要操作的不止是变量,还有一块区域(比如结构体变量)等。为了保护他们,我们就添加了各种锁,比如自旋锁。自旋锁适用于短时期轻量级加锁,长时间就用信号量。
自旋锁使用注意事项:
-
因为等待资源的线程会一直“自旋”,所以线程持有自旋锁,访问临界区的时间不能太长。
-
临界区中,不能存在任何会引起线程阻塞、睡眠的操作,否者可能引起死锁。
-
不能递归的申请自旋锁。
-
考虑到程序的可移植性,在使用自旋锁时不管你的cpu是单核,还是多核,都当做多核来使用。
死锁:线程在获取锁之后阻塞或睡眠,导致多个线程都阻塞。
自旋锁会自动禁止抢占,也就说当线程 A得到锁以后会暂时禁止内核抢占。如果线程 A 在持有锁期间进入了休眠状态,那么线程 A 会自动放弃 CPU 使用权。线程 B 开始运行,线程 B 也想要获取锁,但是此时锁被 A 线程持有,而且内核抢占还被禁止了!线程 B 无法被调度出去,那么线程 A 就无法运行,锁也就无法释放,好了,死锁发生了!
3.信号量
信号量特点:
1.线程可以访问的临界区比较大,访问资源的时间长,所以临界区中可以调用引起阻塞的操作。 2.等待资源的线程不会一直等待,会让出 cpu 使用权,进入挂起、睡眠状态。(cpu经过调度执行其它线程) 3.正是因为会进入睡眠态,所以不能在中断中使用,同时也不能在自旋锁的临界区中使用。(中断讲究快进快出) 4.在等待资源时CPU 会经历 切换、唤醒进程等等操作,会产生一定的开销。当使用信号量所争取的利益大于这个开销时就不要使用信号量。
4.互斥体
信号量值 = 1 可以实现互斥访问,Linux 比信号量更专业的机制来进行互斥,即互斥体 mutex。使用过程中有其它线程想要获取该资源的锁,那么它就会被阻塞陷入睡眠状态,直到该资源被解锁才会被唤醒。
互斥体特点: 1.mutex 可以导致休眠,因此也不能在中断中使用 mutex,中断中只能使用自旋锁。 2.和信号量一样, mutex 保护的临界区可以调用引起阻塞的 API 函数。(临界区中可以引起塞) 3.因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁。
互斥体使用注意事项: 互斥体和自旋锁都是解决互斥问题的一种手段。互斥体是进程级别的,互斥体在使用的时候会发生进程间的切换,因此,使用互斥体资源开销比较大。自旋锁可以节省上下文切换的时间,如果持有锁的时间不长,使用自旋锁是比较好的选择,如果持有锁时间比较长,互斥体显然是更好的选择。
互斥体与自旋锁区别: 1.当锁不能被获取到时,使用互斥体的开销是进程上下文切换时间,使用自旋锁的开销是等待获取自旋锁(由临界区执行时间决定)。若临界区比较小,宜使用自旋锁,若临界区很大,应使用互斥 体。 2.互斥体所保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护包含这样代码的临界区。因为阻塞意味着要进行进程的切换,如果进程被切换岀去后,另一个进程企图获取本自旋锁,死锁就会发生。 3.互斥体存在于进程上下文。因此,如果被保护的共享资源需要在中断或软中断情况下使用,则在互斥体和自旋锁之间只能选择自旋锁。当然,如果一定要使用互斥体,则只能通过mutextrylock()方式进行,不能获取就立即返回以避免阻塞。