Mr Dk.'s BlogMr Dk.'s Blog
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • 🐧 Linux Kernel Development
    • Chapter 1 - Linux 内核简介
    • Chapter 2 - 从内核出发
    • Chapter 3 - 进程管理
    • Chapter 4 - 进程调度
    • Chapter 5 - 系统调用
    • Chapter 7 - 中断和中断处理
    • Chapter 9 - 内核同步介绍
    • Chapter 10 - 内核同步方法
    • Chapter 11 - 定时器和时间管理
    • Chapter 13 - 虚拟文件系统
    • Chapter 14 - 块 I/O 层
    • Chapter 16 - 页高速缓存和页回写

Chapter 9 - 内核同步介绍

Created by : Mr Dk.

2019 / 10 / 23 14:18

Nanjing, Jiangsu, China


需要特别留意保护共享资源,防止共享资源并发访问。

9.1 临界区和竞争条件

临界区:访问和操作共享数据的代码段。多个线程并发访问同一个资源通常不安全,必须保证进入临界区中的代码 原子地 执行:操作在执行结束前不可被打断,要么执行完,要么完全不执行,如同整个临界区是一条不可分割的指令。

如果两个线程同时进入临界区,那么就发生了 竞争条件 (race condition)。避免并发和防止竞争条件称为 同步 (synchronization)。

9.1.1 为什么我们需要保护

银行 ATM 机的例子

  • 必须保证在操作期间对账户加锁
  • 确保每个事物相对于其它事物的操作时原子性的
  • 事物必须完整地发生,要么干脆不发生,但是绝不能打断

9.1.2 单个变量

i++; 的例子。多数 CPU 都提供了指令来原子地读变量、增加变量、并写回变量,使用这样的指令能够解决一定的问题,因为两个原子操作交错执行根本不可能发生。CPU 从物理上确保了这种不可能。

9.2 加锁

对数据结构的操作显然不能通过原子指令来实现。当共享资源是一个复杂的数据结构时,竞争条件往往会使该数据结构遭到破坏。硬件不可能对不定长度的临界区提供原子指令,需要 锁 机制来确保一次有且只有一个线程对数据结构进行操作:线程持有锁,锁保护了数据。

锁的使用是 自愿、非强制 的,不强制使用,但为了防止发生竞争条件,必须使用。锁有多种形式,且加锁的粒度和范围也各不相同。锁由 原子操作 实现:

  • 原子操作不存在竞争
  • 单一指令可以验证锁的关键部分是否被抓住
  • 几乎所有 CPU 都提供了 测试 和 设置 指令,对锁进行原子地验证

9.2.1 造成并发执行的原因

用户空间需要同步:

  • 因为用户程序会被调度程序抢占和重新调度
  • 程序在临界区中时,可能会被非自愿抢占

这类并发实际上并不是同时发生的。如果有多核 CPU,则存在着真正意义上的并发(并行?)。内核可能造成并发执行的原因:

  • 中断 - 任何时刻异步发生
  • 软中断、tasklet
  • 内核抢占
  • 睡眠、与用户空间同步
  • 对称多处理

应当在最开始设计代码时就考虑加锁,而不是事后。如果代码已经写好,在其中找到需要加锁的地方并加锁,是很困难的,而且结果也不好。

9.2.2 了解要保护些什么

执行线程的局部数据仅仅被它本身访问,显然不需要保护。要给数据而不是给代码加锁。编写内核代码时,要问问自己这些问题:

  • 数据是否是全局的?
  • 数据会不会在进程上下文和中断上下文中共享
  • 访问数据时可不可能被抢占
  • 当前进程是不是会睡眠在某些资源上
  • 函数如果在另一个 CPU 上被调度将会发生什么

9.3 死锁

每个线程都在等待一个资源,所有的资源都已经被占用,所有线程都在互相等待,但永远不会释放已经占用的资源。

  • 自死锁:一个执行线程试图获得一个自己已经持有的锁
  • ABBA 死锁:线程 A 和 B 分别获得了 A 和 B 锁,都想要另一个锁

没有线程会释放一开始就持有的锁。一些避免死锁的策略:

  • 按顺序加锁:保证以相同的顺序获取锁,如果两个或多个锁在同一时间里被请求,其它函数也应当按照前次的加锁顺序进行
  • 防止发生饥饿
  • 不要重复请求同一个锁
  • 设计力求简单

只要嵌套使用多个锁,就要按照相同的顺序去获取它们。

9.4 争用和扩展性

锁的争用,指锁被占用时,有其它线程试图获得该锁——多个线程等待获得该锁:高度争用状态。锁使程序以串行的方式对资源进行访问,使用锁会降低系统的性能,被高度争用的锁会成为系统的性能瓶颈,严重降低系统性能。

扩展性是对系统可扩展程度的一个量度,任何可以被计量的计算机组件都可以涉及可扩展性。在 Linux 2.6 内核中,内核加的锁是非常细的粒度。加锁粒度用于描述加锁保护的数据规模:

  • 一个过粗的锁保护大块数据
  • 一个精细的锁保护很小的一块数据

细粒度锁在大型系统上的性能可能会很好,但在小型系统上会增加复杂度,并加大开销

  • 锁争用太严重时,加锁太粗会降低可扩展性
  • 锁争用不明显时,加锁太细会加大系统开销,带来浪费

两种情况都会造成系统性能下降。

Edit this page on GitHub
Prev
Chapter 7 - 中断和中断处理
Next
Chapter 10 - 内核同步方法