前言

内核驱动的并发&竟态很容易理解,其解决方法也不能,看看例程就可以了。
对于API,看看内核源码和内核文档即可。

原文链接https://www.cnblogs.com/lizhuming/p/14907262.html

12. 并发&竞态

本章内容为驱动基石之一
驱动只提供功能,不提供策略

12.1 并发&竞态概念

并发

  • 指多个单元同时、并行执行。
  • 但是并发执行的单元对共享资源的访问容易产生竞态
  • 单核的并发可以参考 MCU RTOS 多任务原理。看似并行,实质串行。不过也存在竞态

并发产生原因(大概):

  • 多线程并发访问。
  • 抢占式并发访问。(linux2.6及高版本的内核为抢占式内核
  • 中断程序并发访问。
  • 多核(SMP)核间并发访问。

竞态

  • 指并发的执行单元对共享资源的访问。
  • 竞态产生的条件:
    • 存在共享资源。
    • 对共享资源进行竞争访问。

12.2 竞态解决方法

需要解决竞态是因为要保护数据。
确保每个时刻都只有一个执行单元访问共享资源。

竞态解决方法有:

  • 原子操作
  • 自旋锁操作
  • 信号量操作
  • 互斥体操作

12.3 原子

参考文档:

  • Documentation\atomic_t.txt
  • Documentation\atomic_bitops.txt

12.3.1 原子介绍

都知道,在 C 的世界里,a = 10; 这样一个简单的赋值,到了汇编的世界就不止一条语句啦。若此时多线程往变量 a 的地址赋值,就可能会产生数据错误。

原子操作就是不可分割操作。
注意:原子操作只能对 整型变量位操作 具有保护功能。

12.3.2 原子操作步骤

原子操作

  • 定义原子变量&设置初始值。
  • 设置原子变量的值。
  • 获取原子变量的值。
  • 原子变量的 加/减。
  • 原子变量的 自加/自减。
  • 原子变量的 加/减 及返回值。
  • 原子变量测试函数。

12.3.3 原子 API

由于函数容易理解,所以就不像以前的笔记一样详细列出。

整型原子的操作需要个 atomic_t 结构体。
bit原子的操作只需要一个地址即可,是直接对内存操作。

atomic_t 32bit 整型原子变量结构体

//atomic_t类型结构体
typedef struct
{
   int counter;
}atomic_t;

atomic64_t 64bit 整型原子变量结构体

//atomic64_t 类型结构体
typedef struct
{
   long long  counter;
}atomic64_t;

整型原子 API 汇总

更多 API(如atomic_dec_unless_positive()、atomic_inc_unless_negative()) 请参考内核源码和推荐的文档。

bit原子的操作不需要 atomic_t 结构体,它是直接对 内存 操作的。

bit 原子 API 汇总

12.4 自旋锁

12.4.1 自旋锁介绍

原子操作只能对整型变量或者bit进行保护。而自旋锁能对一个单元进行保护,是给代码段添加一把锁。

自旋锁是实现互斥访问的常用手段。
获取自旋锁后再运行代码才能被保护起来。

自旋锁特点

  • 当使用自旋锁获取锁失败时(即需要访问的代码段被锁住了),线程不休眠,做死循环检测锁状态,直至自旋锁被释放。
  • 简单,不休眠,可在中断中使用。
  • 使用不当会导致死锁。如:
    • 递归获取锁:第一次获取锁成功,在自旋锁保护的代码段内进行获取锁,那便永远等不到解锁,导致死锁。

自旋锁缺点

  • 死循环检测,占用系统资源。
  • 递归获取锁后会导致死锁。
  • 同一线程不能连续两次获取自旋锁,必须一获取一释放。
  • 自旋锁在锁定期间不能调用引起进程调度的函数,否则可能导致系统崩溃。

12.4.2 自旋锁操作步骤

自旋锁操作

  • 定义自旋锁。
  • 初始化自旋锁。
  • 获取自旋锁。
  • 释放自旋锁。

自旋锁使用注意事项

  • 锁的持有时间要短。因为自旋锁是不会休眠的,以免其它线程获取锁等待太久,降低系统性能。
  • 自旋锁保护的临界区内不能调用引起线程休眠的 API 函数,否则可能引起死锁。
  • 不能递归获取自旋锁,否则会导致死锁。
  • 按多核思想编程。提高系统可移植性。

12.4.3 自旋锁 API

spinlock_t 结构体

typedef struct
{
   struct lock_impl internal_lock;
}spinlock_t;

自旋锁 API 汇总

12.4.4 读写自旋锁

普通的自旋锁是一刀切的,不管访问者对临界区的操作是读还是写。
但是实际上,很多共享资源都允许多个执行单元同时读,这是不影响数据的。

所以,读写自旋锁 允许 读并发,但是不允许 写并发,且不允许读写同时出现。
即有允许以下情景:

  • 多读。
  • 一写。

读写自旋锁 结构体

typedef struct
{
   arch_rwlock_t raw_lock;
}rwlock_t;

读写自旋锁 API

  • 定义&初始化
  • 读锁 API
  • 写锁
    • 把前面读锁的前缀 read_ 改为 write_,即可。

12.4.5 顺序锁

顺序锁读写锁 的一个优化。

读写锁 不允许同时出现。有以下前景:

  • 多读
  • 一写

顺序锁 允许同时出现,但是只能出现一个写。有以下前景:

  • 多读
  • 一写
  • 多读一写

顺序自旋锁 结构体

typedef struct
{
   struct seqcount seqcount;
   spinlock_t lock;
}seqlock_t;

顺序自旋锁 API

  • 定义&初始化
  • 读锁 API
    • 需要注意的是,写操作的顺序锁,会对顺序号加1-2。若 read_seqretry() 检测到顺序号不一致,则请重新读去数据。
  • 写锁 API

12.5 信号量

12.5.1 信号量概念

学过 RTOS 的都知道信号量了。可以看做一个全局计数器。

信号量常用于同步和互斥

信号量的获取失败后,线程可引入休眠,当信号量可用时,系统会通知其退出休眠。

12.5.2 信号量操作

信号量操作

  • 定义信号量。
  • 初始化信号量。
  • 尝试获取信号量。
  • 获取信号量。
  • 释放信号量。

信号量使用注意事项

  • 适用于占用资源较长时间的情景。因为信号量可以引起休眠,占用系统资源少。若占用资源时间少的,建议使用 自旋锁 ,因为不用切换线程,系统开销小。
  • 不能用于中断。同样是因为信号量可以引起休眠。不过可以使用 down_interruptible() 函数。
  • 保护的临界区内可调用引起阻塞的 API

12.5.3 信号量 API

semaphore 结构体

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

12.6 互斥体

12.6.1 互斥体概念

互斥体 的占用其实和 信号量量值为 1 的效果是一样的。
但是互斥体的执行效率更高,毕竟,专业的API做专业的事嘛。

12.6.2 互斥体操作

互斥体执行操作

  • 定义互斥体。
  • 初始化互斥体。
  • 尝试获取互斥体。
  • 获取互斥体。
  • 释放互斥体。

互斥体使用注意事项

  • 不能在中断中使用。因为 mutex 会导致休眠。除非使用函数 int mutex_lock_interruptible
  • 必须由 mutex 持有者释放。因为一次只有一条线程持有。
  • 保护的临界区内可调用引起阻塞的 API

12.6.3 互斥体 API

12.7 完成量

12.7.1 完成量概念

完成量(completion)。

完成量用于一个执行单元等待另一个执行单元。

12.7.2 完成量操作

完成量操作

  • 定义完成量。
  • 初始化完成量。
  • 等待完成量。
  • 唤醒完成量。

12.7.3 完成量 API

完成量结构体

struct completion {
	unsigned int done;
	wait_queue_head_t wait;
};
06-21 00:40