第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是内核实现好的可延迟函数,会自动保证其在多个处理器上的串行执行,因此不需要保护。

posted @ 2025-02-23 12:45  moonのsun  阅读(23)  评论(0)    收藏  举报