行业资讯
📅 2026/8/12 15:39:41
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,tmp1); count tmp1; } return 0; } static int __init demo_init(void ) { struct task_struct * task1NULL; struct task_struct * task2NULL; // 创建线程 运行 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_yashotmail.com); MODULE_DESCRIPTION(thread test);二、原子操作atomic2.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变0int 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_yashotmail.com); MODULE_DESCRIPTION(thread test);三、自旋锁spinlock3.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)尝试获取指定的自旋锁如果没有获取到就返回 0int 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 *lock, 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 (youdomain.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_yashotmail.com); MODULE_DESCRIPTION(thread test);四、信号量semaphore4.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 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)释放信号量4.3 实现信号量/** * file th_demo.c * author your name (youdomain.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_yashotmail.com); MODULE_DESCRIPTION(thread test);五、互斥体mutex5.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 *lock)使用此函数获取信号量失败进入休眠以后可以被信号打断。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,tmp1); count tmp1; mutex_unlock(mymutex); } return 0; } static int __init demo_init(void ) { struct task_struct * task1NULL; struct task_struct * task2NULL; 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_yashotmail.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_yashotmail.com); MODULE_DESCRIPTION(thread test);七、差异特性自旋锁(Spinlock)信号量(Semaphore)互斥体(Mutex)完成量(Completion)核心机制忙等待(Busy-wait)睡眠等待(Sleep-wait)睡眠等待(Sleep-wait)事件通知(Event-based)能否睡眠绝对不能可以可以等待方可以睡眠上下文限制进程/中断/软中断/Tasklet仅限进程上下文仅限进程上下文进程/中断上下文是否可递归不可(死锁)可(计数信号量)不可(死锁)不涉及性能开销极小(无上下文切换)较大(有进程调度)较小(比信号量轻量)中等持有时间极短(微秒级)较长较长取决于事件发生时间典型应用场景中断处理、短临界区、多核间共享数据遗留代码、需要计数资源的场景进程间互斥访问共享资源等待异步操作完成