欢迎光临
我们一直在努力

Linux驱动07-Linux设备驱动中的并发与竞争

一、并发与竞争

① Linux是一个多任务操作系统,肯定会存在多个任务共同操作同一段内存或者设备的情况,多个任务甚至中断都能访问的资源叫做共享资源,在驱动开发中要注意对共享资源的保护,也就是要处理对共享资源的并发访问。
② 并发概念:

        并发(concurrency)是指多个执行单元同时或并行执行。当这些执行单元访问共享资源(包括硬件资源和软件中的全局变量、静态变量等)时,容易引发竞态条件(race conditions)。并发访问带来的问题就是竞态(竞争)。

③ 竞态产生原因:

  • 多CPU系统:SMP(核间并发访问)是一种紧耦合、共享存储的系统架构;多核 CPU 存在核间并发访问。
  • 进程调度:内核中执行的进程可能被更高优先级的进程抢占;
  • 中断机制:包括硬中断、软中断、Tasklet和底半部等,其中高优先级中断可以抢占低优先级中断,中断可以打断进程。

④ 竞态解决方案

        解决竞态问题的核心在于确保对共享资源的互斥访问——即当一个执行单元访问共享资源时,其他执行单元必须被禁止访问。

        共享资源访问的代码区域称为临界区(Critical Sections),需通过互斥机制进行保护。Linux设备驱动中常用的互斥手段包括:中断屏蔽、原子操作、自旋锁、信号量、互斥体。

⑤ 两线程进行操作

#include "asm-generic/current.h"
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/delay.h>
static int count = 0;
int th1(void *data)
{
int i =5000;
while(i–)
{
int tmp = count;
pr_info("name: %s count:%d\\n", current->comm,tmp+1);
count = tmp+1;
}
return 0;
}
static int __init demo_init(void )
{
struct task_struct * task1=NULL;
struct task_struct * task2=NULL;
// 创建线程 + 运行
task1 = kthread_run(th1, NULL ,"th_A");
task2 = kthread_run(th1, NULL ,"th_B");
msleep(5000); // 5s
kthread_stop(task1);
kthread_stop(task2);
pr_info("main th, count is %d\\n",count);
return 0;
}
static void __exit demo_exit(void)
{
pr_info("bye bye\\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("YAS rage_yas@hotmail.com");
MODULE_DESCRIPTION("thread test");

二、原子操作(atomic)

2.1 原子操作介绍

        原子操作能够确保对整型数据的修改具有排他性。Linux内核提供了一系列原子操作函数,主要分为针对位操作和整型变量操作两类。这些函数都基于底层CPU的原子指令实现,因此其具体实现与CPU架构密切相关。值得注意的是,这种排他性不仅适用于单核内部的并发场景,同样适用于多核之间的并发访问。

2.2 原子操作 API 函数

2.2.1 原子整形操作 API 函数

        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

函数 描述
ATOMIC_INIT(int i) 定义原子变量并初始化为给定值(例:atomic_t t = ATOMIC_INIT(2))
int atomic_read(atomic_t *v) 返回原子变量的当前值,读取 v 的值,并且返回。
void atomic_set(atomic_t *v, int i) 设置原子变量的值,向 v 写入 i 值。
void atomic_add(int i, atomic_t *v) 原子变量增加指定值i,给 v 加上 i 值。
void atomic_sub(int i, atomic_t *v) 原子变量减少指定值i,从 v 减去 i 值。
void atomic_inc(atomic_t *v) 原子变量值加1(无返回值),给 v 加 1,也就是自增。
void atomic_dec(atomic_t *v) 原子变量值减1(无返回值),从 v 减 1,也就是自减
int atomic_dec_return(atomic_t *v) 原子递减并返回新值,从 v 减 1,并且返回 v 的值。
int atomic_inc_return(atomic_t *v) 原子递增并返回新值,给 v 加 1,并且返回 v 的值。
int atomic_sub_and_test(int i, atomic_t *
v)
原子减少i后测试结果是否为0(返回true/false),从 v 减 i,如果结果为 0 就返回真,否则返回假
int atomic_dec_and_test(atomic_t *v) 原子递减后测试结果是否为0(返回true/false),从 v 减 1,如果结果为 0 就返回真,否则返回假
int atomic_inc_and_test(atomic_t *v) 原子递增后测试结果是否为0(返回true/false),给 v 加 1,如果结果为 0 就返回真,否则返回假
int atomic_add_negative(int i, atomic_t *
v)
给 v 加 i,如果结果为负就返回真,否则返回假

2.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 位进行翻转。翻转指定位状态(0变1或1变0)
int test_bit(int nr, void *p) 获取 p 地址的第 nr 位的值。
int test_and_set_bit(int nr, void *p) 将 p 地址的第 nr 位置 1,并且返回 nr 位原来的值。测试当前位值并设置其为1(返回原值)
int test_and_clear_bit(int nr, void *p) 将 p 地址的第 nr 位清零,并且返回 nr 位原来的值。测试当前位值并清除为0(返回原值)
int test_and_change_bit(int nr, void *
p)
将 p 地址的第 nr 位翻转,并且返回 nr 位原来的值。测试当前位值并翻转状态(返回原值)

2.3 实现原子操作

#include "asm-generic/current.h"
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <asm/atomic.h>
// static int count = 0;
atomic_t count = ATOMIC_INIT(0);
int th1(void *data)
{
int i = 5000;
while (i–)
{
int tmp = atomic_read(&count);
pr_info("name: %s count:%d\\n", current->comm, tmp + 1);
// atomic_add(1, &count);
atomic_inc(&count);
}
return 0;
}
static int __init demo_init(void)
{
struct task_struct *task1 = NULL;
struct task_struct *task2 = NULL;
int v = 0;
// 创建内核线程 + 运行,kthread_run:动态创建并立刻运行内核线程
task1 = kthread_run(th1, NULL, "th_A");
task2 = kthread_run(th1, NULL, "th_B");
msleep(5000); // 5s
kthread_stop(task1);
kthread_stop(task2);
v = atomic_read(&count);
pr_info("main th, count is %d\\n", v);
return 0;
}
static void __exit demo_exit(void)
{
pr_info("bye bye\\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("YAS rage_yas@hotmail.com");
MODULE_DESCRIPTION("thread test");

三、自旋锁(spinlock)

3.1 自旋锁介绍

① 自旋锁(spinlock)是一种经典的临界资源互斥访问机制。当运行在某CPU上的代码需要获取锁时,会先执行一个原子性的"测试并设置"(Test-And-Set)操作来检查内存变量。由于该操作的原子性,在执行期间其他执行单元无法访问该内存变量。若检测到锁处于空闲状态,程序即成功获取锁并继续执行;若锁已被占用,程序会在一个紧凑循环中持续进行"测试并设置"操作,这种特性正是"自旋锁"名称的由来。

② 理解自旋锁:当一个线程要访问某个共享资源的时候首先要先获取相应的锁, 锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里“转圈圈”的等待锁可用。自旋锁的“自旋”也就是“原地打转”的意思,“原地打转”的目的是为了等待自旋锁可以用,可以访问共享资源。
③ 自旋锁的主要缺点在于:等待获取锁的线程会持续处于自旋状态,这会占用处理器资源并降低系统性能。因此,自旋锁不适合长时间持有,仅适用于短期的轻量级加锁场景。对于需要长时间持有锁的情况,应考虑使用其他同步机制。

④  自旋锁主要适用于SMP系统或支持内核抢占的单CPU环境,在单CPU且不支持内核抢占的系统中,自旋锁会自动退化为空操作。

⑤ 自旋锁API函数适用于SMP系统或支持抢占的单CPU环境下的线程间并发访问控制,专用于线程间的同步保护。需要注意的是,被自旋锁保护的临界区内严禁调用任何可能引发睡眠或阻塞的API函数,否则极易导致死锁

        其工作原理是:当线程A获取自旋锁后,系统会自动禁用内核抢占。若线程A在持有锁期间进入休眠状态,将自动放弃CPU使用权。此时若线程B尝试获取该锁,由于锁仍被线程A持有且内核抢占处于禁用状态,线程B将无法被调度出去,而线程A也无法获得CPU时间释放锁,最终形成死锁局面。

在中断处理中使用自旋锁时,必须在获取锁之前先禁止本地中断(即当前CPU的中断,对于多核SOC则涉及各个CPU核心),否则可能导致死锁。
        其工作原理是:线程A先运行并获取了lock锁,当线程A执行functionA函数时发生中断,中断抢占了CPU。此时中断服务函数也尝试获取lock锁,但由于锁已被线程A持有,中断会持续自旋等待锁释放。然而,在中断处理完成之前,线程A无法恢复执行,从而形成死锁——线程A等待中断释放CPU,中断等待线程A释放锁,两者相互阻塞,导致系统卡死。

⑦ 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;

在使用自旋锁之前,肯定要先定义一个自旋锁变量,定义好自旋锁变量以后就可以使用相应的 API 函数来操作自旋锁,定义方法如下所示:
 

spinlock_t lock; //定义自旋锁

3.2 自旋锁相关API函数

3.2.1 最基本的自旋锁 API 函数

函数 描述
DEFINE_SPINLOCK(spinlock_t lock) 定义并初始化一个自选变量。
int spin_lock_init(spinlock_t *lock) 初始化自旋锁。
void spin_lock(spinlock_t *lock) 获取指定的自旋锁,也叫做加锁。
void spin_unlock(spinlock_t *lock) 释放指定的自旋锁。
int spin_trylock(spinlock_t *lock) 尝试获取指定的自旋锁,如果没有获取到就返回 0
int spin_is_locked(spinlock_t *lock) 检查指定的自旋锁是否被获取,如果没有被获取就
返回非 0,否则返回 0。

3.2.2 中断相关的自旋锁 API 函数

函数 描述
void spin_lock_irq(spinlock_t *lock) 禁止本地中断,并获取自旋锁。
void spin_unlock_irq(spinlock_t *lock) 激活本地中断,并释放自旋锁。
void spin_lock_irqsave(spinlock_t *loc
k, unsigned long flags)
保存中断状态,禁止本地中断,并获取自旋锁。
void spin_unlock_irqrestore(spinlock_t
*lock, unsigned long flags)
将中断状态恢复到以前的状态,并且激活本地中断,
释放自旋锁

        使用 spin_lock_irq/spin_unlock_irq 时,需要用户明确加锁前的中断状态。但由于内核规模庞大且运行状态复杂多变,很难准确判断中断状态,因此不建议使用这组函数。推荐改用 spin_lock_irqsave/spin_unlock_irqrestore,它们会自动保存和恢复中断状态。通常建议:在线程中使用 spin_lock_irqsave/spin_unlock_irqrestore,而在中断处理程序中则使用 spin_lock/spin_unlock。

3.3 实现自旋锁

/**
* @file th_demo.c
* @author your name (you@domain.com)
* @brief 自旋锁的示例 ,
特征: 忙等待 ,如果资源申请不到, 就会原地打转, 不会放弃cpu的调度。阻塞的时间 不应太久。
可以用于中断中, spin_lock_irqsave(关闭本cpu的中断处理), spin_unlock_irqrestore(恢复本cpu的中断处理)
* @version 0.1
* @date 2026-08-10
* @copyright Copyright (c) 2026
*/
#include "asm-generic/current.h"
#include "linux/mutex.h"
#include "linux/spinlock.h"
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/delay.h>
static int count = 0;
static spinlock_t my_spin_lock;
int th1(void *data)
{
int i = 5000;
int tmp = 0;
while (i–)
{
spin_lock(&my_spin_lock);
tmp = count;
pr_info("name: %s count:%d\\n", current->comm, tmp + 1);
count = tmp + 1;
spin_unlock(&my_spin_lock);
}
return 0;
}
static int __init demo_init(void)
{
struct task_struct *task1 = NULL;
struct task_struct *task2 = NULL;

// mutex_init(&mymutex);
spin_lock_init(&my_spin_lock);
// 创建线程 + 运行
task1 = kthread_run(th1, NULL, "th_A");
task2 = kthread_run(th1, NULL, "th_B");
msleep(5000); // 5s
kthread_stop(task1);
kthread_stop(task2);
pr_info("main th, count is %d\\n", count);
return 0;
}
static void __exit demo_exit(void)
{
pr_info("bye bye\\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("YAS rage_yas@hotmail.com");
MODULE_DESCRIPTION("thread test");

四、信号量(semaphore)

4.1 信号量介绍

① 信号量是操作系统中最经典的用于同步和互斥的方法,信号量与操作系统中的经典概念PV
操作对应。信号量是同步的一种方式。 Linux 内核也提供了信号量机制,信号量常常用于控制对共享资源的访问。相比于自旋锁,信号量可以使线程进入休眠状态,但是信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态以后会切换线程,切换线程就会有开销。

② 计数型信号量不适合用于互斥访问,因为它允许多个线程同时访问共享资源。要实现互斥访问,信号量的值必须限制在不超过1,这种信号量就是二值信号量。

③ 信号量的特点:

  • 因为信号量可以使等待资源线程进入休眠状态,因此适用于资源占用时间较长的场景。
  • 信号量不能用于中断处理中,因为信号量会引起休眠,中断不能休眠。
  • 不适用于资源持有时间短的场景,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势,会显著降低性能。

4.2 信号量相关API函数

Linux 内核使用 semaphore 结构体表示信号量,结构体内容如下所示:
 

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

函数 描述
DEFINE_SEAMPHORE(name) 定义一个信号量,并且设置信号量的值为 1。
void sema_init(struct semaphore *sem, int v
al)
初始化信号量 sem,设置信号量值为 val。
void down(struct semaphore *sem) 获取信号量,因为会导致休眠,因此不能在中
断中使用。
int down_trylock(struct semaphore *sem); 尝试获取信号量,如果能获取到信号量就获
取,并且返回 0。如果不能就返回非 0,并且
不会进入休眠。
int down_interruptible(struct semaphore *se
m)
获取信号量,和 down 类似,只是使用 down 进
入休眠状态的线程不能被信号打断。而使用此
函数进入休眠以后是可以被信号打断的。
void up(struct semaphore *sem) 释放信号量

4.3 实现信号量

/**
* @file th_demo.c
* @author your name (you@domain.com)
* @brief 关于信号量 的示例。
1. 可以当作互斥锁
2. 计数信号量
* @version 0.1
* @date 2026-08-10
* @copyright Copyright (c) 2026
*/
#include "asm-generic/current.h"
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/semaphore.h>
static int count = 0;
struct semaphore sem_count;
int th1(void *data)
{
int ret = 0;
int i = 5000;
int tmp = 0;
while (i–)
{
ret = down_interruptible(&sem_count);
tmp = count;
pr_info("name: %s count:%d\\n", current->comm, tmp + 1);
count = tmp + 1;
up(&sem_count);
}
return 0;
}
static int __init demo_init(void)
{
struct task_struct *task1 = NULL;
struct task_struct *task2 = NULL;
sema_init(&sem_count, 1);
// 创建线程 + 运行
task1 = kthread_run(th1, NULL, "th_A");
task2 = kthread_run(th1, NULL, "th_B");
//msleep(4000); // 5s
kthread_stop(task1);
kthread_stop(task2);
pr_info("main th, count is %d\\n", count);
return 0;
}
static void __exit demo_exit(void)
{
pr_info("bye bye\\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("YAS rage_yas@hotmail.com");
MODULE_DESCRIPTION("thread test");

五、互斥体(mutex)

5.1 互斥体介绍

① 自旋锁和互斥锁都是实现互斥访问的基本机制。互斥锁基于自旋锁实现,属于进程级同步机制,适用于多进程间的资源互斥访问。当竞争失败时,互斥锁会触发进程上下文切换,使当前进程进入睡眠状态并调度其他进程执行。由于进程切换开销较大,互斥锁更适用于长时间占用资源的场景。需要注意的是,互斥访问仅允许单个线程访问共享资源,且不支持递归申请。

② 在使用mutex时,需遵循以下规范:

  • 由于mutex可能导致休眠,禁止在中断上下文中使用(中断环境应选择自旋锁)
  • mutex保护的临界区内允许调用可能引发阻塞的API函数
  • mutex必须由持有者释放,且不支持递归加锁和解锁

使用前需先声明mutex变量,并确保满足上述使用条件。
③ Linux 内核使用 mutex 结构体表示互斥体,定义如下:

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

5.2 互斥体相关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 *loc
k)
使用此函数获取信号量失败进入休眠以后可
以被信号打断。

5.3 实现互斥体

#include "asm-generic/current.h"
#include "linux/mutex.h"
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/delay.h>
static int count = 0;
static struct mutex mymutex;
int th1(void *data)
{
int i =5000;
while(i–)
{
mutex_lock(&mymutex);
int tmp = count;
pr_info("name: %s count:%d\\n", current->comm,tmp+1);
count = tmp+1;
mutex_unlock(&mymutex);
}
return 0;
}
static int __init demo_init(void )
{
struct task_struct * task1=NULL;
struct task_struct * task2=NULL;
mutex_init(&mymutex);
// 创建线程 + 运行
task1 = kthread_run(th1, NULL ,"th_A");
task2 = kthread_run(th1, NULL ,"th_B");
msleep(5000); // 5s
kthread_stop(task1);
kthread_stop(task2);
pr_info("main th, count is %d\\n",count);
return 0;
}
static void __exit demo_exit(void)
{
pr_info("bye bye\\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("YAS rage_yas@hotmail.com");
MODULE_DESCRIPTION("thread test");

六、完成量 (Completion)

① 完成量是Linux内核提供的一种高效同步机制,是对信号量的补充,用于实现内核中执行单元(线程/进程)之间的同步通知。

② 核心作用:让一个执行单元等待另一个执行单元完成某一操作后再继续执行,避免任务间的竞争冲突;

③ 实现原理:基于等待队列实现,等待操作的进程会进入睡眠状态(非忙等待),不会占用CPU资源;

④ 核心函数接口:

函数/宏定义功能描述
struct completion my_completion 声明一个完成量(completion)变量
init_completion(&my_completion) 初始化完成量(非安全版本,需确保未被使用)
reinit_completion(&my_completion) 重新初始化完成量(安全版本,可在已使用的完成量上重置)
wait_for_completion(struct completion *c) 阻塞当前线程,直到完成量被触发(不可中断的等待)
complete(struct completion *c) 唤醒一个等待该完成量的线程(单次触发)
complete_all(struct completion *c) 唤醒所有等待该完成量的线程(批量触发,完成后需用 reinit_completion 重置)

⑤ 实现完成量

#include "asm-generic/current.h"
#include "linux/completion.h"
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/delay.h>
//定义完成量
struct completion com_hello;
struct completion com_world;
int th1(void *data)
{
int i = 10;
while (i–)
{
wait_for_completion(&com_hello);
pr_info("hello,");
msleep(100);
complete(&com_world);
}
return 0;
}
int th2(void *data)
{
int i = 10;
while (i–)
{
wait_for_completion(&com_world);
pr_info("world\\n");
complete(&com_hello);
msleep(900);
}
return 0;
}
static int __init demo_init(void)
{
struct task_struct *task1 = NULL;
struct task_struct *task2 = NULL;
//初始化
init_completion(&com_hello);
init_completion(&com_world);
// 为了线程1 可以先动起来。
complete(&com_hello);
// 创建线程 + 运行
task1 = kthread_run(th1, NULL, "th_A");
task2 = kthread_run(th2, NULL, "th_B");
// msleep(5000); // 5s
kthread_stop(task1);
kthread_stop(task2);
return 0;
}
static void __exit demo_exit(void)
{
pr_info("bye bye\\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("YAS rage_yas@hotmail.com");
MODULE_DESCRIPTION("thread test");

七、差异

特性自旋锁(Spinlock)信号量(Semaphore)互斥体(Mutex)完成量(Completion)
核心机制 忙等待(Busy-wait) 睡眠等待(Sleep-wait) 睡眠等待(Sleep-wait) 事件通知(Event-based)
能否睡眠 绝对不能 可以 可以 等待方可以睡眠
上下文限制 进程/中断/软中断/Tasklet 仅限进程上下文 仅限进程上下文 进程/中断上下文
是否可递归 不可(死锁) 可(计数信号量) 不可(死锁) 不涉及
性能开销 极小(无上下文切换) 较大(有进程调度) 较小(比信号量轻量) 中等
持有时间 极短(微秒级) 较长 较长 取决于事件发生时间
典型应用场景 中断处理、短临界区、多核间共享数据 遗留代码、需要计数资源的场景 进程间互斥访问共享资源 等待异步操作完成

赞(0)
未经允许不得转载:171主机测评 » Linux驱动07-Linux设备驱动中的并发与竞争
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址