最全Linux驱动开发八股文(八)
你好,我是拉依达。
这是我的Linux驱动开发八股文详细解析系列。
本系列最开始是我在csdn上更新的文章,目前已经是csdn搜索“linux驱动”综合推荐第一名,累计阅读次数4w次。
全文总字数近8w字,是目前全网最全面,最清晰的入门linux驱动学习资料。
现重新对内容进行整理,希望可以帮助到更多学习嵌入式的同学。
【下面是拉依达推荐学习相关专栏:】
一、Linux驱动学习专栏:拉依达的Linux驱动八股文 - 牛客网
二、Linux应用学习专栏:拉依达的Linux应用八股文 - 牛客网
【我的嵌入式学习和校招经验】 拉依达的嵌入式学习和秋招经验-CSDN博客
嵌入式学习规划/就业经验指导,可私信咨询
———————————————————————————————————————————————————
九、内核并发与竞争
9.1 并发与竞争概念
Linux 系统是个多任务操作系统,会存在多个任务同时访问同一片内存区域,这些任务可能会相互覆盖这段内存中的数据,造成内存数据混乱。我们需要对共享数据进行相应的保护处理。
-
并发:多个执行单元同时进行或多个执行单元微观串行执行,宏观并行执行。
- 多线程并发访问, Linux 是多任务(线程)的系统,多线程访问是最基本的原因
- 抢占式并发访问,Linux 内核支持抢占,调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。
- 中断程序并发访问,硬件中断的权利可以是很大的。
- SMP(多核)核间并发访问,多核 CPU 存在核间并发访问。
-
竞争:并发的执行单元对共享资源(硬件资源和软件上的全局变量)的访问而导致的竞争状态。
-
临界资源: 多个进程访问的资源,共享数据段
-
临界区:多个进程访问的代码段
9.2 原子操作
原子操作是指不能再进一步分割的操作。一般原子操作用于整形变量或者位操作。
Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量,此结构体定义在 include/linux/types.h 文件中
typedef struct {
int counter;
} atomic_t;
如果要使用原子操作 API 函数,首先要先定义一个 atomic_t 的变量,
atomic_t a; //定义 a
atomic_t b = ATOMIC_INIT(0); //定义原子变量 b 并赋初值为 0
原子操作API函数
原子位操作 API 函数
例
- 原子变量 lock,用来实现一次只能允许一个应用访问 LED 灯,led_init 驱动入口函数会将 lock 的值设置为 1。
- 每次打开驱动设备的时候先使用 atomic_dec_and_test 函数将 lock 减 1,如果 atomic_dec_and_test函数返回值为真就表示 lock 当前值为 0,说明设备可以使用
- 如果 atomic_dec_and_test 函数返回值为假,就表示 lock 当前值为负数(lock 值默认是 1),lock 值为负数的可能性只有一个,那就 是其他设备正在使用 LED,退出之前调用函数 atomic_inc 将 lock 加 1,因为此时 lock 的值被减成了负数,必须要对其加 1,将 lock 的值 变为 0。
/* gpioled设备结构体 */
struct gpioled_dev{
......
int led_gpio; /* led所使用的GPIO编号 */
atomic_t lock; /* 原子变量 */
};
struct gpioled_dev gpioled; /* led设备 */
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被使用,返回忙 */
}
......
}
static int led_release(struct inode *inode, struct file *filp)
{
struct gpioled_dev *dev = filp->private_data;
/* 关闭驱动文件的时候释放原子变量 */
atomic_inc(&dev->lock);
return 0;
}
static int __init led_init(void)
{
int ret = 0;
/* 初始化原子变量 */
atomic_set(&gpioled.lock, 1); /* 原子变量初始值为1 */
......
return 0;
}
static void __exit led_exit(void)
{
......
}
9.2 自旋锁
对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,直到线程A释放自旋锁,线程B才可以访问共享资源。
由于等待自旋锁会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长,所以自旋锁适用于短时期的轻量级加锁。
例
- dev_stats 表示设备状态,如果为 0 的话表示设备还没有被使用,如果大于 0 的话就表示设备已经被使用了。
- 调用 spin_lock_irqsave 函数获取锁,为了考虑到驱动兼容性,这里并没有使用 spin_lock 函数来获取锁。
- 判断dev_stats 是否大于 0,如果是的话表示设备已经被使用了,那么就调用 spin_unlock_irqrestore函数释放锁,并且返回-EBUSY。
- 如果设备没有被使用的话就在将 dev_stats 加 1,表示设备要被使用了,然后调用 spin_unlock_irqrestore 函数释放锁。
- 在 release 函数中将 dev_stats 减 1,表示设备被释放了
- 自旋锁的工作就是保护dev_stats 变量,真正实现对设备互斥访问的是 dev_stats。
/* gpioled设备结构体 */
struct gpioled_dev{
......
int dev_stats; /* 设备使用状态,0,设备未使用;>0,设备已经被使用 */
spinlock_t lock; /* 自旋锁 */
};
struct gpioled_dev gpioled; /* led设备 */
static int led_open(struct inode *inode, struct file *filp)
{
......
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;
}
static int led_release(struct inode *inode, struct file *filp)
{
......
/* 关闭驱动文件的时候将dev_stats减1 */
spin_lock_irqsave(&dev->lock, flags); /* 上锁 */
if (dev->dev_stats)
{
dev->dev_stats--;
}
spin_unlock_irqrestore(&dev->lock, flags);/* 解锁 */
return 0;
}
/*
* @description : 驱动入口函数
* @param : 无
* @return : 无
*/
static int __init led_init(void)
{
int ret = 0;
/* 初始化自旋锁 */
spin_lock_init(&gpioled.lock);
......
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit led_exit(void)
{
......
}
9.3 信号量
Linux 内核提供了信号量机制,信号量常常用于控制对共享资源的访问。它是一个计数器,常用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
- 信号量可以使等待资源线程进入休眠状态,适用于占用资源比较久的场合
- 信号量会引起休眠,中断不能休眠,所以信号量不能用于中断。
- 如果共享资源的持有时间比较短,不适合使用信号量,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的优势,此时使用自旋锁。
例
/* gpioled设备结构体 */
struct gpioled_dev{
......
struct semaphore sem; /* 信号量 */
};
struct gpioled_dev gpioled; /* led设备 */
static int led_open(struct inode *inode, struct file *filp)
{
......
/* 获取信号量 */
if (down_interruptible(&gpioled.sem)) { /* 获取信号量,进入休眠状态的进程可以被信号打断 */
return -ERESTARTSYS;
}
#if 0
down(&gpioled.sem); /* 不能被信号打断 */
#endif
return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
......
up(&dev->sem); /* 释放信号量,信号量值加1 */
return 0;
}
/* 设备操作函数 */
static struct file_operations gpioled_fops = {
......
};
static int __init led_init(void)
{
int ret = 0;
/* 初始化信号量 */
sema_init(&gpioled.sem, 1);
......
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit led_exit(void)
{
......
}
9.4 互斥体
Linux 提供了专门的互斥体mutex (等效信号量为1) 。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体(死锁)。在 Linux 驱动的时遇到需要互斥访问的地方一般使用 mutex。
- mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
- mutex 保护的临界区可以调用引起阻塞的 API 函数(信号量也可以)
- 因为一次只有一个线程可以持有 mutex,所以,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁
/* gpioled设备结构体 */
struct gpioled_dev{
......
struct mutex lock; /* 互斥体 */
};
struct gpioled_dev gpioled; /* led设备 */
static int led_open(struct inode *inode, struct file *filp)
{
......
/* 获取互斥体,可以被信号打断 */
if (mutex_lock_interruptible(&gpioled.lock)) {
return -ERESTARTSYS;
}
#if 0
mutex_lock(&gpioled.lock); /* 不能被信号打断 */
#endif
return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
......
/* 释放互斥锁 */
mutex_unlock(&dev->lock);
return 0;
}
/* 设备操作函数 */
static struct file_operations gpioled_fops = {
......
};
static int __init led_init(void)
{
......
/* 初始化互斥体 */
mutex_init(&gpioled.lock);
......
}
static void __exit led_exit(void)
{
......
}
#嵌入式##校招##八股文##Linux##linux驱动#你好,我是拉依达。 这是我的Linux驱动开发八股文详细解析系列。 本系列最开始是我在csdn上更新的文章,目前已经是csdn搜索“linux驱动”综合推荐第一名,累计阅读次数4w次。 全文总字数近8w字,是目前全网最全面,最清晰的入门linux驱动学习资料。 现在我重新对内容进行整理,已专栏的形式发布在牛客上,希望可以帮助到更多学习嵌入式的同学。