Linux并发与竞争

一.概念

Linux 是一个多任务操作系统,肯定会存在多个任务共同操作同一段内存或者设备的情况,多个任务甚至中断都能访问的资源叫做共享资源。在驱动开发中要注意对共享资源的保护,也就是要处理对共享资源的并发访问。

Linux 系统并发产生的原因很复杂,总结一下有下面几个主要原因:

1、多线程并发访问,Linux 是多任务(线程)的系统,所以多线程访问是最基本的原因。

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

3、中断程序并发访问,这个无需多说,学过 STM32 的同学应该知道,硬件中断的权利可是很大的。

4、SMP(多核)核间并发访问,现在 ARM 架构的多核 SOC 很常见,多核 CPU 存在核间并发访问。

二.原子操作

原子操作就是指不能再进一步分割的操作,一般原子操作用于变量或者位操作.假如现在要对无符号整形变量 a 赋值,值为 3,对于 C 语言来讲很简单,直接就是:

复制代码
a = 3

C 语言要先编译为成汇编指令,ARM 架构不支持直接对寄存器进行读写操作,比如要借助寄存器 R0、R1 等来完成赋值操作。假设变量 a 的地址为 0X3000000,"a=3"这一行 C语言可能会被编译为如下所示的汇编代码:

复制代码
ldr r0, =0X30000000     /* 变量 a 地址 */
 ldr r1, = 3             /* 要写入的值 */
 str r1, [r0]             /* 将 3 写入到 a 变量中 */

假设现在线程 A要向 a 变量写入 10 这个值,而线程 B 也要向 a 变量写入 20 这个值,我们理想中的执行顺序如下

但是实际上的执行流程可能如图:

线程 A 最终将变量 a 设置为了 20,而并不是要求的 10!这就是一个最简单的设置变量值的并发与竞争的例子,要解决这个问题就要保证示例代码 中的三行汇编指令作为一个整体运行,也就是作为一个原子存在。

Linux 内核提供了两组原子操作 API 函数,一组是对整形变量进行操作的,一组是对位进行操作的

1.原子整形操作 API 函数

Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量,此结构体定义在 include/linux/types.h 文件中,定义如下:

复制代码
typedef struct {
int counter;
} atomic_t;

如果要使用原子操作 API 函数,首先要先定义一个 atomic_t 的变量,如下所示:

复制代码
atomic_t a; //定义 a

也可以在定义原子变量的时候给原子变量赋初值,如下所示:

复制代码
atomic_t b = ATOMIC_INIT(0); //定义原子变量 b 并赋初值为 0

原子变量有了,接下来就是对原子变量进行操作,比如读、写、增加、减少等等,Linux 内核提供了大量的原子操作 API 函数

原子变量和相应的 API 函数使用起来很简单,参考如下示例:

复制代码
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
*/

2.原子位操作函数

位操作也是很常用的操作,Linux 内核也提供了一系列的原子位操作 API 函数,只不过原子位操作不像原子整形变量那样有个 atomic_t 的数据结构,原子位操作是直接对内存进行操作

三.自旋锁

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

自旋锁的"自旋"也就是"原地打转"的意思,"原地打转"的目的是为了等待自旋锁可以用,可以访问共享资源。从这里我们可以看到自旋锁的一个缺点:那就是等待自旋锁的线程会一直处于自旋状态,这样会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长。自旋锁适用于短时期的轻量级加锁,如果遇到需要长时间持有锁的场景那就需要换其他的方法了.

Linux 内核使用结构体 spinlock_t 表示自旋锁,结构体定义如下所示:

复制代码
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;

在使用自旋锁之前,肯定要先定义一个自旋锁变量,定义方法如下所示:

复制代码
spinlock_t lock; //定义自旋锁

1.自旋锁API函数

自旋锁 API 函数适用于 SMP 或支持抢占的单 CPU 下线程之间的并发访问,也就是用于线程与线程之间

被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API 函数,否则的话会可能会导致死锁现象的发生: 自旋锁会自动禁止抢占,当线程 A得到锁以后会暂时禁止内核抢占。如果线程 A 在持有锁期间进入了休眠状态,那么线程 A 会自动放弃 CPU 使用权。线程 B 开始运行,线程 B 也想要获取锁,但是此时锁被 A 线程持有,而且内核抢占还被禁止了!线程 B 无法被调度出去,那么线程 A 就无法运行,锁也就无法释放,死锁发生了!

中断里面可以使用自旋锁,但是在中断里面使用自旋锁的时候,在获取锁之前一定要先禁止本地中断.否则可能导致锁死现象的发生

线程 A 先运行,并且获取到了 lock 这个锁,当线程 A 运行 functionA 函数的时候中断发生了,中断抢走了 CPU 使用权。右边的中断服务函数也要获取 lock 这个锁,但是这个锁被线程 A 占有着,中断就会一直自旋,等待锁有效。但是在中断服务函数执行完之前,线程 A 是不可能执行的.死锁又发生!

最好的解决方法就是获取锁之前关闭本地中断

建议使用 spin_lock_irqsave/ spin_unlock_irqrestore,因为这一组函数会保存中断状态,在释放锁的时候会恢复中断状态。

四.信号量

相比于自旋锁,信号量可以使线程进入休眠状态.信号量的特点:

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

2.信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。

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

1.信号量API函数

Linux 内核使用 semaphore 结构体表示信号量,结构体内容如下所示:

复制代码
struct semaphore {
raw_spinlock_t    lock;
unsigned int      count;
struct list_head  wait_list;
};

有关信号量的 API 函数如下:

信号量的使用如下所示:

复制代码
struct semaphore sem;     /* 定义信号量 
sema_init(&sem, 1);     /* 初始化信号量 */
down(&sem);                 /* 申请信号量 */
up(&sem);            /* 释放信号量 */

五.互斥体

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

复制代码
struct mutex {
atomic_t    count;
spinlock_t  wait_lock;
};

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

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

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

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

使用例子:

复制代码
struct mutex     lock; /* 定义一个互斥体 */
mutex_init(&lock);     /* 初始化互斥体 */
mutex_lock(&lock);     /* 上锁 */
 /* 临界区 */
 mutex_unlock(&lock);/* 解锁 */
相关推荐
律宏阔3 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔3 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴3 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千4 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei374 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei374 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei374 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW4 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
GeW4 小时前
工业场景中的Linux与数据库选型:高端化转型的技术底座
linux
yunwei374 小时前
eBPF 开发者教程: 简单的 XDP 负载均衡器
linux·后端·性能优化