Linux系统是一个多任务操作系统,会存在多个任务同时访问一片内存区域,这些任务会互相覆盖这段内存中的数据,造成内存数据混乱,产生Linux系统并发产生的主要原因如下:
①、多线程并发访问
②、中断程序并发访问
③、多核(SMP)核间并发访问
④、抢占式并发访问
并发访问带来的问题就是竞争,所谓的临界区就是共享数据段,对于临界区必须保证一次只有一个线程访问,也就是要保证临界区是原子访问的。
防止并发访问共享资源,就是保护共享资源
原子操作就是指不能再进一步分割的操作,一般原子操作用于变量或者位操作
1、原子整形操作API函数
2、原子位操作API函数
void set_bit(int nr, void *p) |
将 p 地址的第 nr 位置 1。 |
void clear_bit(int nr,void *p) |
将 p 地址的第 nr 位清零。 |
void change_bit(int nr, void *p) |
将 p 地址的第 nr 位进行翻转。 |
int test_bit(int nr, void *p) |
获取 p 地址的第 nr 位的值。 |
int test_and_set_bit(int nr, void *p) |
将 p 地址的第 nr 位置 1,并且返回 nr 位原来的值。 |
int test_and_clear_bit(int nr, void *p) |
将 p 地址的第 nr 位清零,并且返回 nr 位原来的值。 |
int test_and_change_bit(int nr, void *p) |
将 p 地址的第 nr 位翻转,并且返回 nr 位原来的值。 |
3、实验操作
/* STEP:1 在设备结构体中定义 */
struct gpioled_dev{
dev_t devid;
int major;
int minor;
struct cdev cdev;
struct class *class;
struct device *device;
struct device_node *nd;
int led_gpio;
atomic_t lock;
};
/* STEP:2 初始化这把锁 */
atomic_set(&gpioled.lock, 1);
/* STEP:3 使用这把锁 */
/* if(atomic_read(&gpioled.lock) <= 0) {
/* return -EBUSY;
/* }
/* else {
/* atomic_dec(&gpioled.lock);
/* }*/
if(!(atomic_dec_and_test(&gpioled.lock))) {
atomic_inc(&gpioled.lock);
return -EBUSY;
}
/* STEP:4 释放这把锁 */
atomic_inc(&dev->lock);
原子操作只能对整形变量或者位进行保护,但是,在实际的使用环境中怎么可能只有整形变量或位这么简单的临界区。举个最简单的例子,设备结构体变量就不是整型变量,我们对于结构体中成员变量的操作也要保证原子性,在线程 A 对结构体变量使用期间,应该禁止其他的线程来访问此结构体变量,这些工作原子操作都不能胜任。
在 Linux内核中就是自旋锁。
当一个线程要访问某个共享资源的时候首先要先获取相应的锁, 锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里“转圈圈”的等待锁可用。
1、自旋锁API函数
函数 |
描述 |
DEFINE_SPINLOCK(spinlock_t lock) |
定义并初始化一个自选变量。 |
nt spin_lock_init(spinlock_t *lock) |
初始化自旋锁。 |
void spin_lock(spinlock_t *lock) |
获取指定的自旋锁,也叫做加锁。 |
void spin_unlock(spinlock_t *lock) |
释放指定的自旋锁。 |
nt spin_trylock(spinlock_t *lock) |
尝试获取指定的自旋锁,如果没有获取到就返回 0 |
nt spin_is_locked(spinlock_t *lock) |
检查指定的自旋锁是否被获取,如果没有被获取就返回非 0,否则返回 0。 |
2、线程与中断
3、线程与中断API函数
函数 |
描述 |
void spin_lock_irq(spinlock_t *lock) |
禁止本地中断,并获取自旋锁。 |
void spin_unlock_irq(spinlock_t *lock) |
激活本地中断,并释放自旋锁。 |
void spin_lock_irqsave(spinlock_t *lock,unsigned long flags) |
保存中断状态,禁止本地中断,并获取自旋锁。 |
void spin_unlock_irqrestore(spinlock_t*lock, unsigned long flags) |
将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁。 |
4、注意事项
我们需要在使用自旋锁的时候要注意一下几点:
①、因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式,比如稍后要讲的信号量和互斥体。
②、自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则的话可能导致死锁。
③、不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就必须“自旋”,等待锁被释放,然而你正处于“自旋”状态,根本没法释放锁。结果就是自己把自己锁死了!
④、在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管你用的是单核的还是多核的 SOC,都将其当做多核 SOC 来编写驱动程序
5、实验操作
/* STEP:1 在设备结构体中定义 */
struct gpioled_dev{
dev_t devid;
int major;
int minor;
struct cdev cdev;
struct class *class;
struct device *device;
struct device_node *nd;
int led_gpio;
int dev_status; /* 0代表可用 */
/* 1代表不可用 */
spinlock_t lock;
};
/* STEP:2 初始化这把锁 */
spin_lock_init(&gpioled.lock);
gpioled.dev_status = 0;
/* STEP:3 使用这把锁 */
unsigned long irqflag;
filp->private_data = &gpioled;
//spin_lock(&gpioled.lock);
spin_lock_irqsave(&gpioled.lock, irqflag);
if(gpioled.dev_status) { /* 不可用 */
spin_unlock(&gpioled.lock);
return -EBUSY;
}
gpioled.dev_status++;
//spin_unlock(&gpioled.lock);
spin_unlock_irqrestore(&gpioled.lock, irqflag);
/* STEP:4 释放这把锁 */
unsigned long irqflag;
struct gpioled_dev *dev = filp->private_data;
//spin_lock(&dev->lock);
spin_lock_irqsave(&dev->lock, irqflag);
if(dev->dev_status) {
dev->dev_status--;
}
//spin_unlock(&dev->lock);
spin_unlock_irqrestore(&dev->lock, irqflag);
Linux 内核也提供了信号量机制,信号量常常用于控制对共享资源的访问。与自旋锁最大的区别在于,当线程B在排队过程中,如果线程A使用完临界资源,使用信号量机制B线程会自动执行,如果使用自旋锁机制,则线程B不会执行。
相比于自旋锁,信号量可以使线程进入休眠状态
1、信号量API函数
函数 |
描述 |
void sema_init(struct semaphore *sem, int val) |
初始化信号量 sem,设置信号量值为 val。 |
void down(struct semaphore *sem) |
获取信号量,因为会导致休眠,因此不能在中断中使用。 |
int down_trylock(struct semaphore *sem); |
尝试获取信号量,如果能获取到信号量就获取,并且返回 0。如果不能就返回非 0,并且不会进入休眠。 |
int down_interruptible(struct semaphore *sem) |
获取信号量,和 down 类似,只是使用 down 进入休眠状态的线程不能被信号打断。而使用此函数进入休眠以后是可以被信号打断的。 |
void up(struct semaphore *sem) |
释放信号量 |
2、实验操作
/* STEP:1 在设备结构体中定义 */
struct gpioled_dev{
dev_t devid;
int major;
int minor;
struct cdev cdev;
struct class *class;
struct device *device;
struct device_node *nd;
int led_gpio;
struct semaphore sem;
};
/* STEP:2 初始化这把锁 */
sema_init(&gpioled.sem, 1);
/* STEP:1 使用这把锁 */
filp->private_data = &gpioled;
down(&gpioled.sem);
/* STEP:1 释放这把锁 */
up(&dev->sem);
互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体。
在使用 mutex 之前要先定义一个 mutex 变量。
在使用 mutex 的时候要注意如下几点:
①、 mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
②、和信号量一样, mutex 保护的临界区可以调用引起阻塞的 API 函数。
③、因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁。
1、互斥体 API 函数
函数 |
描述 |
DEFINE_MUTEX(name) |
定义并初始化一个 mutex 变量。 |
void mutex_init(mutex *lock) |
初始化mutex。 |
void mutex_lock(struct mutex *lock) |
获取 mutex,也就是给 mutex 上锁。如果获取不到就进休眠。 |
void mutex_unlock(struct mutex *lock) |
释放 mutex,也就给 mutex 解锁。 |
int mutex_trylock(struct mutex *lock) |
尝试获取 mutex,如果成功就返回 1,如果失败就返回 0。 |
int mutex_is_locked(struct mutex *lock) |
判断 mutex 是否被获取,如果是的话就返回1,否则返回 0。 |
int mutex_lock_interruptible(struct mutex *lock) |
使用此函数获取信号量失败进入休眠以后可以被信号打断。 |
2、实验操作
/* STEP:1 在设备结构体中定义 */
struct gpioled_dev{
dev_t devid;
int major;
int minor;
struct cdev cdev;
struct class *class;
struct device *device;
struct device_node *nd;
int led_gpio;
struct mutex lock;
};
/* STEP:2 初始化这把锁 */
mutex_init(&gpioled.lock);
/* STEP:3 使用这把锁 */
filp->private_data = &gpioled;
mutex_lock(&gpioled.lock);
/* STEP:4 释放这把锁 */
mutex_unlock(&gpioled.lock);