linux系统-运行规则知识储备

在驱动开发中要注意对共享资源的保护,也就是要处理对共享资源的并发访问,在 Linux 驱动编写过程中对于并发控制的管理非常重要。

并发与竞争

并发与竞争简介

思考问题:
小李和小王要同时使用这一台打印机,都要打印一份文件。—并发
都争着要打印,结果打印了一份儿混乱的文件。—竞争

Linux 系统也是如此,Linux 系统是个多任务操作系统
会存在多个任务同时访问同一片内存区域,—并发
可能会相互覆盖这段内存中的数据,造成内存数据混乱。—竞争

针对这个问题必须要做处理,严重的话可能会导致系统崩溃。有以下几种
①、多线程并发访问, Linux 是多任务(线程)的系统,所以多线程访问是最基本的原因。
②、抢占式并发访问,从 2.6 版本内核开始, Linux 内核支持抢占,也就是说调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。
③、中断程序并发访问,这个无需多说,学过 STM32 的同学应该知道,硬件中断的权利可是很大的。
④、 SMP(多核)核间并发访问,现在 ARM 架构的多核 SOC 很常见,多核 CPU 存在核间并发访问。


临界区:所谓的临界区就是共享数据段,对于临界区必须保证一次只有一个线程访问,也就是要保证临界区是原子访问的。我们都知道,原子是化学反应不可再分的基本微粒,这里的原子访问就表示这一个访问是一个步骤,不能再进行拆分。如果多个线程同时操作临界区就表示存在竞争,我们在编写驱动的时候一定要注意避免并发防止竞争访问。


保护内容是什么

是保护共享资源,防止进行并发访问,保护的不是代码,而是数据多个线程都会访问的共享数据那个数据呀
1、全局变量,而不是某个线程的局部变量。
2、打印的文档也是数据。
3、驱动程序中设备结构体

原子操作

原子操作简介

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

for example:
一个线程给一个变量赋值:

a=3
||汇编后
\/
ldr r0, =0X30000000 /* 变量 a 地址 */
ldr r1, = 3 /* 要写入的值 */
str r1, [r0] /* 将 3 写入到 a 变量中 */

那么,线程A和线程B同时对一个变量a赋值:
理想情况:
在这里插入图片描述
实际上可能的情况:
在这里插入图片描述
完蛋了!两个并发的产生竞争了

这就需要将三行的汇编操作看作是一组原子操作,要执行一块儿执行。

这样的功能当然有linux内核的API函数承担啦。

原子整型操作 API 函数

普通C语言中:定义变量int a,可进行加减操作a++,a–。

linux驱动中的原子整型:定义变量使用了新的结构体,和普通C语言中类似,都是基本的加减操作,不过就是添加了原子整型操作,变得更安全了

Cortex-A7 是 32 位的架构,所以以下都是32位的。

typedef struct {
	int counter;
} atomic_t;

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

linux内核生活中常用也提供了大量的API原子操作函数修改变量的值:
在这里插入图片描述

原子位操作 API 函数

Linux 内核也提供了一系列的原子位操作 API 函数,原子位操作是直接对内存进行操作,API 函数如表:
在这里插入图片描述
在这里插入图片描述

自旋锁

自旋锁简介

除了变量等数据需要保护,还有设备结构体变量需要被保护,这就需要自旋锁出马啦。。。

as the saying gose,“占着茅坑不拉屎,多个进程要进行什么操作必须一个一个来”。

for example steps
1、一个线程A正在使用设备结构体变量dev_struct
2、结构体变量处于自旋锁状态,只对线程A开放;
3、另一个线程B想要访问结构体变量,自己只能在原地打转;
4、等到线程A用完了,线程B才能使用。

自旋锁适用于短时期的轻量级加锁,如果遇到需要长时间持有锁的场景那就需要换其他的方法了。

自旋锁 API 函数

同样,linux内核中自旋锁的操作也有提供API函数,并使用了新的结构体来存设备结构体变量。

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; //定义自旋锁

linux内核生活中常用也提供了一部分的API自旋操作函数修改变量的值:
在这里插入图片描述

自旋锁API函数适用于SMP或支持抢占的单CPU下线程之间的并发访问,也就是用于线程与线程之间。


将会遇到的问题:

1、被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API 函数,否则的话会可能会导致死锁现象的发生。(线程A正用着变量呢,突然被睡眠了,那么这个自旋锁除了睡着的线程A都无法访问)

2、使用自旋锁的时候,一个线程在获取锁之前一定要先禁止本地中断(也就是本 CPU 中断,对于多核 SOC来说会有多个 CPU 核)。
听一个小故事:中断随时都有可能发生且中断也能获取锁,当一个弱小的线程A刚获得了一个自旋锁,还没捂热乎呢,就被中断夺走了CPU的使用权,中断也想获得自旋锁,只能一直原地打转,这样,线程A不放自旋锁,中断不给线程A运行权,两败俱伤,死锁。

在这里插入图片描述
最好的解决方法就是获取锁之前关闭本地中断, Linux 内核提供了相应的 API 函数:
在这里插入图片描述
各有特点
1、spin_lock_irq/spin_unlock_irq,使用它们的时候需要用户能够确定加锁之前的中断状态,中断状态不好把控。
2、spin_lock_irqsave/ spin_unlock_irqrestore,因为这一组函
数会保存中断状态,在释放锁的时候会恢复中断状态,从而保障了系统更稳定的运行

如何使用以上API函数(一般情况):
在线程中使用 spin_lock_irqsave/spin_unlock_irqrestore
在中断中使用 spin_lock/spin_unlock


莫名其妙出现了下半部(BH)(或底半部)这个名词,姑且认为也是一种运行状态,那么,在这里边儿使用自旋锁,该如何呢?
同样是使用API函数啦!
在这里插入图片描述


读写自旋锁

从字面意思理解,就是有读自旋锁写自旋锁

for instant
一个包含用户信息的数据,是需要被保护的,多个线程对其访问时,需满足:
1、只能一个线程对其持有写自旋锁。
2、可以多个线程对其持有读自旋锁,可并发读取。
3、以上读写锁不能同时进行

Linux 内核使用 rwlock_t 结构体表示读写锁:

typedef struct {
	arch_rwlock_t raw_lock;
} rwlock_t;

读写锁操作 API 函数
读锁API函数写锁API函数
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

顺序锁

顺序锁在读写锁的基础上衍生而来的,
for instant ,满足:
1、只能一个线程对其持有写自旋锁。
2、可以多个线程对其持有读自旋锁,可并发读取。
3、可同时进行读写操作

注意事项
1、读的过程中发生了写操作,最好重新进行读取,保证数据完整性。
2、顺序锁保护的资源不能是指针,如果在写操作的时候可能会导致指针无效,这个时候恰巧有读操作访问指针的话就可能导致意外发生,比如读取野指针导致系统崩溃。

Linux 内核使用 seqlock_t 结构体表示顺序锁:

typedef struct {
	struct seqcount seqcount;
	spinlock_t lock;			//这里就是普通自旋锁
} seqlock_t;

顺序锁的 API 函数
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

自旋锁使用注意事项

①、因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式,比如稍后要讲的信号量和互斥体。
②、自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则的话可能导致死锁(占着茅坑不拉屎!)。
③、不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就必须“自旋”,等待锁被释放,然而你正处于“自旋”状态,根本没法释放锁。结果就是自己把自己锁死了!
④、在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管你用的是单核的还是多核的 SOC,都将其当做多核 SOC 来编写驱动程序

信号量

信号量简介

信号量类似于自旋锁,他们的不同是,当线程A在使用资源时,且信号量值为1:
自旋锁中线程B在等线程A时自旋;
信号量中线程B不等待,自己去休眠,等线程A好了在叫它。
特别的,当信号量值为N时,同时可允许的线程数量为N。

信号量的特点:
①、因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。
②、因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
③、如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换
线程引起的开销要远大于信号量带来的那点优势。

Linux 内核使用 semaphore 结构体表示信号量:

struct semaphore {
	raw_spinlock_t lock;
	unsigned int count;
	struct list_head wait_list;
};

信号量 API 函数

有关信号量的 API 函数:
在这里插入图片描述
使用示例:

struct semaphore sem; /* 定义信号量 */
sema_init(&sem, 1)/* 初始化信号量 */
down(&sem); /* 申请信号量 */
/* 临界区 */
up(&sem); /* 释放信号量 */

互斥体

互斥体简介

互斥体就是信号量的值为1的情况,即共享数据一次只能有一个线程访问。

Linux 内核使用 mutex 结构体表示互斥体:

struct mutex {
	/* 1: unlocked, 0: locked, negative: locked, possible waiters */
	atomic_t count;
	spinlock_t wait_lock;
};

使用 mutex 的时候要注意如下几点
①、 mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
②、和信号量一样, mutex 保护的临界区可以调用引起阻塞的 API 函数。
③、因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁。

互斥体 API 函数

有关互斥体的 API 函数:
在这里插入图片描述
互斥体的使用如下所示:

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

实验

咱们简单见识一下应用,不完整驱动程序

基本流程

  • 在设备结构体中定义一个原子变量。
  • 驱动入口函数初始化为1。
  • 一个进程A打开此驱动文件时,原子变量1-1=0,并判断能获得进入权限。
  • 另一个进程B再次打开此驱动文件,原子变量0-1=-1,判断无法获得进入权限,赶紧给第一个进程A原子变量-1+1=0,完好无损地放回去。
  • 等进程A用完这个驱动程序,原子变量0+1=1,还原为初始状态。
  • 如此,实现了设备的互斥访问

后边儿的自旋锁也是这么个意思,暂时不做验证。

/* gpioled设备结构体 添加一个原子变量*/
struct gpioled_dev{
	dev_t devid;			/* 设备号 	 */
	......
	atomic_t locked;		/*原子变量 */
};

/*初始化原子变量 */
static int __init led_init(void)
{
	......
	atomic_set(&gpioled.lock, 1); /* 原子变量初始值为 1 */
}

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;
}
static int led_release(struct inode *inode, struct file *filp)
{
	/* 关闭驱动文件的时候释放原子变量 */
	atomic_inc(&dev->lock);
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值