目录
一、并发与竞争
并发就是多任务同时访问同一片内存区域,这些任务可能相互覆盖这段内存中的数据,造成数据混乱。
引起并发的原因大致可有分为以下几种:
①、多线程并发访问
②、抢占式并发访问,正常运行的线程可以被调度程序挂起,重而运行其他线程
③、中断并发访问
④、多核并发访问
因为并发,带来的问题就是竞争同一片共享数据区域,这个共享数据区域就是需要保护的对象,一般指多个线程都会访问的共享数据,比如全局变量,设备结构体,或者其他数据,某个线程的局部变量不需要保护。
linux内核提供多种方法处理并发与竞争。
二、处理并发与竞争
2.1、原子操作
原子操作指不能再进行分割的操作,常用于变量或者位操作。利用原子操作,可以让语句作为一个整体执行完毕,不会被切割。
2.1.1、原子整形操作
内核用atomic_t的结构体来完成整形数据的原子操作:
typedef struct {
int counter;
} atomic_t;
#ifdef CONFIG_64BIT
typedef struct {
long counter;
} atomic64_t;
#endif
原子操作API函数:
注意:atomic_dec_and_test函数,从v减1,如果结果为0就返回真,这样执行后,实际上v的值就减1了,并不是v的值没变。
使用示例:
//定义原子变量b,并赋初始值
atomic_t b = ATOMIC_INIT(0);
//设置b=10
atomic_set(&b, 10);
//读取b的值,肯定为10
atomic_read(&b);
//b的值加1.b肯定为11
atomic_inc(&b);
2.1.2、原子位操作
原子位操作是直接对内存进行操作,API函数如下:
2.2、自旋锁
前面提到的原子操作,只能对整形变量和位进行保护,在实际开发中还有其他类型的数据,因此只用原子操作是远远不够的。
锁:当一个线程要访问某个共享资源的时候,首先要获取相应的锁,锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他线程就不能获取此锁。自旋:就是原地打转的意思,一直等待锁释放。
使用自旋锁的缺点:等待自旋锁的线程会一直处于自旋状态,浪费处理器事件,降低系统性能,因此自旋锁持有的时间不能太长,适用于短时间的轻量加锁。
自旋锁结构体定义如下:
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;
2.2.1、自旋锁API
以下API函数使用与多核或者支持抢占的单核下线程之间的并发访问:
使用注意事项:
被自旋锁保护的临界区内一定不能调用任何能够引起睡眠和阻塞的API函数,否则可能会导致死锁现象。
中断与线程之间的并发访问:
注意事项:
①、不推荐使用spin_lock_irq/spin_unlock_irq
②、一般在线程中使用spin_lock_irqsave/spin_unlock_irqrestore,在中断中使用spin_lock/spin_unlock
2.3、信号量
信号量的特点:
①、信号量可以使等待资源的线程休眠,适用那些占用资源比较久的场合
②、不能用于中断中,因为信号量会引起休眠,中断需要快进快出
③、如果共享资源持有时间比较短,就不适合适用信号量,因为频繁线程休眠会切换,引起的开销远大于信号量带来的优势。
信号量有一个信号值,来控制访问共享资源的线程数,在初始化的时候将信号量的值设置大于1,那么这个信号量就是计数型信号量,即允许多个线程同时访问共享资源,因此不能用于互斥访问;如果要实现互斥访问,那么信号量的值设置为1,也称为二值信号量。
2.3.1、信号量API
信号量结构体如下:
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); /* 释放信号量 */
2.4、互斥体
将信号量的值设置为1就可以用来进行互斥访问,但更专业的就是用互斥体mutex,表示一次只有一个线程来访问共享资源。
特点:
①、mutex可以导致休眠,因此不能在中断中使用mutex,中断中只能使用自旋锁
②、和信号量一样,mutex保护的临界区可以调用引起阻塞的api函数
③、因为一次只有一个线程可以持有mutex,因此必须由mutex的持有者释放mutex
2.4.1、互斥体API
使用示例:
struct mutex lock; /* 定义一个互斥体 */
mutex_init(&lock); /* 初始化互斥体 */
mutex_lock(&lock); /* 上锁 */
/* 临界区 */
mutex_unlock(&lock); /* 解锁 */