Linux 系统是个多任务操作系统,会存在多个任务同时访问同一片内存区域,这些任务可能会相互覆盖这段内存中的数据,造成内存数据混乱。我们需要对共享数据进行相应的保护处理。
产生并发的主要原因有:
①多线程并发访问, Linux 是多任务(线程)的系统,多线程访问是最基本的原因
②抢占式并发访问,Linux 内核支持抢占,调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。
③中断程序并发访问,硬件中断的权利可以是很大的。
④SMP(多核)核间并发访问,多核 CPU 存在核间并发访问。
并发:多个执行单元同时进行或多个执行单元微观串行执行,宏观并行执行。微观串行,宏观并行:理解成把时间轴放大以后,各个任务是串行执行,然后每个任务执行一定的时间片,执行完后由调度系统调度到另一个任务去执行。因为CPU的速度很快,所以宏观看来是并行执行。
竞争:并发的执行单元对共享资源(硬件资源和软件上的全局变量)的访问而导致的竞争状态。
临界资源: 多个进程访问的资源,共享数据段
临界区:多个进程访问的代码段
一、原子操作
原子操作是指不能再进一步分割的操作。一般原子操作用于整形变量或者位操作。
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
原子变量有了,接下来就是对原子变量进行操作,读、写、增加、减少。
原子操作API函数
如果使用 64 位的 SOC 的话,就使用64 位的原子变量
typedef struct {
long long counter;
} atomic64_t;
相应的操作函数把“atomic_”前缀换为“atomic64_”,将 int 换为 long long即可。
原子位操作:即位操作,在Linux 内核也提供了一系列的原子位操作 API 函数,原子位操作不像原子整形变量那样有个 atomic_t 的数据结构,原子位操作是直接对内存进行操作。
atomic_t lock; /* 原子变量 */
/* 初始化原子变量 */
atomic_set(&lock, 1); /* 原子变量初始值为1 */
atomic_inc(&lock); /*释放原子变量*/
/* 通过判断原子变量的值来检查共享资源有没有被别的应用使用:1 */
//若为1 则1-1为0 ,返回真,取反为假
//若为0 ,则0-1不为0,返回假,取反为真,执行下面
if (!atomic_dec_and_test(&gpioled.lock)) {
atomic_inc(&gpioled.lock); /* 小于0的话就加1,使其原子变量等于0 */
return -EBUSY; /* 共享资源被使用,返回忙 */
}
/* 通过判断原子变量的值来检查共享资源有没有被别的应用使用:2 */
if(atomic_read(&lock) <= 0)
{
return -EBUSY;
}
else
{
atomic_dec(&lock);
}
二、自旋锁
原子操作只能对整形变量或者位进行保护,不能运用到太复杂的环境中。如若共享数据为设备结构体,对于结构体中成员变量的操作要保证原子性,在线程 A 对结构体变量使用期间,应该禁止其他的线程来访问此结构体变量,此时可以使用自旋锁解决。
当一个线程要访问某个共享资源的时候首先要先获取相应的锁, 锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,直到线程A释放自旋锁,线程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; //定义自旋锁
定义好自旋锁变量以后就可以使用相应的 API 函数来操作自旋锁,加锁或者释放锁。
自旋锁API函数
自旋锁API函数适用于SMP或支持抢占的单CPU下线程之间的并发访问,用于线程与线程之间,被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API 函数,否则可能会导致死锁现象。自旋锁会自动禁止抢占,当线程 A得到锁以后会暂时禁止内核抢占。
如果线程 A 在持有锁期间进入了休眠状态,线程 A 会自动放弃 CPU 使用权。线程 B 开始运行,线程 B 也想要获取锁,但是此时锁被 A 线程持有,而且内核抢占还被禁止。线程 B 无法被调度出去,那么线程 A 就无法运行,锁也就无法释放,产生死锁现象。
如果在自旋锁加锁的临界区中使用中断,并且中断也会去访问共享资源,也可能会发送死锁现象。
在中断里面是可以使用自旋锁,但是在中断里面使用自旋锁的时候,在获取锁之前一定要先禁止本地中断(也就是本 CPU 中断,对于多核 SOC来说会有多个 CPU 核),否则可能导致锁死现象的发生。
获取锁之前关闭本地中断,相关自旋锁API
使用 spin_lock_irq/spin_unlock_irq 的时候需要用户能够确定加锁之前的中断状态,但实际上内核很庞大,很难确定某个时刻的中断状态,因此不推荐使用。建议使用 spin_lock_irqsave/ spin_unlock_irqrestore,这一组函数会保存中断状态,在释放锁的时候会恢复中断状态。
一般在线程中使用 spin_lock_irqsave/spin_unlock_irqrestore
在中断中使用spin_lock/spin_unlock
DEFINE_SPINLOCK(lock) /* 定义并初始化一个锁 */
/* 线程 A */
void functionA ()
{
unsigned long flags; /* 中断状态 */
spin_lock_irqsave(&lock, flags) /* 获取锁 */
/* 临界区 */
spin_unlock_irqrestore(&lock, flags) /* 释放锁 */
}
/* 中断服务函数 */
void irq()
{
spin_lock(&lock) /* 获取锁 */
/* 临界区 */
spin_unlock(&lock) /* 释放锁 */
}
注意自旋锁使用事项:
①锁的持有时间不能太长,一定要短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式:信号量、互斥体。
②自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则可能导致死锁。
③不能递归申请自旋锁,否则可能导致死锁。
三、信号量
Linux 内核提供了信号量机制,信号量常常用于控制对共享资源的访问。它是一个计数器,常用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
信号量的特点
①信号量可以使等待资源线程进入休眠状态,适用于占用资源比较久的场合
②信号量会引起休眠,中断不能休眠,所以信号量不能用于中断。
③如果共享资源的持有时间比较短,不适合使用信号量,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的优势,此时使用自旋锁。
示例:假设一个房子有 10 把钥匙,这 10 把钥匙就相当于信号量值为10。可以通过信号量来控制访问共享资源的访问数量。如果要想进房间,需要先获取一把钥匙,信号量值减 1,直到 10 把钥匙被拿走,信号量值为 0,这时不允许任何人进入房间。如果有人从房间出来,并归还钥匙,信号量值加 1,现在可以允许进去一个人。
通过信号量控制访问资源的线程数,在初始化的时将信号量值设置的大于 1,信号量是计数型信号量,计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源。
如果要互斥的访问共享资源,则信号量的值小于等于1,此时信号量称为二值信号量。
Linux 内核使用 semaphore 结构体表示信号量
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
使用信号量①先定义信号量②初始化信号量
struct semaphore sem; /* 定义信号量 */
sema_init(&sem, 1); /* 初始化信号量 */
down(&sem); /* 申请信号量 */
/* 临界区 */
up(&sem); /* 释放信号量 */
信号量常用API
四、互斥体
将信号量的值设置为 1 就可以使用信号量进行互斥访问。但是 Linux 提供了专门的互斥体mutex。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体。在 Linux 驱动的时遇到需要互斥访问的地方一般使用 mutex。
Linux 内核使用 mutex 结构体表示互斥体
struct mutex {
/* 1: unlocked, 0: locked, negative: locked, possible waiters */
atomic_t count;
spinlock_t wait_lock;
};
互斥体使用注意事项:
①mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
②mutex 保护的临界区可以调用引起阻塞的 API 函数(信号量也可以)③因为一次只有一个线程可以持有 mutex,所以,必须由 mutex 的持有者释放 mutex。并
且 mutex 不能递归上锁和解锁
互斥体常用API
struct mutex lock; /* 定义一个互斥体 */
mutex_init(&lock); /* 初始化互斥体 */
mutex_lock(&lock); /* 上锁 */
/* 临界区 */
mutex_unlock(&lock); /* 解锁 */