嵌入式Linux驱动开发(七)并发与竞争

1. linux并发与竞争概念

并发产生原因:
①多线程并发访问。
②抢占式并发访问。
③中断程序并发访问。
④SMP(多核)核间并发访问。存在于多核CPU之间。
**竞争:**多个线程同时操作临界区。

2. linux内核提供的处理方法

2.1 原子操作

Linux内核使用atomic_t结构体完成整形数据的原子操作。用原子变量替代整形变量。
原子操作只能对整型变量或位进行共享资源保护。

typedef struct {
	int counter;
} atomic_t;

/*使用*/
atomic_t a;    //定义原子变量a
atomic_t b = ATOMIC_INIT(0);   //定义原子变量b并赋初值0

/*内核提供了一些原子操作和原子位操作的API,见驱动手册
原子位操作直接对地址进行操作*/

2.2 自旋锁

如果自旋锁正在被线程 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或者多线程引起的并发,不针对中断并发。
嵌入式Linux驱动开发(七)并发与竞争_第1张图片
如果中断也要访问临界区,必须在中断中使用自旋锁之前,禁止掉本地中断(CPU中断),否则也会导致死锁。
嵌入式Linux驱动开发(七)并发与竞争_第2张图片
一般在线程中使用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) /* 释放锁 */
}

下半部(BH)也会竞争,BH中使用自旋锁的API:
在这里插入图片描述

2.3 其他类型锁

一般在内核中使用,了解一下即可。
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来编写驱动。

2.3 信号量

信号量概念见操作系统课程。
信号量特点:
①使得等待资源线程进入休眠,所以可以用于占用资源较久的场合。
②信号量会引起休眠,所以不可以用于中断。
③如果共享资源持有时间较短,不要用信号量,信号量频繁切换线程的开销挺大的。
分类:
1)计数型信号量:初始值大于1,不可以用于互斥访问,它允许多个线程同时访问共享资源。
2)二值信号量:互斥访问。
API:
内核使用semaphore结构体表示信号量。
嵌入式Linux驱动开发(七)并发与竞争_第3张图片

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); /* 释放信号量 */

2.4 互斥体mutex

内核使用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:
嵌入式Linux驱动开发(七)并发与竞争_第4张图片
使用:

struct mutex lock; /* 定义一个互斥体 */
mutex_init(&lock); /* 初始化互斥体 */
mutex_lock(&lock); /* 上锁 */
/* 临界区 */
mutex_unlock(&lock); /* 解锁 */

3. 原子操作示例

在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指定时间,这时候进行打断会被告知失败,原子操作有效。

4. 自旋锁示例

注意点:
由于自旋锁保护的临界区要尽可能短,所以在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);
	...
}

5. 信号量示例

信号量会导致休眠,所以保护的临界区没有时间限制,但是不可以用在中断中。

//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);

6. Mutex示例

不能递归申请,不能在中断中使用(可能导致休眠或阻塞)。

//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);

你可能感兴趣的:(驱动开发,linux,运维)