最全Linux驱动开发八股文(八)

你好,我是拉依达。

这是我的Linux驱动开发八股文详细解析系列

本系列最开始是我在csdn上更新的文章,目前已经是csdn搜索“linux驱动”综合推荐第一名,累计阅读次数4w次。

全文总字数近8w字,是目前全网最全面,最清晰的入门linux驱动学习资料

现重新对内容进行整理,希望可以帮助到更多学习嵌入式的同学。

九、内核并发与竞争

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函数

alt

原子位操作 API 函数

alt

alt

  • 原子变量 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才可以访问共享资源。

由于等待自旋锁会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长,所以自旋锁适用于短时期的轻量级加锁

alt

alt

alt

  • 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 内核提供了信号量机制,信号量常常用于控制对共享资源的访问。它是一个计数器,常用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

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

alt

/* 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 不能递归上锁和解锁

alt

/* 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驱动开发八股文 文章被收录于专栏

你好,我是拉依达。 这是我的Linux驱动开发八股文详细解析系列。 本系列最开始是我在csdn上更新的文章,目前已经是csdn搜索“linux驱动”综合推荐第一名,累计阅读次数4w次。 全文总字数近8w字,是目前全网最全面,最清晰的入门linux驱动学习资料。 现在我重新对内容进行整理,已专栏的形式发布在牛客上,希望可以帮助到更多学习嵌入式的同学。

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务