并发与竞争

  • Linux是一个多任务操作系统 ,并发即多个线程任务共同操作同一段内存或者设备(共享资源)的情况,竞争则是多个任务同时访问同一片内存区域,这些任务可能会相互覆盖这段内存中的数据,造成数据混乱。

主要原因:

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

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

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

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

注:并发强调执行多个操作的对象只能有一个,并行则不强调,多个操作可以由多个对象执行。

原子操作

  • 原子操作就是指不能再进一步分割的操作,一般原子操作用于变量或者位操作。

  • 原子操作API函数

  1. 原子变量结构体

1

  1. 原子 操作API函数

    2

注:根据系统架构,32位和64位的SOC使用原子结构体和API函数有区别。

  1. 原子位操作API函数

    3

自旋锁

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

自旋锁的“自旋”也就是“原地打转”的意思,“原地打转”的目的是为了等待自旋锁可以用,可以访问共享资源。

  1. 自旋锁结构体

    4

  2. 自旋锁API函数

    5

  3. 读写自旋锁

    保证共享资源读和写不能同时进行,但是可以多人并发的读取该共享资源,即当某个数据结构符合读/写或生产者/消费者模型的时候就可以使用读写自旋锁。

    读写自旋锁为读和写操作提供了不同的锁,一次只能允许一个写操作,也就是只能一个线程持有写锁,而且不能进行读操作。但是当没有写操作的时候允许一个或多个线程持有读锁,可以进行并发的读操作。

    • 读写操作API函数

      6

  4. 顺序锁

    顺序锁在读写锁的基础上衍生而来的,使用读写锁的时候读操作和写操作不能同时进行。使用顺序锁的话可以允许在写的时候进行读操作,也就是实现同时读写,但是不允许同时进行并发的写操作。虽然顺序锁的读和写操作可以同时进行,但是如果在读的过程中发生了写操作,最好重新进行读取,保证数据完整性。

    7

    注:

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

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

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

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

信号量

  1. 信号量有一个信号量值,相当于一个房子有 10 把钥匙,这 10 把钥匙就相当于信号量值为10。因此,可以通过信号量来控制访问共享资源的访问数量,如果要想进房间,那就要先获取一把钥匙,信号量值减 1,直到 10 把钥匙都被拿走,信号量值为 0,这个时候就不允许任何人进入房间了,因为没钥匙了。如果有人从房间出来,那他要归还他所持有的那把钥匙,信号量值加 1,此时有 1 把钥匙了,那么可以允许进去一个人。相当于通过信号量控制访问资源的线程数,在初始化的时候将信号量值设置的大于 1,那么这个信号量就是计数型信号量,计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源。

    相比于自旋锁,信号量可以使线程进入休眠状态。

  2. 信号量API函数

    8

互斥体

互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体,Linux为我们提供了mutex机制进行互mu斥体访问。

  1. 互斥体结构体API函数

    9

    参考文档

    I.MX6U嵌入式Linux驱动开发指南文档