Linux驱动编程【竞争与并发】

一、什么是并发与竞争

        Linux系统是一个多任务操作系统,会存在多个任务同时访问一片内存区域,这些任务会互相覆盖这段内存中的数据,造成内存数据混乱,产生Linux系统并发产生的主要原因如下:

        ①、多线程并发访问

        ②、中断程序并发访问

        ③、多核(SMP)核间并发访问

        ④、抢占式并发访问

        并发访问带来的问题就是竞争,所谓的临界区就是共享数据段,对于临界区必须保证一次只有一个线程访问,也就是要保证临界区是原子访问的。

二、保护的是什么东西?

        防止并发访问共享资源,就是保护共享资源

三、原子操作

        原子操作就是指不能再进一步分割的操作,一般原子操作用于变量或者位操作

Linux驱动编程【竞争与并发】_第1张图片

        1、原子整形操作API函数

Linux驱动编程【竞争与并发】_第2张图片

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、线程与中断

Linux驱动编程【竞争与并发】_第3张图片

        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,中断中只能使用自旋锁。

        ②、和信号量一样, 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);

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