文章目录[toc]同步互斥一、自旋锁spinlock基本原理定义和初始化使用方式4个变体变体1spin_lock / spin_unlock最基本变体2spin_lock_irqsave / spin_unlock_irqrestore最常用变体3spin_lock_irq / spin_unlock_irq变体4spin_lock_bh / spin_unlock_bh自旋锁的使用规则二、互斥锁mutex基本原理定义和初始化使用方式基本用法mutex_lock / mutex_unlock可中断版本mutex_lock_interruptible尝试获取mutex_trylock互斥锁的使用规则什么场景用mutex三、信号量semaphore基本原理定义和初始化使用方式信号量的使用规则信号量的典型场景四、原子操作atomic基本原理原子整数操作原子位操作原子操作的适用场景原子操作的局限五、其他机制读写自旋锁rwlock / rwsemcompletion完成量RCURead-Copy-Update六、死锁deadlock场景1自死锁同一个锁获取两次场景2ABBA死锁两个锁交叉获取场景3进程上下文和中断上下文死锁场景4持spinlock时睡眠同步互斥Linux内核里有多个执行路径可能同时访问同一个数据多CPU并发CPU0在跑一个函数CPU1同时在跑一个中断处理函数两个都在操作UCR1寄存器进程与中断并发代码正在执行硬件中断来了CPU暂停当前代码去执行中断处理函数中断处理函数里也访问同样的数据进程与进程并发两个用户程序同时操作同一个串口设备当两个执行路径同时读-改-写同一个变量或寄存器时就会出现竞态条件。经典例子假设count当前值是5 路径A读count得到5→ 加1 → 写回count写入6 路径B读count得到5→ 加1 → 写回count写入6 结果两个路径各加了1但count只变成了6而不是7同步互斥机制就是确保读-改-写这种操作不被打断同一时刻只有一个路径在操作共享资源。一、自旋锁spinlock基本原理自旋锁是一个锁变量。当路径A获取lock了这个锁路径B也想获取时B不会睡眠而是在原地不停循环检查自旋/spinning锁是否被释放了。一旦A释放unlock了锁B立刻获取到继续执行。定义和初始化静态定义全局变量或结构体成员编译时就定好/* 方式一定义并初始化为一体 */staticDEFINE_SPINLOCK(my_lock);/* 方式二先定义再初始化用于结构体成员 */structmy_device{spinlock_tlock;intdata;};/* 在probe或init函数里初始化 */spin_lock_init(dev-lock);使用方式4个变体变体1spin_lock / spin_unlock最基本spin_lock(my_lock);/* 临界区操作共享资源 */spin_unlock(my_lock);只保护多CPU之间的并发。如果路径A持锁时被本CPU的中断打断中断处理函数也要获取同一个锁——死锁。适用场景共享资源只在进程上下文之间竞争不涉及中断。实际驱动开发中很少单独用这个。变体2spin_lock_irqsave / spin_unlock_irqrestore最常用unsignedlongflags;spin_lock_irqsave(my_lock,flags);/* 临界区操作共享资源 */spin_unlock_irqrestore(my_lock,flags);获取锁的同时关闭本CPU中断并保存中断状态到flags释放锁时恢复中断状态。为什么要保存/恢复而不是简单开关举个例子voidfunc_a(void){spin_lock_irqsave(lock,flags);/* 假设进来之前中断是开的flags记录开 */func_b();spin_unlock_irqrestore(lock,flags);/* 恢复成开 */}voidfunc_b(void){spin_lock_irqsave(lock2,flags2);/* 进来之前中断已经被func_a关了flags2记录关 *//* ... */spin_unlock_irqrestore(lock2,flags2);/* 恢复成关不会意外开中断 */}如果func_b里用的是spin_lock_irq/spin_unlock_irq无条件关/开中断那func_b结束时会无条件开中断但func_a还没释放锁呢中断就进来了可能死锁。所以irqsave/irqrestore是最安全的。适用场景共享资源在进程上下文和中断上下文之间竞争。你的UART驱动就是这个场景。这是驱动开发中最常用的自旋锁变体。变体3spin_lock_irq / spin_unlock_irqspin_lock_irq(my_lock);/* 获取锁 无条件关中断 *//* 临界区 */spin_unlock_irq(my_lock);/* 释放锁 无条件开中断 */跟irqsave的区别不保存中断状态释放时无条件开中断。只有确定调用这段代码之前中断一定是开的时候才能用。在不确定的情况下用irqsave更安全。变体4spin_lock_bh / spin_unlock_bhspin_lock_bh(my_lock);/* 获取锁 关软中断bottom half *//* 临界区 */spin_unlock_bh(my_lock);/* 释放锁 开软中断 */bh是bottom half软中断/tasklet的意思。只关闭软中断不关硬中断。适用场景共享资源在进程上下文和软中断之间竞争比如网络驱动里经常用。自旋锁的使用规则持锁期间绝对不能睡眠。不能调用任何可能睡眠的函数kmalloc(GFP_KERNEL)、msleep、copy_from_user、mutex_lock等等。因为睡眠意味着CPU切换去跑其他进程而其他进程可能也要获取这个锁→死锁临界区要尽可能短。因为其他路径在busy-wait空转等浪费CPU时间如果共享资源涉及中断上下文必须用irqsave变体。否则会死锁同一个锁不能嵌套获取。路径A持有锁L再次spin_lock(L)→死锁二、互斥锁mutex基本原理互斥锁跟自旋锁的目的一样——保证同一时刻只有一个路径操作共享资源。区别是等待方式拿不到锁时不是忙等而是睡眠。当前进程被放进等待队列CPU去执行其他进程。锁释放后等待的进程被唤醒。定义和初始化/* 方式一静态定义并初始化 */staticDEFINE_MUTEX(my_mutex);/* 方式二动态初始化结构体成员 */structmy_device{structmutexlock;char*buffer;};mutex_init(dev-lock);使用方式基本用法mutex_lock / mutex_unlockmutex_lock(my_mutex);/* 临界区可以做耗时操作可以睡眠 */mutex_unlock(my_mutex);如果锁已被持有mutex_lock会让当前进程睡眠等待直到锁被释放。可中断版本mutex_lock_interruptibleintretmutex_lock_interruptible(my_mutex);if(ret){/* 被信号打断了没拿到锁返回-ERESTARTSYS */returnret;}/* 拿到了锁 */mutex_unlock(my_mutex);普通mutex_lock在等待时即使用户按CtrlC也不会响应。mutex_lock_interruptible可以被信号打断更适合用户态可能长时间等待的场景。尝试获取mutex_trylockif(mutex_trylock(my_mutex)){/* 拿到了锁 */mutex_unlock(my_mutex);}else{/* 没拿到但不等待立刻返回做其他事 */}互斥锁的使用规则只能在进程上下文使用绝不能在中断上下文使用。因为中断上下文不能睡眠而mutex等待时会睡眠持锁进程可以睡眠跟spinlock的最大区别同一个mutex不能嵌套获取。持有者再次mutex_lock→死锁谁获取谁释放。路径A拿的mutex只能路径A来unlock不能让路径B代替unlock什么场景用mutex典型场景一个字符设备驱动的read/write函数里保护buffer。因为read/write是在进程上下文执行的里面可能要copy_to_user/copy_from_user可能睡眠这种情况spinlock不行不能睡眠只能用mutex。staticssize_tmy_read(structfile*filp,char__user*buf,size_tcount,loff_t*ppos){structmy_device*devfilp-private_data;mutex_lock(dev-lock);/* copy_to_user可能睡眠比如用户空间页面被换出需要page fault */if(copy_to_user(buf,dev-buffer,count)){mutex_unlock(dev-lock);return-EFAULT;}mutex_unlock(dev-lock);returncount;}三、信号量semaphore基本原理信号量内部维护一个计数器。每次获取down计数器减1每次释放up计数器加1。当计数器减到0时后来的获取者睡眠等待这点跟mutex一样不是忙等。mutex本质上是计数器初始值为1的信号量——同一时刻只有1个路径能持有。而信号量可以设初始值为N允许N个路径同时访问。定义和初始化#includelinux/semaphore.h/* 方式一静态定义计数初始值为N */staticDEFINE_SEMAPHORE(my_sem);/* 默认初始值为1等同于mutex *//* 方式二动态初始化指定初始计数值 */structsemaphoremy_sem;sema_init(my_sem,3);/* 允许最多3个路径同时持有 */使用方式/* 获取信号量计数器减1如果已经是0就睡眠等待 */down(my_sem);/* 临界区 */up(my_sem);/* 释放信号量计数器加1唤醒等待者 *//* 可中断版本 */if(down_interruptible(my_sem)){/* 被信号打断了没拿到信号量 */return-ERESTARTSYS;}/* 临界区 */up(my_sem);/* 尝试获取不等待立即返回 */if(down_trylock(my_sem)){/* 没拿到 */}else{/* 拿到了 */up(my_sem);}信号量的使用规则跟mutex一样不能在中断上下文使用等待时会睡眠up和down不要求是同一个路径这点跟mutex不同——mutex要求谁lock谁unlock信号量可以一个路径down另一个路径up可以设初始值大于1允许多个并发访问信号量的典型场景场景1限制并发访问数量。比如一个设备最多支持3个进程同时打开staticDEFINE_SEMAPHORE(dev_sem);/* 假设sema_init为3 */staticintmy_open(structinode*inode,structfile*filp){if(down_trylock(dev_sem))return-EBUSY;/* 已经有3个进程在用了 */return0;}staticintmy_release(structinode*inode,structfile*filp){up(dev_sem);return0;}场景2生产者-消费者同步。一个路径产生数据后up另一个路径down等待数据到来。这里up和down不是同一个路径所以不能用mutex只能用信号量。四、原子操作atomic基本原理原子操作是把一个简单操作加1、减1、读、写、测试并设置等变成CPU硬件级别的不可分割的单条指令。不需要锁没有获取/释放的开销。前面说的count竞态问题——读count、加1、写回count是三步操作可能被打断。原子操作让读-改-写变成一条指令硬件保证不会被打断。原子整数操作#includelinux/atomic.h/* 定义和初始化 */atomic_tcounterATOMIC_INIT(0);/* 初始值为0 *//* 基本操作 */atomic_set(counter,5);/* 设置为5 */intvalatomic_read(counter);/* 读取当前值返回5 */atomic_inc(counter);/* 加1变成6 */atomic_dec(counter);/* 减1变回5 */atomic_add(3,counter);/* 加3变成8 */atomic_sub(2,counter);/* 减2变成6 *//* 操作并返回结果 */intnew_valatomic_inc_return(counter);/* 加1并返回新值 *//* 测试操作 */if(atomic_dec_and_test(counter)){/* 减1后值变成0了返回true *//* 典型用途引用计数减到0时释放资源 */}原子位操作除了整数内核也提供对单个bit的原子操作#includelinux/bitops.hunsignedlongflags0;set_bit(3,flags);/* 把第3位设为1 */clear_bit(3,flags);/* 把第3位设为0 */change_bit(3,flags);/* 翻转第3位 */if(test_bit(3,flags)){/* 第3位是1 */}/* 测试并设置原子地检查旧值并设新值 */if(test_and_set_bit(3,flags)){/* 之前第3位就是1已经被别人设过了 */}else{/* 之前第3位是0现在被我设成1了 */}原子操作的适用场景场景1引用计数。比如一个设备被多少个进程打开了staticatomic_topen_countATOMIC_INIT(0);staticintmy_open(structinode*inode,structfile*filp){atomic_inc(open_count);pr_info(device opened %d times\n,atomic_read(open_count));return0;}staticintmy_release(structinode*inode,structfile*filp){atomic_dec(open_count);return0;}场景2简单标志位。比如标记设备是否正在传输中staticatomic_tbusyATOMIC_INIT(0);if(atomic_cmpxchg(busy,0,1)0){/* 成功从0改成1说明之前没人在用我拿到了 */do_transfer();atomic_set(busy,0);}else{/* 之前已经是1了有人在用 */return-EBUSY;}原子操作的局限原子操作只能保护单个变量的单次操作。如果你需要保护读寄存器A → 根据结果修改寄存器B这种多步操作原子操作无能为力必须用spinlock或mutex。五、其他机制读写自旋锁rwlock / rwsem读写锁区分读者和写者。多个读者可以同时持有锁因为只读不冲突但写者必须独占。/* 读写自旋锁 */rwlock_tmy_rwlock;rwlock_init(my_rwlock);read_lock(my_rwlock);/* 读者获取多个读者可以同时持有 *//* 读共享数据 */read_unlock(my_rwlock);write_lock(my_rwlock);/* 写者获取必须等所有读者和写者都释放 *//* 修改共享数据 */write_unlock(my_rwlock);/* 也有sleepable版本读写信号量 rw_semaphore */structrw_semaphoremy_rwsem;init_rwsem(my_rwsem);down_read(my_rwsem);/* 读者 */up_read(my_rwsem);down_write(my_rwsem);/* 写者 */up_write(my_rwsem);适用场景读多写少的数据结构比如系统的路由表、进程列表。读者之间不互斥可以提高并发性能。同样有read_lock_irqsave等变体。completion完成量一个路径等待另一个路径通知事情做完了。#includelinux/completion.hstructcompletionmy_done;init_completion(my_done);/* 路径A等待完成会睡眠 */wait_for_completion(my_done);/* 或者带超时版本 */unsignedlongtimeoutwait_for_completion_timeout(my_done,msecs_to_jiffies(1000));if(timeout0){/* 超时了1秒内没等到 */}/* 路径B比如中断处理函数通知完成 */complete(my_done);跟信号量的区别completion专门为等待-通知场景设计语义更清晰内核内部实现也做了优化。信号量虽然也能实现类似功能down等待up通知但completion是更推荐的做法。RCURead-Copy-UpdateRCU是一种极端优化读性能的机制读者几乎零开销不需要任何锁操作写者负责更复杂的更新流程先复制一份数据修改副本然后替换指针等所有读者都不再引用旧数据后释放旧数据。六、死锁deadlock死锁就是两个或多个执行路径互相等待对方释放资源谁也无法继续。场景1自死锁同一个锁获取两次spin_lock(lock_A);/* 做一些操作... */spin_lock(lock_A);/* 自己已经持有lock_A再获取→永远等不到→死锁 */怎么发生的通常不是这么直白写的而是函数调用链里隐藏的。比如func_a持有lock_Afunc_a调用func_bfunc_b里面也spin_lock(lock_A)。写代码时可能没注意到func_b里有这个锁。预防用锁之前明确哪些函数会持有哪个锁不要在持锁路径上调用可能获取同一个锁的函数。场景2ABBA死锁两个锁交叉获取/* CPU 0 *//* CPU 1 */spin_lock(lock_A);spin_lock(lock_B);/* ... *//* ... */spin_lock(lock_B);/* 等CPU1释放B */spin_lock(lock_A);/* 等CPU0释放A *//* 互相等死锁 */预防规定全局的锁获取顺序。如果所有代码路径都先获取A再获取B就不会出现交叉。场景3进程上下文和中断上下文死锁/* 进程上下文 */spin_lock(lock);/* 获取了锁 *//* 这时候中断来了CPU去执行中断处理函数 *//* 中断处理函数 */spin_lock(lock);/* 等待锁释放——但持锁的进程被打断了没法释放→死锁 */预防如果一个锁会被中断处理函数使用进程上下文里必须用spin_lock_irqsave获取锁的同时关中断这样中断就不会在持锁期间进来。场景4持spinlock时睡眠spin_lock(lock);kmalloc(size,GFP_KERNEL);/* GFP_KERNEL可能导致当前进程睡眠 *//* 进程睡了锁还没释放。其他路径等这个锁就要一直忙等 *//* 如果调度到的其他进程也要这个锁→死锁或长时间卡住 */spin_unlock(lock);预防持spinlock期间只调用不会睡眠的函数。需要分配内存就用GFP_ATOMIC不睡眠版本。