第5章 内核同步
1 内核抢占
定义:内核控制路径被其他内核进程抢占,称为内核抢占
条件:进程描述符thread_info的抢占计数器preempt_count字段<=0 && 中断打开
发生时机:
- 结束内核控制路径(通常是中断处理程序)时
- 异常处理程序调用preempt_enable()时
- 启用可延迟函数(软中断,tasklet)时
Linux 2.6开始,内核是抢占式的!
2 同步原语
2.1 每CPU变量
- 每CPU变量不是一个CPU独占的变量,而是每个CPU都会在栈中保存一份该变量,互不影响
- 系统为每CPU变量提供了一些操作宏/函数,主要是针对每CPU变量数组
- 每CPU变量也不是高枕无忧,修改时需要确保关闭内核抢占,避免被单个CPU“并行”修改
—— 加锁也可以实现,不过既然加锁了,就没必要使用每CPU变量了,普通变量不就行了?
2.2 原子操作
- 普通操作:读改写分开,中间或许插入别的指令;原子操作:中间不中断
- 32位处理器访问64位变量,或者访问非对齐变量,都是非原子操作
- 内核也为原子操作提供了宏/函数:atomic_xxx()
- 系统为原子位处理提供了处理函数:test_bit()等
2.3 优化屏障和内存屏障
代码在编译的时候,编译器会进行优化,比如不再从内存取值,而是从寄存器取值
代码执行的时候也会进行优化,比如并行执行一些汇编语句。
优化屏障:编译的时候,认为RAM中的内存单元已被更改,不会混淆原语前的汇编指令和原语后的汇编指令
内存屏障:执行的时候,确保原语执行开始前,原语前的操作已经全部完成
2.4 自旋锁
自旋锁作用:
- 加锁:禁止多个CPU同时访问临界区
- 禁止内核抢占:禁止多个进程并发访问临界区
2.5 读写锁
- 读写锁中,读和写具有相同的优先级,谁都要等持锁的对方完成
- 和普通锁不同的是,可以有多个读同时进行
- 读写锁的锁资源数量设置为0x0100 0000,每次读前尝试锁减一,写前尝试锁减0x1000 0000,因此结果就是可以多个读,一个写 —— 很巧妙的设计方法
- down_trylock()尝试去获取信号量,如果无法获取,会立刻返回,并不会休眠 —— 可以用在中断中,适合有多个资源的情况
2.6 顺序锁
- 写的优先级高于读
- 写持锁,读会被阻塞
- 读持锁,写可以继续进行,并修改了顺序锁的顺序数量,读完发现顺序数量修改了,会再读一次
2.7 读-拷贝-更新 RCU
- 读:通过指针间接访问数据结构
- 写:生成整个数据结构的副本,写完毕使用原子操作,修改数据结构的指针指向新的副本
- 等写期间所有读完成,就把旧的副本丢弃——新的读肯定直接使用新副本了,也就是某段时间新旧副本会同时存在
2.8 信号量
- 获取不到资源的时候会挂起(TASK_RUNNING -> TASK_UNINTERRUPT),所以不可以中断和可延迟函数中使用
- 有一种读写信号量,使用方法类似读写锁,读写信号量和读写锁的区别,就是锁和信号量的区别
- 信号量针对多进程使用资源,无论是单处理器还是多处理器,工作方式没有区别,也可以发生内核抢占
3 同步原语的选择
3.1 保护异常所访问的数据结构
最常见的产生同步问题的异常就是系统调用服务例程;
采取的措施就是信号量
3.2 保护中断所访问的数据结构
每个中断处理程序只会被串行的执行,如果一个结构只被一个中断处理程序使用,不用考虑同步问题;
如果多个中断处理程序访问一个数据结构,需要根据单/多处理器系统采取不同的错误同步:
- 单处理器:禁止中断,也就禁止了中断嵌套,保证一个中断执行完才会执行完另一个中断 —— 单处理器不能使用自旋锁,否则可能会卡死
- 多处理器:禁止中断(本地中断),同时使用锁
3.3 保护可延迟函数所访问的数据结构
可延迟函数总是在一个CPU上串行执行,所以单处理系统不需要保护;
多处理器系统,需要使用锁来保护可延迟函数;
tasklet是内核实现好的可延迟函数,会自动保证其在多个处理器上的串行执行,因此不需要保护。
本文来自博客园,作者:moonのsun,转载请注明原文链接:https://www.cnblogs.com/moon-sun-blog/p/18719419

浙公网安备 33010602011771号