同步原语

每CPU变量

每CPU变量主要是数据结构的数组。系统的每个CPU对应数组的一个元素

  • 一个CPU不应该访问其他CPU对应的数组元素
  • 一个CPU可以随意读和修改自己的元素而不用担心出现竞争条件

原子操作

保证指令以原子方式执行——执行过程不被打断

  • 若干汇编语言指令都具有“读-修改-写”类型
  • 进行0次或1次对齐内存访问的汇编指令式原子的(不内存对齐会导致操作不原子)
  • 在读操作之后、写操作之前没有其他处理器占用内存总线,“读-修改-写”就是原子指令。单处理器不会出现内存总线窃用
  • 操作码前缀式lock字节的“读-修改-写”汇编指令即使在多处理器种也是原子的。控制单元检查到前缀就锁定内存总线

优化和内存屏障

  • 问题:处理同步时,必须避免指令重新排序。如果放在同步原语之后的一条指令在同步原语本身之前执行,事情会变得失控

优化屏障

保证编译程序不会混淆放在原语操作之前的汇编语言指令和放在原语操作之后的汇编语言指令

  • Linux实现:barrier()宏,展开为asm volatile("":::"memory")
    • volatile关键字禁止编译器把asm指令与程序中的其他指令重新组合
    • memory关键字强制编译器假定RAM种的所有内存单元已经被汇编语言指令修改
    • 因此编译器不能使用存放在CPU寄存器中的内存单元的值来优化asm指令前的代码

内存屏障

保证在原语之后的操作开始之前,原语之前的操作已经完成

  • 问题:优化屏障并不保证不使当前CPU把汇编语言指令混在一起执行
  • 所有原子操作都起内存屏障的作用
  • 读内存屏障仅仅作用于内存读的指令,写内存屏障仅仅作用于写内存的指令
  • 起内存屏障的作用的指令:
    • 对IO端口操作的指令
    • 带有lock前缀的指令

自旋锁

如果有一个执行线程试图获得一个被已经持有的自旋锁,那么该线程会一直进行忙循环(在CPU上运行)。要是锁未被争用,请求所得执行线程就能立刻获得锁

  • 特点:
    • 可以避免睡眠,但注意自旋锁不要长时间被持有,浪费处理器时间
    • 持有自旋锁的时间最好少于两次上下文切换的开销
    • 多处理器才有用
  • PS:
    • 自旋锁不能递归,会死锁
    • 中断处理程序可以使用自旋锁,不能使用信号量

读写自旋锁

把锁的用途明确分为读取和写入两个场景

  • 当写入时,不能有其他代码并发写或读,要求完全互斥
  • 当读取时,不能有其他代码写,可以并发读
  • 缺点:大量读者会使写者处于饥饿状态
  • 实现:
    • 读写自旋锁是一把锁,并不是分了读锁和写锁
    • 32位字段,其中24位计数器,0x01000000
    • 第24位是“未锁”标志字段
    • 写入时查看第24位是否为1。写入后,0x00000000
    • 读取时查看是否全为0(全为0不能读)。读取后,0x00ffffff,0x00fffffe

顺序锁

和读写自旋锁类似,但为写者赋予较高优先级

  • 解决问题:写者饥饿问题
  • 实现:
    • 锁的结构:一个自旋锁(写者使用),一个整型sequence字段
    • 写入时,获取锁,sequence+1;写入结束,sequence+1,释放锁
    • 保证写者写入时为奇数,没有写者写入时为偶数
    • 读取时,do-while循环直接读,循环里局部变量保存sequence值。结束条件中判断如果局部变量是奇数或者局部变量和当前sequence值不相同,继续循环
    • 保证读取前没有写者在写入,或读取时没有写者在写入
  • 优点:写者无需等待,除非有另一个写者正在写
  • 缺点:读者不得不反复读多次数据才能得到有效副本

读——拷贝——更新(RCU)

RCU允许多个读者和写者并发执行,读者可以直接读,写者并发需要自己选择同步机制

  • 限制RCU的使用范围
    1. RCU只保护被动态分配并通过指针引用的数据结构
      • 动态分配:kmalloc,vmalloc
      • 指针引用:读、写一起用,写更新了,读也更新了
    2. 在被RCU保护的临界区,不能睡眠
      • 写者需要等待读取旧副本的读者结束后再释放旧副本,读者睡眠会导致写者睡眠
  • 实现
    1. 读者不做任何事情防止竞争条件
    2. 写者要更新数据结构时,先生成一个副本,写者修改这个副本
    3. 修改完毕后,写者改变指向数据结构的指针,使它指向新副本
    4. 困难之处:什么时候释放旧副本
      • 当所有读者完成读取操作时,才能释放
      • 这个过程也叫做宽限期
  • 适合场景:读多写少,因为写多了,拷贝开销大。用于网络层和虚拟文件系统

信号量

信号量是一种睡眠锁。当某个进程试图获得已经被占用的信号量,信号量会将其推进一个等待队列,然后让其睡眠。当持有的信号量被释放后,处于等待队列的进程就会被唤醒

  • 信号量结构体包含一个原子变量,等待队列
  • 特点:
    • 适合长时间被持有的情况
    • 锁被短时间持有就不适合用信号量了,因为睡眠、维护等待队列和唤醒有一定开销
    • 进程可以获取信号量,中断上下部都不可以获取信号量
    • 占用信号量的时候不能占用自旋锁

读写信号量

读写信号量是互斥信号量,和读写自旋锁一样,只对写者互斥,不对读者

posted @ 2021-02-12 14:48  肥斯大只仔  阅读(200)  评论(0编辑  收藏  举报