1. 为什么要保证原子性

处理器分两种:cisc(复杂指令集,可以直接在内存上进行操作,如x86,一条汇编指令可以原子的完整读内存、计算、写内存)和rics(精简指令集,所有操作都必须是在CPU内部进行。所以你想给内存某个变量做加法,你要先用load指令把内存load到CPU的寄存器、再执行add,再执行store把结果放到内存中)。

因此a++这句话在rics上并不是原子的,必须翻译成一个rmw序列(读、修改、写)。那么就有可能在中途被打断,执行结果就可能不符合预期了。又例如,CPU去修改某个寄存器,也是这样的rmw过程,因此如果多个线程像修改这个寄存器也会有潜在问题。

所以一些芯片把寄存器分类成了set和clear两个寄存器。那么在修改寄存器的时候,想把某个寄存器的某个bit写1,就去写它的set寄存器,想把某位清0,就去写它的clear寄存器,这样就只有写操作。然后硬件会保证修改寄存器内容及其原子性,你就不用做了。例如,你想把寄存器的bit9写1,则就把1>>9写到set寄存器就好了,无需知道寄存器原来的值是什么。

有的芯片使用bitband技术:一个寄存器有32位,就有32个影子寄存器,对应寄存器的每一位。同上,你想改某一位就写这个bit的影子寄存器,硬件会给把寄存器修改成正确的值。这样,你写的代码就无需考虑原子性,提高了代码的performance。

硬件上保证原子性的一些手段:

排他性(或独占性)的load和store,对于ARM,读的时候调用ldrex,写的时候调用strex:当两个线程同时做load或store(使用ex),就会把并行的序列变成串行,只有第一个store的人会成功,第二个失败(指令会有返回值),第二个store的代码要写成死循环,判断返回值,失败后就重新从load开始再次执行序列。注意,编译器无法产生idrex/strex指令,因此要用它们的话必须手写内嵌汇编。

atomic_add/atomic_sub/atomic_inc/atomic_dec/atomic_set/atomic_read等都是通过上述排他性的wmx序列来完成,因此给一个整数变量加减可以使用这几个api。并且定义变量要使用atomic_t,虽然只是int的typeof,但还是写规范点。

atomic_xxx只能用来对整型变量保证原子性。对于其他类型或语义通常是通过加锁或禁用中断来保证原子性的。

2. Linux内核锁

临界区:就是可能存在多个线程同时访问临界资源的代码片段,而临界资源在一个时间点只能被一个线程访问。这种情况下,我们通过加锁的方式对临界区加以保护(在用户态还可以通过同步信号量或条件变量等方式,但内核中一般只用锁)。

拿到一把锁,要把相关的语义事物全部做完,再解锁。具体哪些部分是一个语义整体,就得你根据实际场景自己分析了,看哪些语义是相互关联的,就一起加锁。另外,要做到语义最小,不能说为了安全,不管三七二十一对整段代码加锁。

另外一点要注意的就是,语义关联的东西必须要共同加锁。这句话的意思是,加锁的对象一定要是一个语义,不能是其中的一部分,例如,一个结构体实例,里面有性别和姓名两个成员,多个线程会修改它,那么这时的语义就是整个结构体,你就要给整个结构体加锁。如果你给姓名和性别单独加锁的话,就可能出现在某个时刻小明的性别变成了女性。

一般那些某个地方代码的错误导致了其他代码的bug都是两个原因:1.内存越界,指针乱踏。2.锁没加对,导致偶然性的bug,可能好几天才会挂。

2.1 spinlock

自旋锁是用在多处理器环境中的锁:如果内核控制路径发现自旋锁开着,就获取锁并继续执行,相反,如果内核控制路径发现锁由运行在另一个CPU上的内核控制路径锁着,就在周围“旋转”,直到锁被释放。这个“旋转”就是在忙等,这期间正在等待的内核控制路径除了浪费时间,无事可做。

spin_lock()的实现逻辑是,他执行的是一个核内锁调度,核间自旋的过程。多核处理器里面,任何一个核拿到了spinlock,这个核内的调度器就被锁住了,也就是这个核上的其他线程就不可能被调度执行了,核内是通过直接把调度器锁住来实现的。而核间才是真正自旋,因此spinlock中的”spin”在多核上才有意义。在单核情况下,spin_lock()就只是简单的去锁住调度器(preempt_disable,即禁掉内核抢占)。

spinlock适合锁住那些时间特别短且不睡眠的区间。这包括了两方面:

核1锁住临界区,核2上某线程在等待进入临界区,那么核2线程可以选择睡眠让其他线程运行,等核1线程退出临界区唤醒自己后再继续运行,也可以原地自旋忙等。如果一个临界区的时间很短,核1的线程很快执行完临界区了,这种情况下,核2线程与其睡眠进行两次上下文切换,还不如原地死等(while循环去检查一个变量的值),因为可能前者的开销更大。

spinlock的区间不能睡眠(不能调用可睡眠函数),这个好理解,因为不能调度了,而睡眠会引发调度。

在定义一个spinlock的时候要将锁初始化为“未锁住”的状态,确保第一次可以获得锁,定义一个自旋锁用DEFINE_SPINLOCK(x)宏即可,x是锁的名字。

虽然spinlock之后这个核不能进行调度了,但这个核上的中断还可能来,spin_lock挡不住中断,如果中断处理程序也要访问临界资源,则spinlock就起不到作用了。这时要用spinlock的修改版本spin_lock_irqsave,即既拿spinlock,也把这个核上的中断关掉。并且,这时线程中必须使用spin_lock_irqsave,要不然线程在spin_lock的时候被中断,中断处理中又调用spin_lock就死锁了。

多核的竞态有哪些情况呢?一个最严重的并发网:CPU0(有t1,t2两个线程和中断irq1)和CPU1(有t3,t4两个线程和中断irq2),这6个例程相互之间都可能产生竞态(访问相同的资源)。

解决竞态的简单做法:在线程里面统一调用spin_lock_irqsave,在中断里面统一调用spinlock。这样就避免了核内和核间的所有竞态,(核间的竞态是通过spin解决的,核内通过禁抢占和中断解决)。如果你知道只有线程才访问临界区,那线程里只用spinlock即可。

注:Linux 2.6.32以后就不支持中断嵌套了,因此中断里spinlock就好了,而老版内核版本,中断里面也要调用spin_lock_irqsave。

中断的几个API:

local_irq_save或local_irq_disable,关闭本CPU的中断。local_irq_save在关中断的同时会保存当前开关中断的状态,可以在restore的时候恢复。local_irq_disable/save是直接去改cprs寄存器,让CPU不响应中断。spin_lock_irqsave,是spinlock加local_irq_save的合体。

irq_disable(iqr_desc),屏蔽某号中断,它该的是描述符,让这个中断不发给CPU了。

我们说spin_lock核内锁调度,核间自旋。而local_irq_save是锁住了本核的中断,但在核间是没有任何作用的(Linux没有任何API能关其他核的中断或调度器)。由于我们平时写的驱动都是跨核的,不要假设自己代码肯定是单核上运行,local_irq_save起不到锁住多核的作用,如果另一个核要访问你这个核上线程的资源就产生竞态了,因此写代码的时候不要用local_irq_save,你自己写的代码基本不会存在只需要使用local_irq_save的情况,建议都改用spin_lock_irqsave来锁中断。当然local_irq_disable就更不要用了。

因此,spin_lock和spin_lock_irqsave是常用的。

注意kmalloc可能睡眠,如果在spinlock申请内存,可以加GFP_ATOMIC的flag,也可以直接用alloc_page系列函数。

还有其他的变种例如local_bh_disable()是锁下半部(锁抢占)的,相应的spin_lock_bh()是多核中锁下半部的。

2.2 mutex和信号量

mutex原理很简单,一个线程拿到了mutex,另一个线程运行时得不到mutex就睡觉,调度出去。mutex适合时间比较长或需要睡眠的临界区间。这里再多说一句,如果你的临界区里面需要调用睡眠函数就不要用spin_lock,因为代码如何运行不可预料,kernel里面有个选项CONFIG_DEBUG_ATOMIC_SLEEP,打开这个选项,在spinlock里睡就会有oops。

以前内核里还有信号量,现在基本被淘汰了,因为太复杂,实现成本太高,已经不建议用了。

加锁的原则:同一把锁,语义整体,粒度最小。因此包括三方面:1.锁一个资源用同一把锁,2.保证语义完整,3.但语义范围尽量小,使加锁粒度最小。

其中1很好理解,一个资源如果用不同的锁是锁不住的。2直接影响到功能的正确性了,而3则会影响程序的并行性能。

如果发现一个语义太大,可能是你的设计或数据结构定义有问题,本来没有互斥的语义给放到一个大的语义里了,就要尝试做语义分解。

补充一下同步锁和互斥锁:

互斥锁: 用来保护临界区,确保两个线程不能同时访问同一个资源。但是不在乎这两个线程访问这个资源的先后顺序。例如mutex,内核中的spinlock。

同步锁: 用来保证两个线程有序地访问某个资源,也有互斥在里面。例如信号量、用户态的条件变量。好像直接提及“同步锁”这个名字的时候不多。

3. 调试Linux死锁

Linux被hang住通常是由于spinlock和锁中断导致的,因为他俩都把CPU堵住了。一个调试hang死的工具是Linux自带的lockup detector。

soft lockup: 锁住调度器。

hard lockup: 锁住了中断。

kernel/watchdog.c就是用来实现lockup detector的(开启内核选项CONFIG_LOCKUP_DETECTOR),它使能了一个高优先级的rt线程,周期性的跑,给某个计数器+1,有个定时器中断定期检查这个计数器。如果定时器发现一定时间内都没+1,则说明调度器锁死了,定时器中断处理程序就打印backtrace(中断服务程序是运行在当前线程的栈,因此打印backtrace就能看到最新被调度的线程的栈)。

hard lockup:需要CPU支持NMI(不可屏蔽中断,通常是通过CPU里的PMU单元实现的),如果PMU发现长时间(这个cycle是借助NMI来计算的,因为定时器可能不工作了)一个中断都不来,就知道发生了hard lockup,这时(触发NMI中断,中断处理函数中)分析栈就知道在哪里锁住中断的。需要把CONFIG_HARDLOCKUP_DETECTOR打开。

由于ARM里面没有NMI,因此内核不支持ARM的hard lockup detector。但有一些内核patch可以用,比如用FIQ模拟MNI(如果FIQ用于其他地方了,这里就用不了了),或者用CPU1去检测CPU0是否被hard lockup(但CPU1没办法获得线程的栈,只能知道lockup了),但这两个patch都没在主线上。FIQ在Linux中基本不用的(一般只做特殊的debugger,常规代码不用)。

注意别跟drivers/watchdog/弄混了,这是看门狗,不是一回事。

不属于linux内核锁的是,Linux内核中的锁相关推荐

  1. 锁失效_关于bigtable中chubby锁失效时的一点思考

    最近跟国内几家热门公司做分布式存储的大佬们聊了聊,过程十分愉快,但同时也有点小虐.说到底,自己在这个领域并没有很久的经验,很多东西仍停留在知其然而不知其所以然的地步.魔鬼藏在细节之处. 不过这也正好是 ...

  2. java 对象锁_个人对java中对象锁与类锁的一些理解与实例

    一  什么是对象锁 对象锁也叫方法锁,是针对一个对象实例的,它只在该对象的某个内存位置声明一个标识该对象是否拥有锁,所有它只会锁住当前的对象,而并不会对其他对象实例的锁产生任何影响,不同对象访问同一个 ...

  3. @Transactional事务中使用锁坑(@Transactional事务中使用锁失效)

    @Transactional事务中使用锁失效 说明: Spring中使用注解@Transactional作事务管理,@Transactional注解在方法上时,是方法完成之后才进行提交事务的 测试代码 ...

  4. mysql 高并发写入锁表_使用mysql中的锁解决高并发问题

    阿里云产品通用代金券,最高可领1888分享一波阿里云红包. 阿里云的购买入口 为什么要加锁 多核计算机的出现,计算机实现真正并行计算,可以在同一时刻,执行多个任务.在多线程编程中,因为线程执行顺序不可 ...

  5. linux c 日志写入文件,linux下C语言实现写日志功能

    先上程序,该程序经过测试能够很好的实现写日志要求 /************************************************************************* ...

  6. Java与C语言中的锁

    Java与C语言中的锁 C 嵌入式汇编的语法格式是: asm(code : output operand list : input operand list : clobber list) __asm ...

  7. Java中的锁原理、锁优化、CAS、AQS详解!

    阅读本文大概需要 2.8 分钟. 来源:jianshu.com/p/e674ee68fd3f 一.为什么要用锁? 锁-是为了解决并发操作引起的脏读.数据不一致的问题. 二.锁实现的基本原理 2.1.v ...

  8. Java中的锁原理、锁优化、CAS、AQS详解

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:景小财 www.jianshu.com/p/e674ee68 ...

  9. Java中的锁[原理、锁优化、CAS、AQS]

    点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:用好Java中的枚举,真的没有那么简单!个人原创+1博客:点击前往,查看更多 作者:高广超 链接:https:/ ...

  10. Java中的锁的概念大汇总

    文章目录 公平锁/非公平锁 公平锁 非公平锁 乐观锁/悲观锁 乐观锁 悲观锁 独占锁/共享锁 独占锁(排它锁) 共享锁 互斥锁/读写锁 互斥锁 读写锁 偏向锁/轻量级锁/重量级锁 偏向锁 轻量级锁 重 ...

最新文章

  1. vim在退出时,处理隐藏缓冲区的方式
  2. [How TO]-如何编写Linux kernel documentation
  3. python 分析两组数据的差异_R语言limma包差异基因分析(两组或两组以上)
  4. 福建师范大学计算机组成原理期末试卷,福建师范大学2020年8月课程考试《计算机组成原理》作业考核试题...
  5. 七夕用腾讯最热门五大编程语言写三行情书
  6. JVM调优系列:(四)GC垃圾回收
  7. 王道考研 计算机网络17 IP数据报 最大传送单元MTU IP地址 IPv4 子网划分 ARP协议 ICMP协议 移动IP
  8. linux 别名,Linux中的别名就这么简单,如何使用和创建永久别名?
  9. 【2017年第1期】智慧城市多源异构大数据处理框架
  10. Python 读写CSV文件
  11. Hinton发布最新论文!表达神经网络中部分-整体层次结构
  12. matlab2018安装摄像头驱动以及如何调用摄像头
  13. caffe训练的实时可视化思路
  14. CAD学习笔记中级课【参数化】
  15. 极光推送JPush使用Java SDK开发
  16. 计算机英语第二版期末翻译试题答案,开放英语I期末翻译测试题
  17. 服务端接入验证苹果支付receipt
  18. lua生成指定大小的随机字符串
  19. 2022年了,我才开始学 typescript ,晚吗?(7.5k字总结)
  20. IntelliJ IDEA 如何实现代码展示自动换行

热门文章

  1. oracle常用语法
  2. 自定义view画走势图(一)
  3. 显示器的分辨率为1024*1024的显示模式,显示器中每个像素点的灰度等级为256级,则帧缓存容量至少为( ) bit
  4. 从火炬传递看搜狐奥运赞助商优势
  5. 什么情况下需要办理年度汇算?节选北京税务
  6. 自定义View——闹钟
  7. 二、演练领域驱动的设计过程
  8. 【CSS】box-sizing属性border-box与content-box区别
  9. myeclipse+websphere 环境配置说明及常见问题
  10. Shell—各种括号的用法