并发产生原因:
①多线程并发访问。
②抢占式并发访问。
③中断程序并发访问。
④SMP(多核)核间并发访问。存在于多核CPU之间。
**竞争:**多个线程同时操作临界区。
Linux内核使用atomic_t结构体完成整形数据的原子操作。用原子变量替代整形变量。
原子操作只能对整型变量或位进行共享资源保护。
typedef struct {
int counter;
} atomic_t;
/*使用*/
atomic_t a; //定义原子变量a
atomic_t b = ATOMIC_INIT(0); //定义原子变量b并赋初值0
/*内核提供了一些原子操作和原子位操作的API,见驱动手册
原子位操作直接对地址进行操作*/
如果自旋锁正在被线程 A 持有,线程 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;
相关API:
自旋锁会自动禁止抢占,所以被自旋锁保护的临界区不可以使用任何可能引起睡眠和阻塞的API,否则会导致死锁。
以下API适用于SMP或者多线程引起的并发,不针对中断并发。
如果中断也要访问临界区,必须在中断中使用自旋锁之前,禁止掉本地中断(CPU中断),否则也会导致死锁。
一般在线程中使用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) /* 释放锁 */
}
一般在内核中使用,了解一下即可。
1)读写自旋锁rwlock_t:
当某个数据结构符合读/写或生产者/消费者模型的时候就可以使用读写自旋锁。
typedef struct {
arch_rwlock_t raw_lock;
} rwlock_t;
2)顺序锁seqlock_t:
需要同时读写的时候可以使用。也就是写的时候也可以读。不能保护指针。
typedef struct {
struct seqcount seqcount;
spinlock_t lock;
} seqlock_t;
3)自旋锁使用注意事项:
①锁的持有时间不可以太久。
②自旋锁保护的临界区内不能使用可能引起睡眠阻塞的API。
③不能递归申请自旋锁。
④必须按照多核SOC来编写驱动。
信号量概念见操作系统课程。
信号量特点:
①使得等待资源线程进入休眠,所以可以用于占用资源较久的场合。
②信号量会引起休眠,所以不可以用于中断。
③如果共享资源持有时间较短,不要用信号量,信号量频繁切换线程的开销挺大的。
分类:
1)计数型信号量:初始值大于1,不可以用于互斥访问,它允许多个线程同时访问共享资源。
2)二值信号量:互斥访问。
API:
内核使用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); /* 释放信号量 */
内核使用mutex结构体表示互斥体。
struct mutex {
/* 1: unlocked, 0: locked, negative: locked, possible waiters */
atomic_t count;
spinlock_t wait_lock;
};
要点:
①用于实现互斥访问,不能递归申请互斥体。
②mutex可以导致休眠,所以不能在中断使用。
③和信号量一样,mutex保护的临界区可以调用引起阻塞的API。
④一次只有一个线程可以持有mutex,必须由mutex的持有者释放mutex。
API:
使用:
struct mutex lock; /* 定义一个互斥体 */
mutex_init(&lock); /* 初始化互斥体 */
mutex_lock(&lock); /* 上锁 */
/* 临界区 */
mutex_unlock(&lock); /* 解锁 */
在gpioled的基础上进行。
//1.在设备结构体中加入atomic_t lock
struct gpioled_dev{
...
atomic_t lock; /* 原子变量 */
};
//2.修改open函数
static int led_open(struct inode *inode, struct file *filp)
{
/* 通过判断原子变量的值来检查 LED 有没有被别的应用使用 */
if (!atomic_dec_and_test(&gpioled.lock)) {
atomic_inc(&gpioled.lock);/* 小于 0 的话就加 1,使其原子变量等于 0 */
return -EBUSY; /* LED 被使用,返回忙 */
}
filp->private_data = &gpioled; /* 设置私有数据 */
return 0;
}
//3.修改release
static int led_release(struct inode *inode, struct file *filp)
{
struct gpioled_dev *dev = filp->private_data;
/* 关闭驱动文件的时候释放原子变量 */
atomic_inc(&dev->lock);
return 0;
}
//4.修改init
static int __init led_init(void) {
int ret = 0;
/* 初始化原子变量=1*/
atomic_set(&gpioled.lock, 1);
...
}
修改atomicApp,在ledApp基础上添加一段模拟占用的代码,然后测试:
./atomicApp /dev/gpioled 1& //后台运行App,打开led
./atomicApp /dev/gpioled 0 //关闭 LED 灯
打开之后要运行App指定时间,这时候进行打断会被告知失败,原子操作有效。
注意点:
由于自旋锁保护的临界区要尽可能短,所以在open和release中申请释放自旋锁就不合适,转而使用一个变量表示设备占用情况。
原理:
定义变量dev_stats表示设备使用情况,为0的时候表示设备没有被使用,大于0的时候表示设备被使用。open函数中先判断dev_stats是否为0,也就是判断设备是否可用,如果为0就使用设备,且将dev_stats加1,表示设备被使用。使用完后在 release 函数中将dev_stats减1,表示设备没有被使用。自旋锁保护的是该变量。
//1.在设备结构体中添加自旋锁和设备状态变量,自旋锁对该变量进行保护
struct gpioled_dev {
...
int dev_status; /*设备状态,0,设备未使用;>0,设备已经被使用*/
spinlock_t lock; /*自旋锁*/
}
//2.open
static int led_open(struct inode *inode, struct file *filp)
{
unsigned long flags;
filp->private_data = &gpioled; /* 设置私有数据 */
spin_lock_irqsave(&gpioled.lock, flags); /* 上锁 */
if (gpioled.dev_stats) { /* 如果设备被使用了 */
spin_unlock_irqrestore(&gpioled.lock, flags); /* 解锁 */
return -EBUSY;
}
gpioled.dev_stats++; /* 如果设备没有打开,那么就标记已经打开了 */
spin_unlock_irqrestore(&gpioled.lock, flags); /* 解锁 */
return 0;
}
//3.release
static int led_release(struct inode *inode, struct file *filp)
{
unsigned long flags;
struct gpioled_dev *dev = filp->private_data;
/* 关闭驱动文件的时候将 dev_stats 减 1 */
spin_lock_irqsave(&dev->lock, flags); /* 上锁 */
if (dev->dev_stats) {
dev->dev_stats--;
}
spin_unlock_irqrestore(&dev->lock, flags); /* 解锁 */
return 0;
}
//4.初始化自旋锁
static int __init led_init(void) {
int ret = 0;
spin_lock_init(&gpioled.lock);
...
}
信号量会导致休眠,所以保护的临界区没有时间限制,但是不可以用在中断中。
//1.添加头文件
#include
//2.修改gpioled设备结构体
struct gpioled_dev {
...
struct semaphore sem;
};
//3.open
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &gpioled; /* 设置私有数据 */
/* 获取信号量,进入休眠状态的进程可以被信号打断 */
if (down_interruptible(&gpioled.sem)) {
return -ERESTARTSYS;
}
#if 0
down(&gpioled.sem); /* 不能被信号打断 */
#endif
return 0;
}
//4.release
static int led_release(struct inode *inode, struct file *filp)
{
struct gpioled_dev *dev = filp->private_data;
up(&dev->sem); /* 释放信号量,信号量值加 1 */
return 0;
}
//5.init
sema_init(&gpioled.sem, 1);
不能递归申请,不能在中断中使用(可能导致休眠或阻塞)。
//1.在设备结构体中添加互斥体
struct gpioled_dev {
...
struct mutex lock;
}
//2.open
static int led_open(struct inode *inode, struct file *filp) {
filp->private_data = &gpioled; /* 设置私有数据 */
/* 获取互斥体,可以被信号打断 */
if (mutex_lock_interruptible(&gpioled.lock)) {
return -ERESTARTSYS;
}
#if 0
mutex_lock(&gpioled.lock); /* 不能被信号打断 */
#endif
return 0;
}
//3.release
mutex_unlock(&dev->lock);
//4.init
mutex_init(&gpioled.lock);