diff --git a/_posts/2021-01-01-TheArtofUnitTesting.md b/_posts/2021-01-01-TheArtofUnitTesting.md new file mode 100644 index 0000000..5771a0f --- /dev/null +++ b/_posts/2021-01-01-TheArtofUnitTesting.md @@ -0,0 +1,48 @@ +--- +title: 单元测试的艺术 +layout: post +categories: 测试 +tags: 单元测试 书籍 +excerpt: 单元测试的艺术 +--- +# 第 1 章 单元测试基础 +**回归**是以前运行良好但是现在不工作的一个或多个工作单元。 + +**遗留代码**在维基百科中定义为“与一个不再受支持或继续生产的操作系统,或其他计算机技术相关的源代码”,但是很多公司把任何比当前维护的应用更老旧的版本都称为遗留代码。这个词经常用来指代那些难以使用,难以测试,通常也更难以阅读的代码。 + +一个**单元测试**是一段自动化的代码,这段代码调用被测试的工作单元,之后对这个单元的单个最终结果的某些假设进行检验。单元测试几乎都是用单元测试框架编写的。单元测试容易编写,能快速运行。单元测试可靠、可读,并且可维护。只要产品代码不发生变化,单元测试的结果是稳定的。 + +## 1.7 成功进行TDD的三种核心技能 +成功进行测试驱动开发,你需要三种技能集:知道如何编写优秀的测试、在编码前编写测试,以及良好的测试设计。 + +- 仅仅做到先编写测试,并不能保证测试是可维护、可读以及可靠的。你正在读的这本书讲的全都是进行优秀单元测试的技巧。 +- 仅仅做到编写的测试可读、可维护,并不能保证你获得先编写测试的各种好处。市面上大部分讲TDD的书介绍的都是测试优先的技能,而不讲授优秀测试技能。我特别推荐Kent Beck的Test-Driven Development: by Example(Addison-Wesly Professional,2002)。 +- 仅仅做到先编写测试,并且测试可读、可维护,并不能保证你得到一个设计完善的系统。设计能力才是使代码优美、可维护的关键。关于这方面的好书,我推荐Steve Freeman和Nat Pryce的Growing Object-Oriented Software, Guided by Tests(Addison-Wesly Professional,2009)以及Robert C. Martin的《程序员的职业素养》。 + +## 1.8 小结 +一个优秀单元测试应具有的特质,如下所示: +- 一段自动化的代码,它调用另一个方法,然后检验关于此方法或类的逻辑行为的某些假设; +- 用一个自动化测试框架编写。 +- 容易编写。 +- 运行快速。 +- 能由开发团队里的任何人重复执行。 + +# 第 2 章 第一个单元测试 +## 2.1 单元测试框架 +## 2.1.1 单元测试框架提供什么 +![JCmpnI.png](https://s1.ax1x.com/2020/04/15/JCmpnI.png) +![](http://www.ituring.com.cn/figures/2020/ArtUnitTesting/02-001.png) + +# 第 3 章 使用存根破除依赖 +# 第 4 章 使用模拟对象进行交互测试 +# 第 5 章 隔离(模拟)框架 +# 第 6 章 深入了解隔离框架 +# 第 7 章 测试层次和组织 +# 第 8 章 优秀单元测试的支柱 +# 第 9 章 在组织中引入单元测试 +# 第 10 章 遗留代码 +# 第 11 章 设计与可测试性 +# 其他参考书籍 +- Test-Driven development: by Example +- 程序员的职业素养 +- Growing Object-Oriented Software, Guided by Tests diff --git a/_posts/2021-01-01-designPattern.md b/_posts/2021-01-01-designPattern.md new file mode 100644 index 0000000..7790a42 --- /dev/null +++ b/_posts/2021-01-01-designPattern.md @@ -0,0 +1,331 @@ +--- +title: 大话设计模式 +layout: post +categories: 设计模式 +tags: C++ 设计模式 书籍 +excerpt: 大话设计模式 +--- +# 第1章 代码无错就是优?——简单工厂模式 +## 1.4 面向对象编程 +- 封装 +将业务计算`operation`封装成单独的运算类。 +- 继承 +- 多态 + +## 1.8 业务的封装 +将业务逻辑和界面逻辑分开,让它们之间的耦合度下降。 + +## 1.9 紧耦合vs.松耦合 +将加减乘除等运算分离,修改其中一个不影响另外的几个,通过继承来做到这一点。 + +## 1.10 简单工厂模式 +创建一个工厂类来根据输入创建不同的类,`autosar`中的插件工厂就使用了这种模式。 +```csharp +public class OperationFactory +{ + public static Operation createOperate(string operate) + { + Operation oper = null; + switch (operate) + { + case "+": + { + oper = new OperationAdd(); + break; + } + case "-": + { + oper = new OperationSub(); + break; + } + case "*": + { + oper = new OperationMul(); + break; + } + case "/": + { + oper = new OperationDiv(); + break; + } + } + return oper; + } +} +``` + + +第2章 商场促销——策略模式 17 +2.1 商场收银软件 17 +2.2 增加打折 18 +2.3 简单工厂实现 19 +2.4 策略模式 22 +2.5 策略模式实现 25 +2.6 策略与简单工厂结合 27 +2.7 策略模式解析 28 +第3章 拍摄UFO——单一职责原则 30 +3.1 新手机 30 +3.2 拍摄 30 +3.3 没用的东西 31 +3.4 单一职责原则 31 +3.5 方块游戏的设计 31 +3.6 手机职责过多吗? 33 +第4章 考研求职两不误——开放-封闭原则 34 +4.1 考研失败 34 +4.2 开放-封闭原则 35 +4.3 何时应对变化 36 +4.4 两手准备,并全力以赴 37 +第5章 会修电脑不会修收音机?——依赖倒转原则 38 +5.1 MM请求修电脑 38 +5.2 电话遥控修电脑 39 +5.3 依赖倒转原则 40 +5.4 里氏代换原则 41 +5.5 修收音机 43 +第6章 穿什么有这么重要?——装饰模式 44 +6.1 穿什么有这么重要? 44 +6.2 小菜扮靓第一版 45 +6.3 小菜扮靓第二版 47 +6.4 装饰模式 50 +6.5 小菜扮靓第三版 53 +6.6 装饰模式总结 56 +第7章 为别人做嫁衣——代理模式 57 +7.1 为别人做嫁衣! 57 +7.2 没有代理的代码 58 +7.3 只有代理的代码 60 +7.4 符合实际的代码 61 +7.5 代理模式 63 +7.6 代理模式应用 65 +7.7 秀才让小六代其求婚 66 +第8章 雷锋依然在人间——工厂方法模式 67 +8.1 再现活雷锋 67 +8.2 简单工厂模式实现 68 +8.3 工厂方法模式实现 69 +8.4 简单工厂vs.工厂方法 71 +8.5 雷锋工厂 72 +第9章 简历复印——原型模式 77 +9.1 夸张的简历 77 +9.2 简历代码初步实现 78 +9.3 原型模式 80 +9.4 简历的原型实现 82 +9.5 浅复制与深复制 84 +9.6 简历的深复制实现 87 +9.7 复制简历vs.手写求职信 89 +第10章 考题抄错会做也白搭——模板方法模式 90 +10.1 选择题不会做,蒙呗! 90 +10.2 重复=易错+难改 91 +10.3 提炼代码 93 +10.4 模板方法模式 96 +10.5 模板方法模式特点 98 +10.6 主观题,看你怎么蒙 98 +第11章 无熟人难办事?——迪米特法则 100 +11.1 第一天上班 100 +11.2 无熟人难办事 100 +11.3 迪米特法则 102 +第12章 牛市股票还会亏钱?——外观模式 103 +12.1 牛市股票还会亏钱? 103 +12.2 股民炒股代码 104 +12.3 投资基金代码 106 +12.4 外观模式 108 +12.5 何时使用外观模式 110 +第13章 好菜每回味不同——建造者模式 112 +13.1 炒面没放盐 112 +13.2 建造小人一 113 +13.3 建造小人二 114 +13.4 建造者模式 115 +13.5 建造者模式解析 118 +13.6 建造者模式基本代码 119 +第14章 老板回来,我不知道——观察者模式 123 +14.1 老板回来?我不知道! 123 +14.2 双向耦合的代码 124 +14.3 解耦实践一 126 +14.4 解耦实践二 128 +14.5 观察者模式 131 +14.6 观察者模式特点 134 +14.7 观察者模式的不足 135 +14.8 事件委托实现 136 +14.9 事件委托说明 139 +14.10 石守吉失手机后的委托 140 +第15章 就不能不换DB吗?——抽象工厂模式 141 +15.1 就不能不换DB吗? 141 +15.2 最基本的数据访问程序 142 +15.3 用了工厂方法模式的数据访问程序 143 +15.4 用了抽象工厂模式的数据访问程序 146 +15.5 抽象工厂模式 149 +15.6 抽象工厂模式的优点与缺点 151 +15.7 用简单工厂来改进抽象工厂 151 +15.8 用反射+抽象工厂的数据访问程序 154 +15.9 用反射+配置文件实现数据访问程序 157 +15.10 无痴迷,不成功 157 +第16章 无尽加班何时休——状态模式 158 +16.1 加班,又是加班! 158 +16.2 工作状态-函数版 159 +16.3 工作状态-分类版 160 +16.4 方法过长是坏味道 162 +16.5 状态模式 163 +16.6 状态模式好处与用处 165 +16.7 工作状态-状态模式版 166 +第17章 在NBA我需要翻译——适配器模式 171 +17.1 在NBA我需要翻译! 171 +17.2 适配器模式 171 +17.3 何时使用适配器模式 174 +17.4 篮球翻译适配器 174 +17.5 适配器模式的.NET应用 178 +17.6 扁鹊的医术 178 +第18章 如果再回到从前——备忘录模式 180 +18.1 如果再给我一次机会…… 180 +18.2 游戏存进度 180 +18.3 备忘录模式 183 +18.4 备忘录模式基本代码 184 +18.5 游戏进度备忘 186 +第19章 分公司=一部门——组合模式 189 +19.1 分公司不就是一部门吗? 189 +19.2 组合模式 190 +19.3 透明方式与安全方式 193 +19.4 何时使用组合模式 194 +19.5 公司管理系统 194 +19.6 组合模式好处 198 +第20章 想走?可以!先买票——迭代器模式 200 +20.1 乘车买票,不管你是谁! 200 +20.2 迭代器模式 201 +20.3 迭代器实现 202 +20.4 .NET的迭代器实现 206 +20.5 迭代高手 208 + +# 第21章 有些类也需计划生育——单例模式 +## 21.1 类也需要计划生育 +问题:有些类不需要反复创建实例 + +## 21.2 判断对象是否是null +解决办法:判断对象是否是null , 若是,创造新的实例;否则引用即可。 +- 在不使用实例的时候要即使释放对象或者设置一个标志变量 +- 将获取实例封装成一个方法 + +## 21.3 生还是不生是自己的责任 +```cs +class Singleton +{ + private static Singleton instance; + private static readonly object syncRoot = new object(); + private Singleton() + { + } + + public static Singleton GetInstance() + { + if (instance == null) + { + lock (syncRoot) + { + if (instance == null) + { + instance = new Singleton(); + } + } + } + return instance; + } +} +``` +- 构造函数`Singleton`是`private`的,所以实例化这个对象的任务交给了这个类本身 +- `GetInstance`是`public`的,外部只想着获取一个实例就可以了。 + +## 21.4 单例模式 +```plantuml +@startuml +class Singleton{ + -instance :Singleton + -Singleton() + +GetInstance() +} +@enduml +``` + +## 21.5 多线程时的单例 +在获取实例的时候需要加锁 + +## 21.6 双重锁定 +`if (instance == null)`这个判断需要做两次来避免错误。 + +## 21.7 静态初始化 +饿汉单例模式:自己被加载时就将自己实例化 +懒汉单例模式:要在第一次被引用时 ,才会将自己实例化 + +----- + +第22章 手机软件何时统一——桥接模式 220 +22.1 凭什么你的游戏我不能玩 220 +22.2 紧耦合的程序演化 221 +22.3 合成/聚合复用原则 225 +22.4 松耦合的程序 226 +22.5 桥接模式 229 +22.6 桥接模式基本代码 231 +22.7 我要开发“好”游戏 233 +第23章 烤羊肉串引来的思考——命令模式 234 +23.1 吃烤羊肉串! 234 +23.2 烧烤摊vs.烧烤店 235 +23.3 紧耦合设计 236 +23.4 松耦合设计 237 +23.5 松耦合后 240 +23.6 命令模式 242 +23.7 命令模式作用 244 +第24章 加薪非要老总批?——职责链模式 245 +24.1 老板,我要加薪! 245 +24.2 加薪代码初步 246 +24.3 职责链模式 249 +24.4 职责链的好处 251 +24.5 加薪代码重构 252 +24.6 加薪成功 256 +第25章 世界需要和平——中介者模式 257 +25.1 世界需要和平! 257 +25.2 中介者模式 258 +25.3 安理会做中介 262 +25.4 中介者模式优缺点 265 +第26章 项目多也别傻做——享元模式 267 +26.1 项目多也别傻做! 267 +26.2 享元模式 269 +26.3 网站共享代码 272 +26.4 内部状态与外部状态 274 +26.5 享元模式应用 277 +第27章 其实你不懂老板的心——解释器模式 279 +27.1 其实你不懂老板的心 279 +27.2 解释器模式 280 +27.3 解释器模式好处 282 +27.4 音乐解释器 283 +27.5 音乐解释器实现 284 +27.6 料事如神 289 +第28章 男人和女人——访问者模式 291 +28.1 男人和女人! 291 +28.2 最简单的编程实现 292 +28.3 简单的面向对象实现 293 +28.4 用了模式的实现 295 +28.5 访问者模式 300 +28.6 访问者模式基本代码 301 +28.7 比上不足,比下有余 304 +第29章 OOTV杯超级模式大赛——模式总结 305 +29.1 演讲任务 305 +29.2 报名参赛 305 +29.3 超模大赛开幕式 306 +29.4 创建型模式比赛 309 +29.5 结构型模式比赛 314 +29.6 行为型模式一组比赛 321 +29.7 行为型模式二组比赛 325 +29.8 决赛 330 +29.9 梦醒时分 333 +29.10 没有结束的结尾 334 +附 录 A 培训实习生——面向对象基础 335 +A.1 培训实习生 335 +A.2 类与实例 335 +A.3 构造方法 337 +A.4 方法重载 338 +A.5 属性与修饰符 340 +A.6 封装 342 +A.7 继承 343 +A.8 多态 347 +A.9 重构 350 +A.10 抽象类 353 +A.11 接口 354 +A.12 集合 358 +A.13 泛型 360 +A.14 委托与事件 362 +A.15 客套 366 +附 录 B 参考文献 367 diff --git a/_posts/2021-01-01-dsaa_c++4.md b/_posts/2021-01-01-dsaa_c++4.md new file mode 100644 index 0000000..fdf4842 --- /dev/null +++ b/_posts/2021-01-01-dsaa_c++4.md @@ -0,0 +1,7 @@ +--- +title: 数据结构与算法分析c++描述 +layout: post +categories: 算法 +tags: C++ 算法 书籍 +excerpt: 数据结构与算法分析c++描述 +--- diff --git a/_posts/2021-01-01-postgresql-c1.md b/_posts/2021-01-01-postgresql-c1.md index 0fe9bcc..f9993f8 100644 --- a/_posts/2021-01-01-postgresql-c1.md +++ b/_posts/2021-01-01-postgresql-c1.md @@ -2,7 +2,7 @@ title: PostgreSQL 数据库内核分析-c1 layout: post categories: PostgreSQL -tags: PostgreSQL 代码架构 +tags: PostgreSQL 体系结构 书籍 excerpt: PostgreSQL 数据库内核分析-c1 --- # 1. PostgreSQL代码结构 diff --git a/_posts/2021-01-01-postgresql-c2.md b/_posts/2021-01-01-postgresql-c2.md new file mode 100644 index 0000000..31442aa --- /dev/null +++ b/_posts/2021-01-01-postgresql-c2.md @@ -0,0 +1,635 @@ +--- +title: PostgreSQL 数据库内核分析-c2 +layout: post +categories: PostgreSQL +tags: PostgreSQL 体系结构 +excerpt: PostgreSQL 数据库内核分析-c2 +--- +# 2. 第2章 PostgreSQL的体系结构 +图2-1 PostgreSQL的体系结构 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-04_111936.jpg) + +- 连接管理系统: 接收外部操作对系统的请求,对操作请求进行预处理和分发,起系统逻辑控制作用 +- 编译执行系统: 由查询编译器、查询执行器完成,完成操作请求在数据库中的分析处理和转化工作, 最终时间物理存储介质中数据的操作; +- 存储管理系统由索引管理器、内存管理器、外存管理器组成,负责存储和管理物理数据,提供对编译查询系统的支持; +- 事务系统由事务管理器、日志管理器、并发控制、锁管理器组成,日志管理器和事务管理器完成对操作请求处理的事务一致性支持,锁管理器和并发控制提供对并发访问数据的一致性支持; +- 系统表是PostgreSQL数据库的元信息管理中心, 包括数据库对象信息和数据库管理控制信息。系统表管理元数据信息,将PostgreSQL数据库的各个模块连接在一起,形成一个高效的数据管理系统。 + +## 2.1. 系统表 +由SQL命令关联的系统表操作自动维护系统表信息。 +为提高系统性能,在内存中建立了共享的系统表CACHE,使用Hash函数和Hash表提高查询效率。 + +### 2.1.1. 主要系统表功能及依赖关系 +图2-2 关键系统表之间的相互依赖关系 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-05_175848.jpg) + +### 2.1.2. 系统视图 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-05_180326.jpg) + +`initdb`过程中生成了系统表。 + +## 2.2. 数据集簇 +由PostgreSQL管理的用户数据库以及系统数据库总称。 +`OID`: 对象标识符,对于数据库中的对象如表、索引、视图、类型等,也对应于系统表`pg_class`中的一个元组。 + +初始化数据集簇包括创建包含数据库系统所有数据的数据目录、创建共享的系统表、创建其他的配置文件和控制文件,并创建三个数据库:模板数据库template1和template0、默认的用户数据库`postgres`。 +以后用户创建一个新数据库时,`template1`数据库里的所有内容(包括系统表文件)都会拷贝过来。 + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-05_182238.jpg) +表2-9 `PGDATA`中的子文件和目录 + +### 2.2.1. initdb的使用 +``` +[postgres@pghost1 ~]$ /opt/pgsql/bin/initdb -D /pgdata/10/data -W +The files belonging to this database system will be owned by user "postgres". +This user must also own the server process. +The database cluster will be initialized with locale "en_US.UTF-8". +The default database encoding has accordingly been set to "UTF8". +The default text search configuration will be set to "english". +Data page checksums are disabled. +Enter new superuser password: +Enter it again: +fixing permissions on existing directory /export/pg10_data …… ok +creating subdirectories …… ok +selecting default max_connections …… 100 +selecting default shared_buffers …… 128MB +selecting dynamic shared memory implementation …… posix +creating configuration files …… ok +running bootstrap script …… ok +performing post-bootstrap initialization …… ok +syncing data to disk …… ok +WARNING:enabling "trust" authentication for local connections +You can change this by editing pg_hba.conf or using the option -A, or +——auth-local and ——auth-host, the next time you run initdb. +Success.You can now start the database server using: +/opt/pgsql/bin/pg_ctl -D /pgdata/10/data -l logfile start +[postgres@pghost1 ~]$ +``` + +### 2.2.2. postgres.bki +`postgres.bki`仅用于初始化数据集 +模板数据库`template1`是通过运行在`bootstrap`模式的`Postgres`程序读取`postgres.bki`文件创建的。 + +### 2.2.3. initdb的执行过程 +从`initdb.c`中的`main`函数开始执行。 + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-05_182339.jpg) + +## 2.3. PostgreSQL进程结构 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-05_200450.jpg) + +入口是`Main`模块中的`main`函数,在初始化数据集、启动数据库服务器时,都从这里开始执行。 +最主要的两个进程: 守护进程`Postmaster`和服务进程`Postgres`。 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-05_201252.jpg) +守护进程`Postmaster`为每一个客户端分配一个服务进程`Postgres`。 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-05_201400.jpg) + +## 2.4. 守护进程Postmaster +守护进程`Postmaster`的请求响应模型。 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-05_201615.jpg) +守护进程`Postmaster`及其子进程的通信就通过共享内存和信号来实现,其代码位于`src/backend/postmaster` +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-05_202257.jpg) + + +### 2.4.1. 初始化内存上下文 +内存上下文机制用于避免内存泄露。 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-05_203618.jpg) + +```cpp +// gtm.h +#define TopMemoryContext (GetMyThreadInfo->thr_thread_context) +#define ThreadTopContext (GetMyThreadInfo->thr_thread_context) +#define MessageContext (GetMyThreadInfo->thr_message_context) +#define CurrentMemoryContext (GetMyThreadInfo->thr_current_context) +#define ErrorContext (GetMyThreadInfo->thr_error_context) +``` + + +### 2.4.2. 配置参数 +配置参数的数据结构: +```cpp +struct config_generic { + /* constant fields, must be set correctly in initial value: */ + const char* name; /* name of variable - MUST BE FIRST */ + GtmOptContext context; /* context required to set the variable */ + const char* short_desc; /* short desc. of this variable's purpose */ + const char* long_desc; /* long desc. of this variable's purpose */ + int flags; /* flag bits, see below */ + /* variable fields, initialized at runtime: */ + enum config_type vartype; /* type of variable (set only at startup) */ + int status; /* status bits, see below */ + GtmOptSource reset_source; /* source of the reset_value */ + GtmOptSource source; /* source of the current actual value */ + GtmOptStack* stack; /* stacked prior values */ + void* extra; /* "extra" pointer for current actual value */ + char* sourcefile; /* file current setting is from (NULL if not + * file) */ + int sourceline; /* line in source file */ +}; + +struct config_int { + struct config_generic gen; + /* constant fields, must be set correctly in initial value: */ + int* variable; + int boot_val; + int min; + int max; + GtmOptIntCheckHook check_hook; + GtmOptIntAssignHook assign_hook; + /* variable fields, initialized at runtime: */ + int reset_val; + void* reset_extra; +}; +``` + +配置参数的步骤: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-05_204002.jpg) + +### 2.4.3. 创建监听套接字 +相关数据结构: +```cpp +// 服务器IP地址 +extern char* ListenAddresses; +// 与服务器某个IP地址绑定的监听套接字描述符 +extern int ServerListenSocket[MAXLISTEN]; +// 使用`getaddrinfo`系统函数返回与协议无关的套接字时需要关心的信息 +struct addrinfo { + int ai_flags; + int ai_family; + int ai_socktype; + int ai_protocol; + size_t ai_addrlen; // 地址结构长度 + char* ai_canonname; + struct sockaddr* ai_addr; // 地址指针 + struct addrinfo* ai_next; +}; +``` + +PostgreSQL中的`List`数据结构: +```cpp +typedef struct List { + NodeTag type; /* T_List, T_IntList, or T_OidList */ + int length; + ListCell* head; + ListCell* tail; +} List; + +struct ListCell { + union { + void* ptr_value; + int int_value; + Oid oid_value; + } data; + ListCell* next; +}; +``` + +### 2.4.4. 注册信号处理函数 +三个信号集: +```cpp +// 不希望屏蔽的信号集、要屏蔽的信号集、进行用户连接认证时需要屏蔽的信号集 +extern THR_LOCAL sigset_t UnBlockSig, BlockSig, AuthBlockSig; +``` + +几个主要的信号处理函数的功能: +```cpp +// 配置文件改变产生`SIGHUP`信号 +static void SIGHUP_handler(SIGNAL_ARGS); + (void)gspqsignal(SIGHUP, SIGHUP_handler); /* reread config file and have + +// 处理`SIGINT`/`SIGQUIT`/`SIGTERM` +static void pmdie(SIGNAL_ARGS); + (void)gspqsignal(SIGINT, pmdie); /* send SIGTERM and shut down */ + (void)gspqsignal(SIGQUIT, pmdie); /* send SIGQUIT and die */ + (void)gspqsignal(SIGTERM, pmdie); /* wait for children and shut down */ + +// 用于清除退出子进程的资源 + (void)gspqsignal(SIGCHLD, reaper); /* handle child termination */ +``` + +### 2.4.5. 辅助进程启动 +- SysLogger +- pgstat +- autovacuum + +```cpp +/* + * Postmaster subroutine to start a syslogger subprocess. + * 收集所有的stderr输出,并写入到日志文件 + */ +ThreadId SysLogger_Start(void) + +/* ---------- + * pgstat_init() - + * + * Called from postmaster at startup. Create the resources required + * by the statistics collector process. If unable to do so, do not + * fail --- better to let the postmaster start with stats collection + * disabled. + * ---------- + */ +void pgstat_init(void) + +/* + * Main loop for the autovacuum launcher process. + */ +NON_EXEC_STATIC void AutoVacLauncherMain() +``` + +### 2.4.6. 装载客户端认证文件 +`pg_hba.conf`文件则负责客户端的连接和认证。这两个文件都位于初始化数据目录中。 +``` +# TYPE DATABASE USER ADDRESS METHOD +``` + +`pg_ident.conf`文件是基于身份认证的配置文件。 +``` +# MAPNAME SYSTEM-USERNAME PG-USERNAME +``` + +### 2.4.7. 循环等待客户连接请求 +```cpp +typedef struct Port { + int sock; /* File descriptor */ + CMSockAddr laddr; /* local addr (postmaster) */ + CMSockAddr raddr; /* remote addr (client) */ + char* remote_host; /* name (or ip addr) of remote host */ + char* remote_port; /* text rep of remote port */ + CM_PortLastCall last_call; /* Last syscall to this port */ + int last_errno; /* Last errno. zero if the last call succeeds */ + + char* user_name; + ………… +} Port; + +/* + * Main idle loop of postmaster + */ +static int ServerLoop(void) { + // 初始化读文件描述符集 + nSockets = initMasks(&readmask); + // 等待客户端提出连接请求 + selres = select(nSockets, &rmask, NULL, NULL, &timeout); + // 如果发现该套接字上有用户提出连接请求,调用`ConnCreate`创建一个`Port`结构体 + if (FD_ISSET(t_thrd.postmaster_cxt.ListenSocket[i], &rmask)) { + Port* port = NULL; + ufds[i].revents = 0; + if (IS_FD_TO_RECV_GSSOCK(ufds[i].fd)) { + port = ConnCreateToRecvGssock(ufds, i, &nSockets); + } else { + port = ConnCreate(ufds[i].fd); + } + + if (port != NULL) { + /* + * Since at present, HA only uses TCP sockets, we can directly compare + * the corresponding enty in t_thrd.postmaster_cxt.listen_sock_type, even + * though ufds are not one-to-one mapped to tcp and sctp socket array. + * If HA adopts STCP sockets later, we will need to maintain socket type + * array for ufds in initPollfd. + */ + if (threadPoolActivated && !isConnectHaPort) { + result = g_threadPoolControler->DispatchSession(port); + } else { + // 启动后台进程接替`Postmaster`与客户进行连接 + result = BackendStartup(port, isConnectHaPort); + // 根据Port结构体来启动一个`Postgres`服务进程 + static int BackendStartup(Port* port, bool isConnectHaPort) + { + Backend* bn = NULL; /* for backend cleanup */ + ThreadId pid; + pid = initialize_worker_thread(WORKER, port); + ThreadId initialize_thread(ThreadArg* thr_argv) + int error_code = gs_thread_create(&thread, InternalThreadFunc, 1, (void*)thr_argv); + static void* InternalThreadFunc(void* args) + { + gs_thread_exit((GetThreadEntry(thr_argv->role))(thr_argv)); + static int BackendRun(Port* port) { + // 进入`PostgresMain`的执行入口 + return PostgresMain(ac, av, port->database_name, port->user_name); + } + return (void*)NULL; + } + + /* + * Everything's been successful, it's safe to add this backend to our list + * of backends. + */ + bn->pid = pid; + bn->is_autovacuum = false; + bn->cancel_key = t_thrd.proc_cxt.MyCancelKey; + DLInitElem(&bn->elem, bn); + // 将该`bn`结构体加入到`backend_list`链表中。 + DLAddHead(g_instance.backend_list, &bn->elem); + } + } + } +} +``` + +## 2.5. 辅助进程 +```cpp +typedef struct knl_g_pid_context { + ThreadId StartupPID; + ThreadId TwoPhaseCleanerPID; + ThreadId FaultMonitorPID; + ThreadId BgWriterPID; + ThreadId* CkptBgWriterPID; + ThreadId* PageWriterPID; + ThreadId CheckpointerPID; + ThreadId WalWriterPID; + ThreadId WalReceiverPID; + ThreadId WalRcvWriterPID; + ThreadId DataReceiverPID; + ThreadId DataRcvWriterPID; + ThreadId AutoVacPID; + ThreadId PgJobSchdPID; + ThreadId PgArchPID; + ThreadId PgStatPID; + ThreadId PercentilePID; + ThreadId PgAuditPID; + ThreadId SysLoggerPID; + ThreadId CatchupPID; + ThreadId WLMCollectPID; + ThreadId WLMCalSpaceInfoPID; + ThreadId WLMMonitorPID; + ThreadId WLMArbiterPID; + ThreadId CPMonitorPID; + ThreadId AlarmCheckerPID; + ThreadId CBMWriterPID; + ThreadId RemoteServicePID; + ThreadId AioCompleterStarted; + ThreadId HeartbeatPID; + ThreadId CsnminSyncPID; + ThreadId BarrierCreatorPID; + volatile ThreadId ReaperBackendPID; + volatile ThreadId SnapshotPID; + ThreadId AshPID; + ThreadId StatementPID; + ThreadId CommSenderFlowPID; + ThreadId CommReceiverFlowPID; + ThreadId CommAuxiliaryPID; + ThreadId CommPoolerCleanPID; + ThreadId* CommReceiverPIDS; + ThreadId TsCompactionPID; + ThreadId TsCompactionAuxiliaryPID; +} knl_g_pid_context; +``` + +为辅助线程分配的`PID`。 + + +### 2.5.1. SysLogger系统日志进程 +配置文件中相关的配置项: +```conf +#log_destination = 'stderr' # Valid values are combinations of + # stderr, csvlog, syslog, and eventlog, + # depending on platform. csvlog + # requires logging_collector to be on. + +# This is used when logging to stderr: +logging_collector = on # Enable capturing of stderr and csvlog + # into log files. Required to be on for + # csvlogs. + # (change requires restart) + +# These are only used if logging_collector is on: +#log_directory = 'pg_log' # directory where log files are written, + # can be absolute or relative to PGDATA +log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern, + # can include strftime() escapes +log_file_mode = 0600 # creation mode for log files, + # begin with 0 to use octal notation +#log_truncate_on_rotation = off # If on, an existing log file with the + # same name as the new log file will be + # truncated rather than appended to. + # But such truncation only occurs on + # time-driven rotation, not on restarts + # or size-driven rotation. Default is + # off, meaning append to existing files + # in all cases. +#log_rotation_age = 1d # Automatic rotation of logfiles will + # happen after that time. 0 disables. +log_rotation_size = 20MB # Automatic rotation of logfiles will + # happen after that much log output. + # 0 disables. +``` + +入口函数: +```cpp +/* + * Postmaster subroutine to start a syslogger subprocess. + */ +ThreadId SysLogger_Start(void) { + // 创建管道用于接收stderr的输出 + if (!CreatePipe(&t_thrd.postmaster_cxt.syslogPipe[0], &t_thrd.postmaster_cxt.syslogPipe[1], &sa, 32768)); + + // 创建线程来运行`SYSLOGGER` + sysloggerPid = initialize_util_thread(SYSLOGGER); + /* + * Main entry point for syslogger process + * argc/argv parameters are valid only in EXEC_BACKEND case. + */ + NON_EXEC_STATIC void SysLoggerMain(int fd) + + + /* success, in postmaster */ + if (sysloggerPid != 0) { + // 使用`dump2`系统函数将`stdout`和`stderr`重定向到日志的写入端 + fflush(stdout); + if (dup2(t_thrd.postmaster_cxt.syslogPipe[1], fileno(stdout)) < 0); + fflush(stderr); + if (dup2(t_thrd.postmaster_cxt.syslogPipe[1], fileno(stderr)) < 0); +} +``` + +### 2.5.3 WalWriter进程 +中心思想: 对数据文件的修改必须是只能发生在这些修改已经记录到日志之后, 也就是先写日志后写数据。 + +好处: 显著减少了写磁盘的次数。 + +物理文件位置:`pg_xlog`目录下 + +日志记录格式: `xlog.h` + +日志命名规则: 时间线+ 日志文件标号 + 日志文件段标号 + +内部结构: `WAL`的缓冲区和控制结构在共享内存中, 他们是用轻量的锁保护的, **对共享内存的需求由缓冲区数量决定**。 + +WAL相关参数: +- fsync +**参数说明** : 设置openGauss服务器是否使用fsync()系统函数(请参见[wal_sync_method]确保数据的更新及时写入物理磁盘中。 +- synchronous_commit +**参数说明:**设置当前事务的同步方式。 +- wal_sync_method +**参数说明:**设置向磁盘强制更新WAL数据的方法。 +- wal_buffers +**参数说明:**设置用于存放WAL数据的共享内存空间的XLOG_BLCKSZ数,XLOG_BLCKSZ的大小默认为8KB。 该参数受`wal_writer_delay`/`commit_delay`的影响。 +- wal_writer_delay +**参数说明:**WalWriter进程的写间隔时间。 +- commit_delay +**参数说明:**表示一个已经提交的数据在WAL缓冲区中存放的时间。 +- commit_siblings +**参数说明:**当一个事务发出提交请求时,如果数据库中正在执行的事务数量大于此参数的值,则该事务将等待一段时间([commit_delay] + +相关函数: +- 启动函数: `StartWalWriter` +- 实际工作函数: `WalWriterMain` +工作函数流程如下所示: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-04-09_074806.jpg) + + +在线备份和恢复: +把数据文件和`WAL`日志文件保存到另一个存储设备上, 这个动作叫归档, 归档后日志文件可以删除。可以使用`pg_dump`工具来`dump`备份数据库文件。 + +## 2.6. 服务进程Postgres +服务进程Postgres是实际的接收查询请求并调用相应模块处理查询的`PostgreSQL`服务进程。 +| 文件 | 作用 | +|--------------|---------------------------------------| +| postgres.cpp | 管理查询的整体流程 | +| pquery.cpp | 执行一个分析好的查询命令 | +| utility.cpp | 执行各种非查询命令 | +| dest.cpp | 处理Postgres和远端客户的一些消息通信操作,并负责返回命令的执行结果 | + +图2-15 `PostgresMain`函数流程 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_111808.jpg) + +### 2.6.1. 初始化内存环境 +调用`MemoryContextInit`来完成 + +### 2.6.2. 配置运行参数和处理客户端传递的GUC参数 +将参数设置成默认值、根据命令行参数配置参数、读配置文件重新设置参数。 + +### 2.6.3. 设置信号处理和信号屏蔽 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_112755.jpg) + +### 2.6.4. 初始化Postgres的运行环境 +- 设置`DataDir`目录 +- `BaseInit`完成基本初始化,完成`Postgres`进程的内存、信号量以及文件句柄的创建和初始化工作 + - InitCommunication(); + Attach to shared memory and semaphores, and initialize our input/output/debugging file descriptors. + - DebugFileOpen(); + 初始化`input`/`output`/`debugging`文件描述符 + - InitFileAccess(); + Do local initialization of file, storage and buffer managers + 初始化文件访问 + - smgrinit(); + 初始化或者关闭存储管理器 + - InitBufferPoolAccess(); + 初始化共享缓冲区存储区 + +- `InitPostgres`完成Postgres的初始化 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_113513.jpg) + +### 2.6.5. 创建内存上下文并设置查询取消跳跃点 +相关数据结构: +```cpp +#define MessageContext (GetMyThreadInfo->thr_message_context) +``` + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_113628.jpg) + +### 2.6.6. 循环等待处理查询 + +相关数据结构: +```cpp + // 用来接收服务器发送给客户端的消息的缓冲区 + char PqSendBuffer[PQ_BUFFER_SIZE]; + int PqSendPointer; /* Next index to store a byte in PqSendBuffer */ + + // 用来接收客户端发送给服务器的消息的缓冲区 + char PqRecvBuffer[PQ_BUFFER_SIZE]; + int PqRecvPointer; /* Next index to read a byte from PqRecvBuffer */ + int PqRecvLength; /* End of data available in PqRecvBuffer */ +``` +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_113955.jpg) +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_113947.jpg) + +**消息的类型**: 对不同类型调用响应的函数来具体处理这些消息 +- 启动消息:开始一个会话 +- 简单查询:`SQL`命令 +对应类型为`Q`的查询会调用`exec_simple_query`函数来处理,实际上增删改查都是从这里开始执行的。 +- 扩展查询 +- 函数调用 +- 取消正在处理的请求 +- 终止 + +**主要流程**: +```cpp + for (;;) { + /* 释放上次查询时的内存 + * Release storage left over from prior query cycle, and create a new + * query input buffer in the cleared t_thrd.mem_cxt.msg_mem_cxt. + */ + + /* + * (1) If we've reached idle state, tell the frontend we're ready for + * a new query. + * + * Note: this includes fflush()'ing the last of the prior output. + * + * This is also a good time to send collected statistics to the + * collector, and to update the PS stats display. We avoid doing + * those every time through the message loop because it'd slow down + * processing of batched messages, and because we don't want to report + * uncommitted updates (that confuses autovacuum). The notification + * processor wants a call too, if we are not in a transaction block. + */ + + if (send_ready_for_query) { + if (IS_CLIENT_CONN_VALID(u_sess->proc_cxt.MyProcPort)) + // 告诉客户端它已经准备好接收查询了 + ReadyForQuery((CommandDest)t_thrd.postgres_cxt.whereToSendOutput); + } + + /* + * (2) Allow asynchronous signals to be executed immediately if they + * come in while we are waiting for client input. (This must be + * conditional since we don't want, say, reads on behalf of COPY FROM + * STDIN doing the same thing.) + */ + t_thrd.postgres_cxt.DoingCommandRead = true; + + /* + * (3) read a command (loop blocks here) + */ + if (saved_whereToSendOutput != DestNone) + t_thrd.postgres_cxt.whereToSendOutput = saved_whereToSendOutput; + + firstchar = ReadCommand(&input_message); + static int ReadCommand(StringInfo inBuf) { + // 客户端通过网络连接 + if (t_thrd.postgres_cxt.whereToSendOutput == DestRemote) + result = SocketBackend(inBuf); + // 客户端和服务器在同一台服务器上 + else if (t_thrd.postgres_cxt.whereToSendOutput == DestDebug) + result = InteractiveBackend(inBuf); + } + } +``` + + +### 2.6.7. 简单查询的执行流程 +`exec_simple_query`函数的执行流程: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_121517.jpg) + +#### 2.6.7.1. 编译器 +扫描用户输入的字符串形式的命令,检查其合法性并将其转换为`Postgres`定义的内部数据结构。 +入口函数为`pg_parse_query`。 + +#### 2.6.7.2. 分析器 +接收编译其传递过来的各种命令数据结构, 对他们进行相应的处理,最终转换为统一的数据结构`Query`。 +分析器的入口函数是`parse_analyze`。 +如果是查询命令,在生成`Query`后进行规则重写,重写部分的入口是`QueryRewrite`函数。 + +#### 2.6.7.3. 优化器 +接收分析器输出的`Query`结构体,进行优化处理后,输出执行器可以执行的计划(`Plan`) +入口函数: `pg_plan_query` + +#### 2.6.7.4. 执行器 +- 非查询命令的入口函数: `ProcessUtility` +- 查询命令的入口函数: `ProcessQuery` + +----------- + +`Postgres`后台进程的执行实现了`PostgreSQL`的多任务并发执行。 diff --git a/_posts/2021-01-01-postgresql-c3.md b/_posts/2021-01-01-postgresql-c3.md new file mode 100644 index 0000000..d70c388 --- /dev/null +++ b/_posts/2021-01-01-postgresql-c3.md @@ -0,0 +1,813 @@ +--- +title: PostgreSQL 数据库内核分析-c3 +layout: post +categories: PostgreSQL +tags: PostgreSQL 存储 书籍 +excerpt: PostgreSQL 数据库内核分析-c3 +--- +# 3. 第3章 存储管理 +## 3.1. 存储管理器的体系结构 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-25_203318.jpg) + +`PG`提供了轻量级锁,用于支持对共享内存中同一数据的互斥访问。 + +存储管理器的主要任务: + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-25_204308.jpg) +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-25_204321.jpg) + + + +以读写元组的过程为例, 说明各个模块的使用顺序: + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-25_204745.jpg) + +- 设置`cache`来存储系统表元组和表的基本信息; +- 首先从共享缓冲池查找所需文件对应的缓冲块 +- 缓冲区和存储介质的交互式通过存储介质管理器`SMGR`来进行的,这是外存管理的核心 +- 在磁盘管理器与物理文件之间还存在一层虚拟文件描述符`VFD`机制,防止进程打开的文件数超过操作系统限制,通过合理使用有限个实际文件描述符来满足无限的`VFD`访问需求。 +- 在写入元组的时候,需要使用一定策略找到一个具有合适空闲空间的缓冲块,然后将该元组装填到缓冲块中。系统会在适当的时候将这些被写入数据的缓冲块刷回到存储介质中。 +- 删除元组: 在元组上作删除标记,物理清除工作由`VACUUM`策略来完成。 + +## 3.2. 外存管理 + +外存管理体系结构: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-26_081553.jpg) + +### 3.2.1. 表和元组的组织方式 +**堆文件的概念:** +同一个表中的元组按照创建顺序依次插入到表文件中,元组之间不进行关联,这样的表文件称为堆文件。 + +**堆文件的分类:** +- 普通堆 +- 临时堆 : 会话过程中临时创建,会话结束自动删除 +- 序列 : 元组值自动正常的特殊堆 +- TOAST表 : 专门用于存储变长数据。 + +堆文件的物理结构如下: + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-03_095605.jpg) + +`Linp`的类型是`ItemIdData`,如下所示: +```cpp +typedef struct ItemIdData { + unsigned lp_off : 15, /* offset to tuple (from start of page) */ + lp_flags : 2, /* state of item pointer, see below */ + lp_len : 15; /* byte length of tuple */ +} ItemIdData; +``` + +使用`ItemIdData`来指向文件块中的一个元组。元组信息除了存放元组的实际数据,还存放元组头部信息, 通过`HeapTupleHeaderData`来描述, 在其中包含了删除标记位。 +```cpp +typedef struct HeapTupleHeaderData { + union { + // 记录对元组执行操作的事务ID和命令ID, 用于并发控制时检查元组对事务的可见性 + HeapTupleFields t_heap; + // 记录元组的长度等信息 + DatumTupleFields t_datum; + } t_choice; + + ItemPointerData t_ctid; /* current TID of this or newer tuple */ + + /* Fields below here must match MinimalTupleData! */ + + uint16 t_infomask2; /* number of attributes + various flags */ + + uint16 t_infomask; /* various flag bits, see below */ + + uint8 t_hoff; /* sizeof header incl. bitmap, padding */ + + /* ^ - 23 bytes - ^ */ + + bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* bitmap of NULLs -- VARIABLE LENGTH */ + + /* MORE DATA FOLLOWS AT END OF STRUCT */ +} HeapTupleHeaderData; +``` + +`PostgreSQL`中使用了`HOT`技术, 用于在对元组采用多版本技术存储时占用存储过多的问题, 如果更新操作没有修改索引属性, 就不生成拥有完全相同键值的索引记录。 + +### 3.2.2. 磁盘管理器 +主要实现在`md.c`, 通过`VFD`机制来进行文件操作。 +```cpp +typedef struct _MdfdVec { + // 该文件对应的`VFD` + File mdfd_vfd; /* fd number in fd.c's pool */ + // 这个文件是一个大表文件的第几段 + BlockNumber mdfd_segno; /* segment number, from 0 */ + struct _MdfdVec *mdfd_chain; /* next segment, or NULL */ +} MdfdVec; +``` + +### 3.2.3. VFD机制 +绕过操作系统对于文件描述符数量的限制。 + +#### 3.2.3.1. 实现原理 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-03_103200.jpg) + +当进程申请打开一个文件时,返回一个虚拟文件描述符,该描述符指的是一个`VFD`数据结构 +一个进程打开的`VFD`存储在`VfdCache`中。 +```cpp +typedef struct vfd +{ + int fd; /* current FD, or VFD_CLOSED if none */ + // 用于决定在关闭此文件时是要同步修改还是删除该文件 + unsigned short fdstate; /* bitflags for VFD's state */ + ResourceOwner resowner; /* owner, for automatic cleanup */ + File nextFree; /* link to next free VFD, if in freelist */ + // 指向比该VFD最近更常用的虚拟文件描述符 + File lruMoreRecently; /* doubly linked recency-of-use list */ + // 指向比该VFD最近更不常用的虚拟文件描述符 + File lruLessRecently; + off_t seekPos; /* current logical file position */ + off_t fileSize; /* current size of file (0 if not temporary) */ + char *fileName; /* name of file, or NULL for unused VFD */ + /* NB: fileName is malloc'd, and must be free'd when closing the VFD */ + int fileFlags; /* open(2) flags for (re)opening the file */ + int fileMode; /* mode to pass to open(2) */ +} Vfd; +static Vfd *VfdCache; +``` + +#### 3.2.3.2. LRU池 +每个进程都是用一个`LRU`池来管理所有已经打开的`VFD`, 当`LRU`池未满时,进程打开的文件个数未超过操作系统限制;当`LRU`池满时,进程需要首先关闭一个`VFD`,使用替换最长时间未使用`VFD`的策略。 + +`LRU`池的数据结构如下所示: + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-03_104250.jpg) + +### 3.2.4. 空闲空间映射表 +对于每个表文件, 同时创建一个名为`关系表OID_fsm`的文件,用于记录该表的空闲空间大小, 称之为空闲空间映射表文件`FSM`。 +使用一个字节来记录空闲空间范围大小。 + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-03_104819.jpg) + +为了实现快速查找, `FSM`文件使用树结构来存储每个表块的空闲空间, +块之间使用了一个三层树的结构, 第0层和第1层为辅助层,第2层FSM块用于实际存放各表块的空闲空间值。 + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-04_094153.jpg) + +每个`FSM`块大约可以保存`4000`个叶子节点, 三层树结构总共可以存储`4000^3`个叶子节点。 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-04_103812.jpg) + +### 3.2.5. 可见性映射表 +为了能够加快`VACUUM`查找包含无效元组的文件块的过程,在表中为表文件定义了一个新的附属文件——可见性映射表(VM)。 + +### 3.2.6. 大数据存储 + +## 3.3. 内存管理 +存储管理的本质问题:如何减少`IO`次数。要尽可能让最近使用的文件块停留在内存中,这样能有效减少磁盘IO代价。 + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-04_105119.jpg) + +### 3.3.1. 内存上下文概述 +内存上下文用于解决在以指针传值时造成的内存泄漏问题。 +系统的内存分配操作在各种语义的内存上下文中进行,所有在内存上下文中分配的内存空间都通过内存上下文进行记录。 +一个内存上下文实际上相当于一个进程环境, 进程环境中调用库函数(`malloc`/`free`/`realloc`)执行内存操作, 类似的, 内存上下文使用(`palloc`/`pfree`/`repalloc`)函数进行内存操作。 + +#### 3.3.1.1 MemoryContext +每一个子进程拥有多个私有的内存上下文,每个子进程的内存上下文组成一个树形结构。 + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-04_110158.jpg) + +```cpp +typedef struct MemoryContextData +{ + NodeTag type; /* identifies exact kind of context */ + MemoryContextMethods *methods; /* virtual function table */ + MemoryContext parent; /* NULL if no parent (toplevel context) */ + MemoryContext firstchild; /* head of linked list of children */ + MemoryContext nextchild; /* next child of same parent */ + char *name; /* context name (just for debugging) */ + bool isReset; /* T = no space alloced since last reset */ +} MemoryContextData; + +typedef struct MemoryContextMethods { + // 内存上下文操作的函数指针 + void* (*alloc)(MemoryContext context, Size size); + /* call this free_p in case someone #define's free() */ + void (*free_p)(MemoryContext context, void* pointer); + void* (*realloc)(MemoryContext context, void* pointer, Size size); + void (*init)(MemoryContext context); + void (*reset)(MemoryContext context); + void (*delete_context)(MemoryContext context); + Size (*get_chunk_space)(MemoryContext context, void* pointer); + bool (*is_empty)(MemoryContext context); + void (*stats)(MemoryContext context, int level); +#ifdef MEMORY_CONTEXT_CHECKING + void (*check)(MemoryContext context); +#endif +} MemoryContextMethods; + +/* + * This is the virtual function table for AllocSet contexts. + */ +static MemoryContextMethods AllocSetMethods = { + AllocSetAlloc, + AllocSetFree, + AllocSetRealloc, + AllocSetInit, + AllocSetReset, + AllocSetDelete, + AllocSetGetChunkSpace, + AllocSetIsEmpty, + AllocSetStats +#ifdef MEMORY_CONTEXT_CHECKING + ,AllocSetCheck +#endif +}; +``` + +`MemoryContextMethods`方法是对`MemoryContext`类型的上下文执行的一系列操作。 + +```plantuml +@startuml +MemoryContext <|-- AllocSetContext +MemoryContextMethods <|-- AllocSetMethods +@enduml +``` + +在任何时候,都有一个当前的`MemoryContext`,其用`CurretMemoryContext`表示。 +通过`MemoryContextSwitchTo`函数来修改当前上下文。 + +内存上下文的组织结构如下: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-04_113319.jpg) + +层次结构如下: +- AllocSet + - AllocBlockData + - chunk + +参考[PostgreSQL内存上下文学习](https://niyanchun.com/memorycontext-in-postgresql.html), 内存块(block)和内存片(chunk)的区别如下: + +AllocSetContext中有两个数据结构AllocBlock和AllocChunk,分别代表内存块和内存片,PostgreSQL就是使用内存块和内存片来管理具体的内存的。AllocSet所管理的内存区域包含若干个内存块(内存块用AllocBlockData结构表示),每个内存块又被分为多个内存片(用AllocChunkData结构表示)单元。我们一般认为内存块是“大内存”,而内存片是“小内存”,PostgreSQL认为大内存使用频率低,小内存使用频率高。所以申请大内存(内存块)使用的是malloc,并且使用完以后马上会释放给操作系统,而申请小内存(内存片)使用的是自己的一套接口函数,而且使用完以后并没有释放给操作系统,而是放入自己维护的一个内存片管理链表,留给后续使用。 + + +```cpp +typedef AllocSetContext *AllocSet; +typedef struct AllocSetContext +{ + MemoryContextData header; /* Standard memory-context fields */ + /* Info about storage allocated in this context: */ + AllocBlock blocks; /* head of list of blocks in this set */ + AllocChunk freelist[ALLOCSET_NUM_FREELISTS]; /* free chunk lists */ + /* Allocation parameters for this context: */ + Size initBlockSize; /* initial block size */ + Size maxBlockSize; /* maximum block size */ + Size nextBlockSize; /* next block size to allocate */ + Size allocChunkLimit; /* effective chunk size limit */ + AllocBlock keeper; /* if not NULL, keep this block over resets */ +} AllocSetContext; + +// 内存块使用`AllocBlockData`数据结构表示1 +typedef struct AllocBlockData +{ + AllocSet aset; /* aset that owns this block */ + AllocBlock next; /* next block in aset's blocks list */ + char *freeptr; /* start of free space in this block */ + char *endptr; /* end of space in this block */ +} AllocBlockData; +``` + +核心数据结构为AllocSetContext,这个数据结构有3个成员MemoryContextData header;和AllocBlock blocks;和AllocChunk freelist[ALLOCSET_NUM_FREELISTS];。这3个成员把内存管理分为3个层次。 +- 第1层是MemoryContext,MemoryContext管理上下文之间的父子关系,设置MemoryContext的内存管理函数。 +- 第2层为AllocBlock blocks,把所有内存块通过双链表链接起来。 +- 第3层为具体的内存单元chunk,内存单元Chunk是从内存块AllocBlock内部分配的,内存块和内存单元chunk的转换关系为:AllocChunk chunk = (AllocChunk)(((char*)block) + ALLOC_BLOCKHDRSZ);和AllocBlock block = (AllocBlock)(((char*)chunk) - ALLOC_BLOCKHDRSZ);。 +内存单元chunk经过转换得到最终的用户指针,内存单元chunk和用户指针的转换关系为:((AllocPointer)(((char*)(chk)) + ALLOC_CHUNKHDRSZ))和((AllocChunk)(((char*)(ptr)) - ALLOC_CHUNKHDRSZ))。 + + +#### 3.3.1.2 内存上下文初始化与创建 +由函数`MemoryContextInit`完成。 +先创建根节点`TopMemoryContext`, 然后在该节点下创建子节点`ErrorContext`用于错误恢复处理。 + +内存上下文的创建由`AllocSetContextCreate`实现: +- 创建内存上下文节点: `MemoryContextCreate` + - 从`TopMemoryContext`节点中分配一块内存用于存放内存上下文节点 + - 初始化`MemoryContext`节点 + `context = (AllocSet)MemoryContextCreate(type, sizeof(AllocSetContext), parent, name, __FILE__, __LINE__);` +- 分配内存块 + - 填充`context`结构体 + 根据最大块大小`maxBlockSize`来设置`allocChunkLimit`的值,在这里`allocChunkLimit`的决定函数是`f(ALLOC_CHUNK_LIMIT, ALLOC_CHUNKHDRSZ, ALLOC_BLOCKHDRSZ, ALLOC_CHUNK_FRACTION, maxBlockSize)`。 + - 如果`minContextSize > ALLOC_BLOCKHDRSZ + ALLOC_CHUNKHDRSZ`,调用标准库函数分配内存块, 预分配的内存块将计入到内存上下文的`keeper`字段中, 作为内存上下文的保留快, 以便重置内存上下文的时候, 该内存块不会被释放。 + +完成上下文创建之后, 可以在该内存上下文进行内存分配, 通过`MemoryContextAlloc`和`MemoryContextAllocZero`两个接口函数来完成。 + +#### 3.3.1.3 内存上下文中内存的分配 +> 参考: [PostgreSQL内存上下文](http://shiyuefei.top/2018/01/11/PostgreSQL%E5%86%85%E5%AD%98%E4%B8%8A%E4%B8%8B%E6%96%87/) + + +|函数|功能| +|---|----| +|MemoryContextCreate|创建上下文节点| +|AllocSetContextCreate|创建上下文实例| +|MemoryContextDelete|删除内存上下文| +|MemoryContextReset|重置内存上下文| +|MemoryContextSwitchTo|切换当前上下文| +|palloc|在当前上下文中申请内存, 调用了当前内存上下文的`methods`字段中指定的`alloc`函数,调用了`AllocSetAlloc`| +|pfree|释放内存, 调用了当前内存上下文的`methods`字段中指定的`free_p`函数| +|repalloc|在当前上下文中重新申请内存, 调用了当前内存上下文的`methods`字段中指定的`ralloc`函数| + + + + +#### 3.3.1.4 内存上下文中的内存重分配 +#### 3.3.1.5 释放内存上下文 + + + + +### 3.3.2. 高速缓存 +设立高速缓存提高访问效率,用于存放系统表元组(`SysCache`)和表模式信息(`RelCache`)。 +`RelCache`中存放的是`RelationData`数据结构。 + +#### 3.3.2.1 SysCache + +系统表数据结构: +```cpp +typedef struct catcache +{ + int id; /* cache identifier --- see syscache.h */ + struct catcache *cc_next; /* link to next catcache */ + const char *cc_relname; /* name of relation the tuples come from */ + Oid cc_reloid; /* OID of relation the tuples come from */ + Oid cc_indexoid; /* OID of index matching cache keys */ + bool cc_relisshared; /* is relation shared across databases? */ + TupleDesc cc_tupdesc; /* tuple descriptor (copied from reldesc) */ + int cc_ntup; /* # of tuples currently in this cache */ + int cc_nbuckets; /* # of hash buckets in this cache */ + int cc_nkeys; /* # of keys (1..CATCACHE_MAXKEYS) */ + int cc_key[CATCACHE_MAXKEYS]; /* AttrNumber of each key */ + PGFunction cc_hashfunc[CATCACHE_MAXKEYS]; /* hash function for each key */ + ScanKeyData cc_skey[CATCACHE_MAXKEYS]; /* precomputed key info for + * heap scans */ + bool cc_isname[CATCACHE_MAXKEYS]; /* flag "name" key columns */ + Dllist cc_lists; /* list of CatCList structs */ +#ifdef CATCACHE_STATS + long cc_searches; /* total # searches against this cache */ + long cc_hits; /* # of matches against existing entry */ + long cc_neg_hits; /* # of matches against negative entry */ + long cc_newloads; /* # of successful loads of new entry */ + + /* + * cc_searches - (cc_hits + cc_neg_hits + cc_newloads) is number of failed + * searches, each of which will result in loading a negative entry + */ + long cc_invals; /* # of entries invalidated from cache */ + long cc_lsearches; /* total # list-searches */ + long cc_lhits; /* # of matches against existing lists */ +#endif + Dllist cc_bucket[1]; /* hash buckets --- VARIABLE LENGTH ARRAY */ +} CatCache; /* VARIABLE LENGTH STRUCT */ +``` + +系统表的数据结构是一个以`CatCache`为元素的数组 + +```cpp +static CatCache *SysCache[ + lengthof(cacheinfo)]; +static int SysCacheSize = lengthof(cacheinfo); +static const struct cachedesc cacheinfo[] = { + {AggregateRelationId, /* AGGFNOID */ + AggregateFnoidIndexId, + 1, + { + Anum_pg_aggregate_aggfnoid, + 0, + 0, + 0 + }, + 32 + } +} +``` + +`cachedesc`结构如下所示: + +```cpp +struct cachedesc +{ + Oid reloid; /* OID of the relation being cached */ + Oid indoid; /* OID of index relation for this cache */ + int nkeys; /* # of keys needed for cache lookup */ + int key[4]; /* attribute numbers of key attrs */ + int nbuckets; /* number of hash buckets for this cache */ +}; +``` + + + +##### 3.3.2.1.1 SysCache 初始化 +`SysCache`的初始化过程: +- InitCatalogCache + - 循环调用`InitCatCache`根据`cacheinfo`中的每一个元素生成`CatCache`结构 + - 分配内存 + - 填充字段 +- InitCatalogCachePhase2 + - 依次完善`SysCache`数组中的`CatCache`结构 + - 根据对应的系统表填充`CatCache`结构中的成员。 + + +##### 3.3.2.1.2 CatCache 中缓存元组的组织 +对应结构: + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-04-15_112759.jpg) +##### 3.3.2.1.3 在 CatCache 中查找元组 + +`SearchCatCache`的作用: 在一个给定的`CatCache`中查找元组。 + +主要流程: + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-08_095857.jpg) + + + +#### 3.3.2.2 RelCache +- `RelCache`的初始化 +- `RelCache`的插入 +- `RelCache`的查询 +- `RelCache`的删除 + +### 3.3.2.3 Cache同步 +每个进程都有属于自己的`Cache`, 当某个`Cache`中的一个元组被删除或更新时,需要通知其他进程对其`Cache`进行同步。PG会记录下已经被删除的无效元组,并通过`SI Message`方式(共享消息队列方式)在进程之间传递这一消息。 + + +`SI Message`的数据结构如下所示: + +```cpp +typedef union +{ + int8 id; /* type field --- must be first */ + SharedInvalCatcacheMsg cc; + SharedInvalCatalogMsg cat; + SharedInvalRelcacheMsg rc; + SharedInvalSmgrMsg sm; + SharedInvalRelmapMsg rm; +} SharedInvalidationMessage; +``` + +三种无效消息: +- `SysCache`中元组无效 +- `RelCache`中元组无效 +- `SMGR`无效(关闭表文件) + +------- + +进程通过调用函数`CacheInvalidateHeapTuple`对无效消息进行注册。 + + + + +### 3.3.3. 缓冲池管理 +如果需要访问的系统表元组在`Cache`中无法找到或者需要访问普通表的元组,就需要对缓冲池进行访。 + +需要访问的数据以 磁盘块 为单位调用函数`smgrread`写入缓冲池, `smgrwrite`将缓冲池数据写回到磁盘。 调入缓冲池中的磁盘块称为缓冲区、缓冲块或者页面, 多个缓冲区组成了缓冲池。 + +对共享缓冲区的管理采取了静态方式:在系统配置时规定好了共享缓冲区的总数 +``` +int NBuffers = 64; +``` + +> 我的小问题: 当前系统的`NBuffers`是多少? + +通过追踪对应的数据结构, + +```cpp +knl_instance_context g_instance; + +knl_instance_context { + knl_instance_attr_t attr; +} + +typedef struct knl_instance_attr { + + knl_instance_attr_sql attr_sql; + knl_instance_attr_storage attr_storage; + knl_instance_attr_security attr_security; + knl_instance_attr_network attr_network; + knl_instance_attr_memory attr_memory; + knl_instance_attr_resource attr_resource; + knl_instance_attr_common attr_common; + +} knl_instance_attr_t; + +typedef struct knl_instance_attr_storage { + int NBuffers; +} +``` + +答案是 +``` +p g_instance.attr.attr_storage.NBuffers + +4096 +``` + +----- + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-08_102424.jpg) + + +分类: +- 共享缓冲池: 要用作普通可共享表的操作场所 +- 本地缓冲池: 仅本地可见的临时表的操作场所 + +对缓冲池的管理通过`pin`机制和`lock`机制完成。 +- `pin`是缓冲区的访问计数器, 保存在缓冲区的`refcount`属性 + ```cpp + typedef struct sbufdesc + { + BufferTag tag; /* ID of page contained in buffer */ + BufFlags flags; /* see bit definitions above */ + uint16 usage_count; /* usage counter for clock sweep code */ + unsigned refcount; /* # of backends holding pins on buffer */ + int wait_backend_pid; /* backend PID of pin-count waiter */ + + slock_t buf_hdr_lock; /* protects the above fields */ + + int buf_id; /* buffer's index number (from 0) */ + int freeNext; /* link in freelist chain */ + + LWLockId io_in_progress_lock; /* to wait for I/O to complete */ + LWLockId content_lock; /* to lock access to buffer contents */ + } BufferDesc; + + ``` + +- `lock`机制为缓冲区的并发访问提供了`EXCLUSIVE`锁和`SHARE`锁 + +#### 3.3.3.1 初始化共享缓冲池 +`InitBufferPool`函数: 在共享缓冲区管理中, 使用了全局数组`BufferDescriptors`来管理池中的缓冲区。 +```cpp +#define GetBufferDescriptor(id) (&t_thrd.storage_cxt.BufferDescriptors[(id)].bufferdesc) + +typedef struct knl_t_storage_context { + /* + * Bookkeeping for tracking emulated transactions in recovery + */ + TransactionId latestObservedXid; + struct RunningTransactionsData* CurrentRunningXacts; + struct VirtualTransactionId* proc_vxids; + + union BufferDescPadded* BufferDescriptors; + // 存储缓冲池的起始地址 + char* BufferBlocks; +} + +// 对缓冲区进行描述 +typedef struct BufferDesc { + BufferTag tag; /* ID of page contained in buffer */ + /* state of the tag, containing flags, refcount and usagecount */ + pg_atomic_uint32 state; + + int buf_id; /* buffer's index number (from 0) */ + + ThreadId wait_backend_pid; /* backend PID of pin-count waiter */ + + LWLock* io_in_progress_lock; /* to wait for I/O to complete */ + LWLock* content_lock; /* to lock access to buffer contents */ + + /* below fields are used for incremental checkpoint */ + pg_atomic_uint64 rec_lsn; /* recovery LSN */ + volatile uint64 dirty_queue_loc; /* actual loc of dirty page queue */ +} BufferDesc; +``` + +数组元素的个数是缓冲区的总数, `BufferDescriptors`就是共享缓冲池。 + +缓冲区初始化流程: + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-04-09_114542.jpg) + +- 在共享内存中开辟内存 +- `InitBufferPool` 初始化缓冲区描述符, 建立`Hash`表和`FreeList`结构 + - `ShmemInitHash`初始化缓冲区Hash表 + + +#### 3.3.3.2 共享缓冲区查询 +查询时, 以`BufferTag`作为索引键,查找Hash表并返回目标缓冲区在缓冲区描述符数组中的位置。 +```cpp +typedef struct buftag +{ + RelFileNode rnode; /* physical relation identifier */ + ForkNumber forkNum; + BlockNumber blockNum; /* blknum relative to begin of reln */ +} BufferTag; +``` + +缓冲区读取流程 +```cpp +Buffer ReadBuffer(Relation reln, BlockNumber block_num) +{ + return ReadBufferExtended(reln, MAIN_FORKNUM, block_num, RBM_NORMAL, NULL); + buf = ReadBuffer_common(reln->rd_smgr, reln->rd_rel->relpersistence, fork_num, block_num, mode, strategy, &hit); + // 执行缓冲区替换策略 + bufHdr = BufferAlloc(smgr, relpersistence, forkNum, blockNum, strategy, &found); +} + +``` + +`ReadBuffer_common`函数是所有缓冲区读取的通用函数, 其流程: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-08_104333.jpg) + +`BufferAlloc`函数用于获得指定的共享缓冲区, 其流程: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-08_104404.jpg) + +#### 3.3.3.3 共享缓冲区替换策略 +##### 3.3.3.3.1 一般的缓冲区替换策略 +在缓冲区中维护一个`FreeList`, `BufferStrategyControl`结构用于对`FreeList`进行控制。 +```cpp +typedef struct +{ + /* Clock sweep hand: index of next buffer to consider grabbing */ + int nextVictimBuffer; + + int firstFreeBuffer; /* Head of list of unused buffers */ + int lastFreeBuffer; /* Tail of list of unused buffers */ + + /* + * NOTE: lastFreeBuffer is undefined when firstFreeBuffer is -1 (that is, + * when the list is empty) + */ + + /* + * Statistics. These counters should be wide enough that they can't + * overflow during a single bgwriter cycle. + */ + uint32 completePasses; /* Complete cycles of the clock sweep */ + uint32 numBufferAllocs; /* Buffers allocated since last reset */ + + /* + * Notification latch, or NULL if none. See StrategyNotifyBgWriter. + */ + Latch *bgwriterLatch; +} BufferStrategyControl; +``` + +在函数`StrategyGetBuffer`中, 当`FreeList`无法找到合适的缓冲区时, 通过`nextVictimBuffer`对所有缓冲区进行扫描,直到找到空闲缓冲区, 该过程使用`clock-sweep`算法实现。 + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-08_105951.jpg) + +##### 3.3.3.3.2 缓冲环替换策略 +环形缓冲区可以使内存中的缓冲区可以重复利用, 通过数据结构`BufferAccessStrategyData`来控制。 +```cpp +typedef struct BufferAccessStrategyData +{ + /* Overall strategy type */ + BufferAccessStrategyType btype; + /* Number of elements in buffers[] array */ + int ring_size; + + /* + * Index of the "current" slot in the ring, ie, the one most recently + * returned by GetBufferFromRing. + */ + int current; + + /* + * True if the buffer just returned by StrategyGetBuffer had been in the + * ring already. + */ + bool current_was_in_ring; + + /* + * Array of buffer numbers. InvalidBuffer (that is, zero) indicates we + * have not yet selected a buffer for this ring slot. For allocation + * simplicity this is palloc'd together with the fixed fields of the + * struct. + */ + Buffer buffers[1]; /* VARIABLE SIZE ARRAY */ +} BufferAccessStrategyData; +``` + +#### 3.3.3.4 本地缓冲区管理 +通过`InitLocalBuffer`来完成, 在使用时创建缓冲区。 + +缓冲区的获取主要通过调用`LocalBufferAlloc`函数来完成。 + +`LocalBufferAlloc`函数流程图如下所示: + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-08_111804.jpg) + +### 3.3.4. IPC +`IPC`有多重实现方法, 包括文件、`Socket`、共享内存等。`PostgreSQL`中的`IPC`主要采用共享内存的方式来实现。 + +PG的`IPC`机制提供的功能: +- 进程和`Postermaster`的通信机制 +- 统一管理进程的相关变量和函数 +- 提供了`SI message`机制 +- 有关清楚的函数 + +#### 3.3.4.1 共享内存管理 +在初始化过程中, 为共享内存创建了一个名为`ShmemIndex`的哈希索引。 +```cpp +static HTAB *ShmemIndex = NULL; /* primary index hashtable for shmem */ + +struct HTAB +{ + HASHHDR *hctl; /* => shared control information */ + HASHSEGMENT *dir; /* directory of segment starts */ + HashValueFunc hash; /* hash function */ + HashCompareFunc match; /* key comparison function */ + HashCopyFunc keycopy; /* key copying function */ + HashAllocFunc alloc; /* memory allocator */ + MemoryContext hcxt; /* memory context if default allocator used */ + char *tabname; /* table name (for error messages) */ + bool isshared; /* true if table is in shared memory */ + bool isfixed; /* if true, don't enlarge */ + + /* freezing a shared table isn't allowed, so we can keep state here */ + bool frozen; /* true = no more inserts allowed */ + + /* We keep local copies of these fixed values to reduce contention */ + Size keysize; /* hash key length in bytes */ + long ssize; /* segment size --- must be power of 2 */ + int sshift; /* segment shift = log2(ssize) */ +}; +``` + +当试图为一个模块分配内存时,调用函数`ShmemInitStruct`,现在`ShmemIndex`索引中查找, 找不到再调用`ShmemAlloc`函数在内存中为其分配区域。 + +#### 3.3.4.2 SI Message +PostgreSQL在共享内存中开辟了`shmInvalBuffer`记录系统中所发出的所有无效消息以及所有进程处理无消息的进度。 +```cpp +static SISeg *shmInvalBuffer; /* pointer to the shared inval buffer */ + +typedef struct SISeg +{ + /* + * General state information + */ + int minMsgNum; /* oldest message still needed */ + int maxMsgNum; /* next message number to be assigned */ + int nextThreshold; /* # of messages to call SICleanupQueue */ + int lastBackend; /* index of last active procState entry, +1 */ + int maxBackends; /* size of procState array */ + + slock_t msgnumLock; /* spinlock protecting maxMsgNum */ + + /* + * Circular buffer holding shared-inval messages + */ + SharedInvalidationMessage buffer[MAXNUMMESSAGES]; + + /* + * Per-backend state info. + * + * We declare procState as 1 entry because C wants a fixed-size array, but + * actually it is maxBackends entries long. + */ + ProcState procState[1]; /* reflects the invalidation state */ +} SISeg; +``` + +这里的`SISeg.buffer`采用了环形缓冲区结构。 + +`ProcState`结构记录了`PID`为`procPid`的进程读取无效消息的状态 + +```cpp +typedef struct ProcState +{ + /* procPid is zero in an inactive ProcState array entry. */ + pid_t procPid; /* PID of backend, for signaling */ + PGPROC *proc; /* PGPROC of backend */ + /* nextMsgNum is meaningless if procPid == 0 or resetState is true. */ + int nextMsgNum; /* next message number to read */ + bool resetState; /* backend needs to reset its state */ + bool signaled; /* backend has been sent catchup signal */ + bool hasMessages; /* backend has unread messages */ + + /* + * Backend only sends invalidations, never receives them. This only makes + * sense for Startup process during recovery because it doesn't maintain a + * relcache, yet it fires inval messages to allow query backends to see + * schema changes. + */ + bool sendOnly; /* backend only sends, never receives */ + + /* + * Next LocalTransactionId to use for each idle backend slot. We keep + * this here because it is indexed by BackendId and it is convenient to + * copy the value to and from local memory when MyBackendId is set. It's + * meaningless in an active ProcState entry. + */ + LocalTransactionId nextLXID; +} ProcState; + +``` + +`SI Message`机制示意图: + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-08_115154.jpg) + +`SendSharedInvalidMessages`调用`SIInsertDataEntries`来完成无效消息的插入。 + + +## 3.4. 表操作与元组操作 +### 3.4.1. 表操作 +### 3.4.2. 元组操作 +## 3.5. VACUUM机制 +### 3.5.1. VACUUM操作 +### 3.5.2. Lazy VACUUM +### 3.5.3. Full VACUUM +## 3.6. ResourceOwner资源跟踪 +## 3.7. 小结 diff --git a/_posts/2021-01-01-postgresql-c5.md b/_posts/2021-01-01-postgresql-c5.md new file mode 100644 index 0000000..3267a2d --- /dev/null +++ b/_posts/2021-01-01-postgresql-c5.md @@ -0,0 +1,875 @@ +--- +title: PostgreSQL 数据库内核分析-c5 +layout: post +categories: PostgreSQL +tags: PostgreSQL 编译 规划 书籍 +excerpt: PostgreSQL 数据库内核分析-c5 +--- +# 第5章 查询编译 +查询编译的主要认识是根据用户的查询语句生成数据库中的最优执行计划,在此过程中要考虑视图、规则以及表的连接路径等问题。 + +## 5.1. 概述 +当PostgreSQL的后台服务进程接收到查询语句后,首先将其传递到查询分析校块,进行词法、语法和语义分析。若是简单的命令(例如建表、创建用户、备份等)则将其分配到功能性命令处理模块;对于复杂的命令(SELECT/INSERT/DELETE/UPDATE)则要为其构建查询树(Query结构体),然后交给查询重写模块。査询重写模块接收到查询树后,按照该杳询所涉及的规则和视图对査询树迸行重写,生成新的査询树。生成路径模块依据重写过的查询树,考虑关系的访问方式、连接方式和连接顺序等问题,采用动态规划算法或遗传算法,生成最优的表连接路径。最后,由最优路径生成可执行的计划,并将其传递到査询执行模块执行。 + +查询优化的核心是生成路径和生成计划两个模块。 +查询优化要处理的问题焦点在于如何计算最优的表连接路径。 + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_123000.jpg) + + +## 5.2. 查询分析 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_143314.jpg) + +```cpp +exec_simple_query + pg_parse_query + raw_parser + base_yyparse + pg_analyze_and_rewrite + /* 语义分析 */ + parse_analyze + // 根据分析树生成一个对应的查询树 + /* 查询重写 */ + pg_rewrite_query + // 继续对这一查询树进行修改,返回查询树链表 +``` + + +### 5.2.1. Lex和Yacc简介 +> 参考: https://zhuanlan.zhihu.com/p/143867739 + +假定我们有一个温度计,我们要用一种简单的语言来控制它。关于此的一个会话、如下: + +``` +heat on +Heater on! +heat off +Header off! +target temperature set! +``` + +1.词法分析工具`Lex` +通过`Lex`命令可以从`Lex`文件生成一个包含有扫描器的C语言源代码,其他源代码可以通过调用该扫描器来实现词法分析。 +`Lex`主要是通过正则表达式来定义各种`TOKEN`。 +通过`example.l`来定义词法规则 +```s +%{ +#include +#include "y.tab.h" +%} + +%% +[0-9]+ return NUMBER; +heat return TOKHEAT; +on|off return STATE; +target return TOKTARGET; +temperature return TOKTEMPERATURE; +\n /* ignore end of line */; +[ \t]+ /* ignore whitespace */ +%% +``` + +命令: +```shell +lex example.l +cc lex.yy.c -o first -ll +``` + +2.语法分析工具`Yacc` +从输入流中寻找某一个特定的语法结构——`example.y` +```s +commands: /* empty */ +| commands command +; + +command: heat_switch | target_set ; + +heat_switch: +TOKHEAT STATE +{ + printf("\tHeat turned on or off\n"); +}; + +target_set: +TOKTARGET TOKTEMPERATURE NUMBER +{ + printf("\tTemperature set\n"); +}; +``` +编译命令: +``` +lex example.l +yacc –d example.y +cc lex.yy.c y.tab.c –o example +``` + +### 5.2.2. 词法和语法分析 +词法和语法分析输出语法树`raw_parsetree_list`。 + +词法和语法分析的源文件: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_153016.jpg) + +#### 5.2.2.1. `SELECT`语句在文件`gram.y`中的定义 +```s +SelectStmt: select_no_parens %prec UMINUS + | select_with_parens %prec UMINUS + ; + +select_with_parens: + '(' select_no_parens ')' { $$ = $2; } + | '(' select_with_parens ')' { $$ = $2; } + ; + +select_no_parens: + simple_select { $$ = $1; } + | select_clause sort_clause + { + insertSelectOptions((SelectStmt *) $1, $2, NIL, + NULL, NULL, NULL, + yyscanner); + $$ = $1; + } + | select_clause opt_sort_clause for_locking_clause opt_select_limit + { + insertSelectOptions((SelectStmt *) $1, $2, $3, + (Node*)list_nth($4, 0), (Node*)list_nth($4, 1), + NULL, + yyscanner); + $$ = $1; + } + | select_clause opt_sort_clause select_limit opt_for_locking_clause + { + insertSelectOptions((SelectStmt *) $1, $2, $4, + (Node*)list_nth($3, 0), (Node*)list_nth($3, 1), + NULL, + yyscanner); + $$ = $1; + } + | with_clause select_clause + { + insertSelectOptions((SelectStmt *) $2, NULL, NIL, + NULL, NULL, + $1, + yyscanner); + $$ = $2; + } + | with_clause select_clause sort_clause + { + insertSelectOptions((SelectStmt *) $2, $3, NIL, + NULL, NULL, + $1, + yyscanner); + $$ = $2; + } + | with_clause select_clause opt_sort_clause for_locking_clause opt_select_limit + { + insertSelectOptions((SelectStmt *) $2, $3, $4, + (Node*)list_nth($5, 0), (Node*)list_nth($5, 1), + $1, + yyscanner); + $$ = $2; + } + | with_clause select_clause opt_sort_clause select_limit opt_for_locking_clause + { + insertSelectOptions((SelectStmt *) $2, $3, $5, + (Node*)list_nth($4, 0), (Node*)list_nth($4, 1), + $1, + yyscanner); + $$ = $2; + } + ; + +select_clause: + simple_select { $$ = $1; } + | select_with_parens { $$ = $1; } + ; + +simple_select: + SELECT hint_string opt_distinct target_list + into_clause from_clause where_clause + group_clause having_clause window_clause + { + SelectStmt *n = makeNode(SelectStmt); + n->distinctClause = $3; + n->targetList = $4; + n->intoClause = $5; + n->fromClause = $6; + n->whereClause = $7; + n->groupClause = $8; + n->havingClause = $9; + n->windowClause = $10; + n->hintState = create_hintstate($2); + n->hasPlus = getOperatorPlusFlag(); + $$ = (Node *)n; + } + | values_clause { $$ = $1; } + | TABLE relation_expr + { + /* same as SELECT * FROM relation_expr */ + ColumnRef *cr = makeNode(ColumnRef); + ResTarget *rt = makeNode(ResTarget); + SelectStmt *n = makeNode(SelectStmt); + + cr->fields = list_make1(makeNode(A_Star)); + cr->location = -1; + + rt->name = NULL; + rt->indirection = NIL; + rt->val = (Node *)cr; + rt->location = -1; + + n->targetList = list_make1(rt); + n->fromClause = list_make1($2); + $$ = (Node *)n; + } + | select_clause UNION opt_all select_clause + { + $$ = makeSetOp(SETOP_UNION, $3, $1, $4); + } + | select_clause INTERSECT opt_all select_clause + { + $$ = makeSetOp(SETOP_INTERSECT, $3, $1, $4); + } + | select_clause EXCEPT opt_all select_clause + { + $$ = makeSetOp(SETOP_EXCEPT, $3, $1, $4); + } + | select_clause MINUS_P opt_all select_clause + { + $$ = makeSetOp(SETOP_EXCEPT, $3, $1, $4); + } + ; +``` + +#### 5.2.2.2. 实例 +数据结构: +```cpp +typedef struct SelectStmt { + NodeTag type; + + /* + * These fields are used only in "leaf" SelectStmts. + */ + List *distinctClause; /* NULL, list of DISTINCT ON exprs, or + * lcons(NIL,NIL) for all (SELECT DISTINCT) */ + IntoClause *intoClause; /* target for SELECT INTO */ + List *targetList; /* the target list (of ResTarget) */ + List *fromClause; /* the FROM clause */ + Node *whereClause; /* WHERE qualification */ + List *groupClause; /* GROUP BY clauses */ + Node *havingClause; /* HAVING conditional-expression */ + List *windowClause; /* WINDOW window_name AS (...), ... */ + WithClause *withClause; /* WITH clause */ + + /* + * In a "leaf" node representing a VALUES list, the above fields are all + * null, and instead this field is set. Note that the elements of the + * sublists are just expressions, without ResTarget decoration. Also note + * that a list element can be DEFAULT (represented as a SetToDefault + * node), regardless of the context of the VALUES list. It's up to parse + * analysis to reject that where not valid. + */ + List *valuesLists; /* untransformed list of expression lists */ + + /* + * These fields are used in both "leaf" SelectStmts and upper-level + * SelectStmts. + */ + List *sortClause; /* sort clause (a list of SortBy's) */ + Node *limitOffset; /* # of result tuples to skip */ + Node *limitCount; /* # of result tuples to return */ + List *lockingClause; /* FOR UPDATE (list of LockingClause's) */ + HintState *hintState; + + /* + * These fields are used only in upper-level SelectStmts. + */ + SetOperation op; /* type of set op */ + bool all; /* ALL specified? */ + struct SelectStmt *larg; /* left child */ + struct SelectStmt *rarg; /* right child */ + + /* + * These fields are used by operator "(+)" + */ + bool hasPlus; + /* Eventually add fields for CORRESPONDING spec here */ +} SelectStmt; +``` + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_161132.jpg) + +```sql +CREATE TABLE class(classno varchar(20), classname varchar(30), gno varchar(20)); +CREATE TABLE student(sno varchar(20), sname varchar(30), sex varchar(5), age integer, nation varchar(20), classno varchar(20)); +CREATE TABLE course(cno varchar(20), cname varchar(30), credit integer, priorcourse varchar(20)); +CREATE TABLE sc(sno varchar(20), cno varchar(20), score integer); +``` + +查询2005级各班高等数学的平均成绩, 且只查询平均分在`80`以上的班级, 并将结果按照升序排列。 +查询命令: +```sql +SELECT classno,classname, AVG(score) AS avg_score +FROM sc, (SELECT* FROM class WHERE class.gno='2005') AS sub +WHERE sc.sno IN (SELECT sno FROM student WHERE student.classno=sub.classno) AND + sc.cno IN (SELECT course.cno FROM course WHERE course.cname='高等数学') +GROUP BY classno, classname +HAVING AVG(score) > 80.0 +ORDER BY avg_score; +``` + +一些重要的结构定义: +1.DISTINCT + +``` +opt_distinct: + DISTINCT { $$ = list_make1(NIL); } + | DISTINCT ON '(' expr_list ')' { $$ = $4; } + | ALL { $$ = NIL; } + | /*EMPTY*/ { $$ = NIL; } + ; +``` + +2.目标属性: `SELECT`语句中所要查询的属性列表 +``` +target_list: + target_el { $$ = list_make1($1); } + | target_list ',' target_el { $$ = lappend($1, $3); } + ; +target_el: a_expr AS ColLabel + { + $$ = makeNode(ResTarget); + $$->name = $3; + $$->indirection = NIL; + $$->val = (Node *)$1; + $$->location = @1; + } +``` + +`ResTarget`数据结构如下: +```cpp +typedef struct ResTarget { + NodeTag type; + char *name; /* column name or NULL */ + List *indirection; /* subscripts, field names, and '*', or NIL */ + Node *val; + /* the value expression to compute or assign */ + int location; /* token location, or -1 if unknown */ +} ResTarget; +``` + + +图5-5 分析树种目标属性的数据结构组织 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_171550.jpg) + +`SELECT classno,classname, AVG(score) AS avg_score`语句对应的目标属性在内存中的数据组织结构如下图: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_171910.jpg) + +3.FROM子句 +语法定义: +```cpp +from_clause: + FROM from_list { $$ = $2; } + | /*EMPTY*/ { $$ = NIL; } + ; + +from_list: + table_ref { $$ = list_make1($1); } + | from_list ',' table_ref { $$ = lappend($1, $3); } + ; + +table_ref: relation_expr /* 关系表达式 */ + { + $$ = (Node *) $1; + } + | relation_expr alias_clause /* 取别名的关系表达式 */ + { + $1->alias = $2; + $$ = (Node *) $1; + } + | func_table + { + RangeFunction *n = makeNode(RangeFunction); + n->funccallnode = $1; + n->coldeflist = NIL; + $$ = (Node *) n; + } + +relation_expr: + qualified_name + { + /* default inheritance */ + $$ = $1; + $$->inhOpt = INH_DEFAULT; + $$->alias = NULL; + } + | qualified_name '*' + +qualified_name: + ColId + { + $$ = makeRangeVar(NULL, $1, @1); + } + | ColId indirection + +RangeVar* makeRangeVar(char* schemaname, char* relname, int location) +{ + RangeVar* r = makeNode(RangeVar); + + r->catalogname = NULL; + r->schemaname = schemaname; + r->relname = relname; + r->partitionname = NULL; + r->inhOpt = INH_DEFAULT; + r->relpersistence = RELPERSISTENCE_PERMANENT; + r->alias = NULL; + r->location = location; + r->ispartition = false; + r->partitionKeyValuesList = NIL; + r->isbucket = false; + r->buckets = NIL; + r->length = 0; + return r; +} + +typedef struct RangeVar { + NodeTag type; + char* catalogname; /* the catalog (database) name, or NULL */ + char* schemaname; /* the schema name, or NULL */ + char* relname; /* the relation/sequence name */ + char* partitionname; /* partition name, if is a partition */ + InhOption inhOpt; /* expand rel by inheritance? recursively act + * on children? */ + char relpersistence; /* see RELPERSISTENCE_* in pg_class.h */ + Alias* alias; /* table alias & optional column aliases */ + int location; /* token location, or -1 if unknown */ + bool ispartition; /* for partition action */ + List* partitionKeyValuesList; + bool isbucket; /* is the RangeVar means a hash bucket id ? */ + List* buckets; /* the corresponding bucketid list */ + int length; +#ifdef ENABLE_MOT + Oid foreignOid; +#endif +} RangeVar; + + +``` + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_174410.jpg) +`FROM sc, (SELECT* FROM class WHERE class.gno='2005') AS sub`解析为: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_174428.jpg) + +4.WHERE子句 +语法结构: +``` +where_clause: + WHERE a_expr { $$ = $2; } + | /*EMPTY*/ { $$ = NULL; } + ; +``` + +数据结构: +```cpp +typedef struct A_Expr { + NodeTag type; + A_Expr_Kind kind; /* see above */ + List *name; /* possibly-qualified name of operator */ + Node *lexpr; /* left argument, or NULL if none */ + Node *rexpr; /* right argument, or NULL if none */ + int location; /* token location, or -1 if unknown */ +} A_Expr; +``` + +```SQL +WHERE sc.sno IN (SELECT sno FROM student WHERE student.classno=sub.classno) AND + sc.cno IN (SELECT course.cno FROM course WHERE course.cname='高等数学') +``` + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_180853.jpg) +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_180954.jpg) + +5.`GROUP BY`子句 +语法定义: +``` +group_clause: + GROUP_P BY group_by_list { $$ = $3; } + | /*EMPTY*/ { $$ = NIL; } + ; +group_by_list: + group_by_item { $$ = list_make1($1); } + | group_by_list ',' group_by_item { $$ = lappend($1,$3); } + ; + +group_by_item: + a_expr { $$ = $1; } +``` + +`GROUP BY classno, classname` +内存组织结构: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-07_090157.jpg) + +6.HAVING子句和ORDER BY子句 +语法定义: +``` +having_clause: + HAVING a_expr { $$ = $2; } + | /*EMPTY*/ { $$ = NULL; } + ; + +sort_clause: + ORDER BY sortby_list { $$ = $3; } + ; + +sortby_list: + sortby { $$ = list_make1($1); } + | sortby_list ',' sortby { $$ = lappend($1, $3); } + ; + +sortby: a_expr USING qual_all_Op opt_nulls_order + { + $$ = makeNode(SortBy); + $$->node = $1; + $$->sortby_dir = SORTBY_USING; + $$->sortby_nulls = (SortByNulls)$4; + $$->useOp = $3; + $$->location = @3; + } +``` + +```sql +HAVING AVG(score) > 80.0 +ORDER BY avg_score; +``` + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_182711.jpg) + +### 5.2.3. 语义分析 +检查命令中是否有不符合语义规定的成分。 + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_143314.jpg) + +关键数据结构: +```cpp +/* 用于记录语义分析的中间信息 */ +struct ParseState { + List* p_rtable; /* range table so far */ +} + +/* 用于存储查询树 */ +typedef struct Query { + CmdType commandType; /* select|insert|update|delete|merge|utility */ + List* rtable; /* list of range table entries 查询中使用的表的列表 */ + int resultRelation; /* rtable index of target relation for + * INSERT/UPDATE/DELETE/MERGE; 0 for SELECT + * 涉及数据修改的范围表的编号 + */ + FromExpr* jointree; /* table join tree (FROM and WHERE clauses) */ + List* targetList; /* target list (of TargetEntry) */ +} Query; +``` + +关键过程: +```cpp +/* + * parse_analyze + * Analyze a raw parse tree and transform it to Query form. + * + * Optionally, information about $n parameter types can be supplied. + * References to $n indexes not defined by paramTypes[] are disallowed. + * + * The result is a Query node. Optimizable statements require considerable + * transformation, while utility-type statements are simply hung off + * a dummy CMD_UTILITY Query node. + */ +Query* parse_analyze( + Node* parseTree, const char* sourceText, Oid* paramTypes, int numParams, bool isFirstNode, bool isCreateView) +{ + ParseState* pstate = make_parsestate(NULL); + // 将分析树`parseTree`转换成查询树`query` + query = transformTopLevelStmt(pstate, parseTree, isFirstNode, isCreateView); + // 以查询为例 + static Query* transformSelectStmt(ParseState* pstate, SelectStmt* stmt, bool isFirstNode, bool isCreateView) +} +``` + +这里的`parseTree`数据结构通过`Node`指针来传递, 通过`NodeTag`来确定数据结构的类型。 +```cpp +typedef struct Node { + NodeTag type; +} Node; + +typedef enum NodeTag { + /* + * TAGS FOR STATEMENT NODES (mostly in parsenodes.h) + */ + T_Query = 700, + T_PlannedStmt, + T_InsertStmt, + T_DeleteStmt, + T_UpdateStmt, + T_MergeStmt, + T_SelectStmt, + …… +} +``` + +可以在执行`SQL`命令时打印查询树的规则, 相关参数: +``` +#debug_print_parse = off +#debug_print_rewritten = off +#debug_print_plan = off +``` + +分析树对应的语义分析函数如下所示: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_193515.jpg) +1.`transformSelectStmt`函数的总体流程如下: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_194446.jpg) + +2.FROM子句的语义分析处理 +`transformFromClause`负责处理`FROM`子句的语法分析,并生成范围表。(`Query::rtable`) + +关键数据结构: +```cpp +typedef struct RangeTblEntry { + NodeTag type; + + RTEKind rtekind; /* see above */ +} +``` + +关键过程: +```cpp +void transformFromClause(ParseState* pstate, List* frmList, bool isFirstNode, bool isCreateView) +{ + foreach (fl, frmList) { + Node* n = (Node*)lfirst(fl); + RangeTblEntry* rte = NULL; + int rtindex; + List* relnamespace = NIL; + + // 根据`fromClause`的`Node`产生一个或多个`RangeTblEntry`结构, 加入到`ParseState::p_rtable`中 + n = transformFromClauseItem( + pstate, n, &rte, &rtindex, NULL, NULL, &relnamespace, isFirstNode, isCreateView); + Node* transformFromClauseItem(ParseState* pstate, Node* n, RangeTblEntry** top_rte, int* top_rti, + RangeTblEntry** right_rte, int* right_rti, List** relnamespace, bool isFirstNode, bool isCreateView, bool isMergeInto) + { + // 普通表 + if (IsA(n, RangeVar)) { + /* Plain relation reference, or perhaps a CTE reference */ + RangeVar* rv = (RangeVar*)n; + RangeTblRef* rtr = NULL; + RangeTblEntry* rte = NULL; + + /* if it is an unqualified name, it might be a CTE reference */ + if (!rv->schemaname) { + CommonTableExpr* cte = NULL; + Index levelsup; + + cte = scanNameSpaceForCTE(pstate, rv->relname, &levelsup); + if (cte != NULL) { + // 将其作为一个`RTE`加入到`ParseState::p_rtable`中 + rte = transformCTEReference(pstate, rv, cte, levelsup); + } + } + + /* if not found as a CTE, must be a table reference */ + if (rte == NULL) { + rte = transformTableEntry(pstate, rv, isFirstNode, isCreateView); + } + + rtr = transformItem(pstate, rte, top_rte, top_rti, relnamespace); + return (Node*)rtr; + } + } + // 执行重名检查 + checkNameSpaceConflicts(pstate, pstate->p_relnamespace, relnamespace); +``` + +`From`子句得到的`Query`如下: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_200915.jpg) + +> 我的小问题: `Query`和`RangeTblEntry`数据结构之间的关系是什么? + + +在生成`query`的过程中,可以利用`ParseState::p_rtable`的结构来生成,这个结构是一个链表,它的子节点是`RangeTblEntry`类型。 +```cpp +Query* parse_analyze( + Node* parseTree, const char* sourceText, Oid* paramTypes, int numParams, bool isFirstNode, bool isCreateView) +{ + ParseState* pstate = make_parsestate(NULL); + // 将分析树`parseTree`转换成查询树`query` + query = transformTopLevelStmt(pstate, parseTree, isFirstNode, isCreateView); + // 以查询为例 + static Query* transformSelectStmt(ParseState* pstate, SelectStmt* stmt, bool isFirstNode, bool isCreateView) + /* process the FROM clause */ + transformFromClause(pstate, stmt->fromClause, isFirstNode, isCreateView); { + foreach (fl, frmList) { + Node* n = (Node*)lfirst(fl); + RangeTblEntry* rte = NULL; + // 根据`fromClause`的`Node`产生一个或多个`RangeTblEntry`结构, 加入到`ParseState::p_rtable`中 + n = transformFromClauseItem(pstate, n, &rte, &rtindex, NULL, NULL, &relnamespace, isFirstNode, isCreateView); + } + } +} +``` + +----- + +3.目标属性的语义分析处理 +```cpp +Query* parse_analyze( + Node* parseTree, const char* sourceText, Oid* paramTypes, int numParams, bool isFirstNode, bool isCreateView) +{ + ParseState* pstate = make_parsestate(NULL); + // 将分析树`parseTree`转换成查询树`query` + query = transformTopLevelStmt(pstate, parseTree, isFirstNode, isCreateView); + // 以查询为例 + static Query* transformSelectStmt(ParseState* pstate, SelectStmt* stmt, bool isFirstNode, bool isCreateView) + Query* qry = makeNode(Query); + /* transform targetlist */ + qry->targetList = transformTargetList(pstate, stmt->targetList); + // * Turns a list of ResTarget's into a list of TargetEntry's. + foreach (o_target, targetlist) { + // 多列元素`*` + p_target = list_concat(p_target, ExpandColumnRefStar(pstate, cref, true)); + p_target = list_concat(p_target, ExpandIndirectionStar(pstate, ind, true)); + // 单列 + p_target = lappend(p_target, transformTargetEntry(pstate, res->val, NULL, res->name, false)); + } + +} +``` + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_201840.jpg) + +4.WHERE子句的语义分析处理 +```cpp + /* transform WHERE + * Only "(+)" is valid when it's in WhereClause of Select, set the flag to be trure + * during transform Whereclause. + */ + Node* qual = NULL; + setIgnorePlusFlag(pstate, true); + qual = transformWhereClause(pstate, stmt->whereClause, "WHERE"); + setIgnorePlusFlag(pstate, false); + qry->jointree = makeFromExpr(pstate->p_joinlist, qual); +``` + +连接树`jointree`整体的数据组织结构如下: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-06_201743.jpg) + +------ + +```sql +SELECT classno,classname, AVG(score) AS avg_score +FROM sc, (SELECT* FROM class WHERE class.gno='2005') AS sub +WHERE sc.sno IN (SELECT sno FROM student WHERE student.classno=sub.classno) AND + sc.cno IN (SELECT course.cno FROM course WHERE course.cname='高等数学') +GROUP BY classno, classname +HAVING AVG(score) > 80.0 +ORDER BY avg_score; +``` + +## 5.3. 查询重写 +### 5.3.1. 规则系统 +规则系统可以理解成编译器中的`PASS`,在系统表`pg_rewrite`中存储重写规则. +系统表`pg_rewrite`的`scheme`如下所示: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-07_102617.jpg) + + +### 5.3.2. 查询重写的处理操作 +## 5.4. 查询规划 +查询优化的核心思想是“尽量先做选择操作,后做连接操作”。 + + +### 5.4.1. 总体处理流程 +**预处理**:对查询树`Query`进行进一步改造,最重要的是提升子链接和提升子查询。 +**生成路径**: 采用动态规划算法或遗传算法,生成最优连接路径和候选的路径链表。 +**生成计划**: 先生成基本计划树,然后添加子句所定义的计划节点形成完整计划树。 +**查询规划模块**: 入口函数`pg_plan_queries`,将查询树链表变成执行计划链表,只处理非`Utilitiy`命令。调用`pg_plan_query`对每一个查询树进行处理,并将生成的`PlannedStmt`结构体构成一个链表返回。 +```plantuml +digraph G { + pg_plan_queries -> pg_plan_query -> planner + planner -> standard_planner [label="Query* parse"] + standard_planner -> planner [label="PlannedStmt*"] +} +``` +从`planner`函数开始进入执行计划的生成阶段, `PlannedStmt`结构体包含了执行器执行该查询所需要的全部信息。 + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_102513.jpg) + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_105349.jpg) +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_105431.jpg) +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_105509.jpg) + + +### 5.4.2. 实例分析 +#### 5.4.2.1. 查询分析之后的查询树 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_105821.jpg) + +查询2005级各班高等数学的平均成绩, 且只查询平均分在`80`以上的班级, 并将结果按照升序排列。 +原始查询命令: +```sql +SELECT classno,classname, AVG(score) AS avg_score +FROM sc, (SELECT* FROM class WHERE class.gno='2005') AS sub +WHERE sc.sno IN (SELECT sno FROM student WHERE student.classno=sub.classno) AND + sc.cno IN (SELECT course.cno FROM course WHERE course.cname='高等数学') +GROUP BY classno, classname +HAVING AVG(score) > 80.0 +ORDER BY avg_score; +``` + +#### 5.4.2.2. 提升子链接 +将`WHERE`子句中的子链接提升成为范围表 + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_111050.jpg) + + +提升后的查询命令如下: +```sql +SELECT sub.classno, sub.classname, AVG(score) AS avg_score +FROM sc, (SELECT* FROM class WHERE class.gno='2005') AS sub, (SELECT course.cno FROM course WHERE course.cname='高等数学') AS ANY_subquery +WHERE sc.sno IN (SELECT sno FROM student WHERE student.classno=sub.classno) + AND sc.cno = ANY_subquery.cno +GROUP BY classno, classname +HAVING AVG(score) > 80.0 +ORDER BY avg_score; +``` + +#### 5.4.2.3. 提升子查询 +对于子查询`(SELECT* FROM class WHERE class.gno='2005') AS sub`,提升后如下: +```sql +SELECT class.classno, class.classname, AVG(score) AS avg_score +FROM sc, (SELECT* FROM class WHERE class.gno='2005') AS sub, (SELECT course.cno FROM course WHERE course.cname='高等数学') AS ANY_subquery, class, course +WHERE sc.sno IN (SELECT sno FROM student WHERE student.classno=sub.classno) + AND sc.cno = course.cno + AND course.cname = '高等数学' + AND class.gno = '2005' +GROUP BY classno, classname +HAVING AVG(score) > 80.0 +ORDER BY avg_score; +``` + +子查询整体提升完毕后如下: +```sql +SELECT classno, classname, AVG(score) AS avg_score +FROM sc, class, course +WHERE sc.sno IN (SELECT sno FROM student WHERE student.classno=sub.classno) + AND sc.cno = course.cno + AND course.cname = '高等数学' + AND class.gno = '2005' +GROUP BY classno, classname +HAVING AVG(score) > 80.0 +ORDER BY avg_score; +``` + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_112909.jpg) + +#### 5.4.2.4. 生成路径 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_113048.jpg) +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_113056.jpg) +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_113108.jpg) + +#### 5.4.2.5. 生成计划 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_113019.jpg) diff --git a/_posts/2021-01-01-postgresql-c6.md b/_posts/2021-01-01-postgresql-c6.md new file mode 100644 index 0000000..7ff8fd3 --- /dev/null +++ b/_posts/2021-01-01-postgresql-c6.md @@ -0,0 +1,84 @@ +--- +title: PostgreSQL 数据库内核分析-c6 +layout: post +categories: PostgreSQL +tags: PostgreSQL 执行 书籍 +excerpt: PostgreSQL 数据库内核分析-c6 +--- +# 6. 第6章 查询执行 +在查询执行阶段, 将根据执行计划进行数据提取、处理、存储等一系列活动,以完成整个查询执行过程。根据执行计划的安排,调用存储、索引、并发等模块,按照各种执行计划中各种计划节点的实现算法来完成数据的读取或者修改的过程。 + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_115025.jpg) + +`Portal` : 选择策略,是查询语句还是非查询语句 +`Executor` : 输入包含了一个查询计划树`Plan Tree` +`ProcessUtility` : 为每个非查询指令实现了处理流程 + + +## 6.1. 查询执行策略 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_123253.jpg) + +### 6.1.1. 可优化语句和数据定义语句 +### 6.1.2. 四种执行策略 +### 6.1.3. 策略选择的实现 +### 6.1.4. Portal执行的过程 + +Portal执行时的函数调用关系: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_141106.jpg) + +## 6.2. 数据定义语句执行 +### 6.2.1. 数据定义语句执行流程 +根据`NodeTag`字段的值来区分各种不同节点, 并引导执行流程进入相应的处理函数。 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_141141.jpg) + +### 6.2.2. 执行实例 +```sql +CREATE TABLE course( + no SERIAL, + name VARCHAR, + credit INT, + CONSTRAINT conl CHECK(credit >=0 AND name <>''), + PRIMARY KEY(no) +) +``` + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_142205.jpg) +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_142329.jpg) +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_143411.jpg) + +将一个`T_CreateStmt`操作转换为一个操作序列`T_CreateSeqStmt`+`T_CreateStmt`+`T_IndexStmt`,之后`ProcessUtility`将逐个对序列中的操作进行处理。 + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_143550.jpg) + +### 6.2.3. 主要的功能处理器函数 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_143948.jpg) + +## 6.3. 可优化语句执行 +### 6.3.1. 物理代数与处理模型 +### 6.3.2. 物理操作符的数据结构 +### 6.3.3. 执行器的运行 +### 6.3.4. 执行实例 +```sql +/* 例6.2 */ +course(no, name, credit) +teacher(no, name, sex, age) +teach_coures(no, cno, stu_num) + +SELECT t.name, c.name, stu_num +FROM course AS c, teach_course AS tc, teacher AS t +WHERE c.no=tc.cno AND tc.tno=t.no AND c.name='Database System' AND t.name='Jennifer'; +``` + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_145754.jpg) +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-01-12_150129.jpg) + +## 6.4. 计划节点 +### 6.4.1. 控制节点 +### 6.4.2. 扫描节点 +### 6.4.3. 物化节点 +### 6.4.4. 连接节点 +## 6.5. 其他子功能介绍 +### 6.5.1. 元组操作 +### 6.5.2. 表达式计算 +### 6.5.3. 投影操作 +## 6.6. 小结 diff --git a/_posts/2021-01-01-postgresql-c7.md b/_posts/2021-01-01-postgresql-c7.md new file mode 100644 index 0000000..e3318fc --- /dev/null +++ b/_posts/2021-01-01-postgresql-c7.md @@ -0,0 +1,818 @@ +--- +title: PostgreSQL 数据库内核分析-c7 +layout: post +categories: PostgreSQL +tags: PostgreSQL 事务 书籍 +excerpt: PostgreSQL 数据库内核分析-c7 +--- +# 第7章 事务处理与并发控制 +## 7.1 事务系统简介 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-08_134328.jpg) + +- 事务管理器是事务系统的中枢, 它的实现是一个有限状态自动机, 通过接受外部系统的命令或者信号, 并根据当前事务所处的状态, 决定事务的下一步执行过程。 +- 事务执行的写阶段需要由各种锁保证事务的隔离级别 +- 日志管理器用来记录事务执行的状态以及数据的变化过程, 包括事务提交日志(CLOG)和事务日志(XLOG)。 + +--------- + +```sql +BEGIN; +SELECT * FROM test; +END; +``` + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-08_195632.jpg) + +## 7.2 事务系统的上层 +概念定义: +事务: `PostgreSQL`中的事务实际上是数据库理论中的命令概念。 +“事务” : 数据库理论中谈到的事务概念。 + + +`PostgreSQL`执行一条SQL语句前会调用`StartTransactionCommand`函数, 执行结束时调用`CommitTransactionCommand`。如果命令执行失败,调用`AbortCurrentTransaction`函数。 + +事务系统上层的入口函数, 不处理具体事务 +- StartTransactionCommand +- CommitTransactionCommand +- AbortCurrentTransaction + + +### 7.2.1 事务块状态 +整个事务系统的状态, 反映了当前用户输入命令的变化过程和事务底层的执行状态的变化。 +其数据结构是一个枚举类型。 +```cpp +typedef enum TBlockState +{ + /* not-in-transaction-block states */ + TBLOCK_DEFAULT, /* idle */ + TBLOCK_STARTED, /* running single-query transaction */ + + /* transaction block states */ + TBLOCK_BEGIN, /* starting transaction block */ + TBLOCK_INPROGRESS, /* live transaction */ + TBLOCK_END, /* COMMIT received */ + TBLOCK_ABORT, /* failed xact, awaiting ROLLBACK */ + TBLOCK_ABORT_END, /* failed xact, ROLLBACK received */ + TBLOCK_ABORT_PENDING, /* live xact, ROLLBACK received */ + TBLOCK_PREPARE, /* live xact, PREPARE received */ + + /* subtransaction states */ + TBLOCK_SUBBEGIN, /* starting a subtransaction */ + TBLOCK_SUBINPROGRESS, /* live subtransaction */ + TBLOCK_SUBRELEASE, /* RELEASE received */ + TBLOCK_SUBCOMMIT, /* COMMIT received while TBLOCK_SUBINPROGRESS */ + TBLOCK_SUBABORT, /* failed subxact, awaiting ROLLBACK */ + TBLOCK_SUBABORT_END, /* failed subxact, ROLLBACK received */ + TBLOCK_SUBABORT_PENDING, /* live subxact, ROLLBACK received */ + TBLOCK_SUBRESTART, /* live subxact, ROLLBACK TO received */ + TBLOCK_SUBABORT_RESTART /* failed subxact, ROLLBACK TO received */ +} TBlockState; +``` + +### 7.2.2 事务块操作 +### 7.2.2.1 事务块基本操作 +- StartTransactionCommand +判断当前事务块的状态进入不同的底层事务执行函数中,是进入事务处理模块的入口函数 +- CommitTransactionCommand +在每条SQL语句执行后调用 +- AbortCurrentTransaction +当系统遇到错误时,返回到调用点并执行该函数。 + +### 7.2.2.2 事务块状态的改变 +通过接受外部系统的命令或者信号, 并根据当前事务所处的状态, 决定事务块的下一步状态。 + +事务块状态改变函数: +- BeginTransactionBlock +执行`BEGIN`命令 +- EndTransactionBlock +执行`END`命令 +- UserAbortTransactionBlock +执行`ROLLBACK`命令 + + +一些概念: +- `BEGIN`和`END`命令类似一个大括号,在这个大括号的`SQL`语句被系统认为是处于同一个事务块中的语句。 +- 事务之间存在嵌套关系,如子事务和顶层事务之间的关系,通过`TransactionStateData::nestingLevel`来表示 + + +事务块状态机: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-09_094901.jpg) + +## 7.3 事务系统的底层 +### 7.3.1 事务状态 +低层次的事务状态通过`TransState`来表示 +```cpp +typedef enum TransState +{ + TRANS_DEFAULT, /* idle */ + TRANS_START, /* transaction starting */ + TRANS_INPROGRESS, /* inside a valid transaction */ + TRANS_COMMIT, /* commit in progress */ + TRANS_ABORT, /* abort in progress */ + TRANS_PREPARE /* prepare in progress */ +} TransState; +``` + +事务处理过程中存储事务状态相关数据的数据结构是`TransactionStateData` +```cpp +typedef struct TransactionStateData +{ + TransactionId transactionId; /* my XID, or Invalid if none */ + SubTransactionId subTransactionId; /* my subxact ID */ + char *name; /* savepoint name, if any */ + int savepointLevel; /* savepoint level */ + TransState state; /* low-level state */ + TBlockState blockState; /* high-level state */ + int nestingLevel; /* transaction nesting depth */ + int gucNestLevel; /* GUC context nesting depth */ + MemoryContext curTransactionContext; /* my xact-lifetime context */ + ResourceOwner curTransactionOwner; /* my query resources */ + TransactionId *childXids; /* subcommitted child XIDs, in XID order */ + int nChildXids; /* # of subcommitted child XIDs */ + int maxChildXids; /* allocated size of childXids[] */ + Oid prevUser; /* previous CurrentUserId setting */ + int prevSecContext; /* previous SecurityRestrictionContext */ + bool prevXactReadOnly; /* entry-time xact r/o state */ + bool startedInRecovery; /* did we start in recovery? */ + struct TransactionStateData *parent; /* back link to parent */ +} TransactionStateData; +``` + +- nestingLevel +每通过`PushTransaction`函数开启一个子事务,该子事务的`nestingLevel`将在父事务的基础上加1。 +- curTransactionOwner +指向`ResourceOwner`的指针, 用于记录当前事务占有的资源。 + +### 7.3.2 事务操作函数 +执行实际的事务操作。 +### 7.3.2.1 启动事务 +- StartTransaction + +### 7.3.2.2 提交事务 +- CommitTransaction + +### 7.3.2.3 退出事务 +在系统遇到错误时调用 +- AbortTransaction + +### 7.3.2.4 清理事务 +释放事务所占用的内存资源 +- CleanupTransaction + +### 7.3.3 简单查询事务执行过程实例 +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-09_101003.jpg) + +处理的事务: +```sql +BEGIN; +SELECT * FROM test; +END; +``` + +相关过程的关键函数: +- BEGIN + - StartTransactionCommand + - StartTransaction + - ProcessUtility + - BeginTransactionBlock + - CommitTransactionCommand +- SELECT + - StartTransactionCommand + - ProcessQuery + - EndTransactionBlock +- END + - StartTransactionCommand + - ProcessUtility + - CommitTransactionBlock + - CommitTransactionCommand + - CommitTransaction + +## 7.4 事务保存点和子事务 +保存点的作用主要是实现事务的回滚, 即从当前子事务回滚到该事务链中的某个祖先事务。 +```sql +BEGIN Transaction S; +insert into table1 values(1); +SAVEPOINT P1; +insert into table1 values(2); +SAVEPOINT P2; +insert into table1 values(3); +ROLLBACK TO savepoint P1; +COMMIT; +``` + +语句的执行结果是在表`table1`中插入数据1; + +### 7.4.1 保存点实现原理 +```cpp +typedef struct TransactionStateData +{ + struct TransactionStateData *parent; /* back link to parent */ +} TransactionStateData; +``` + +`parent`指向本事务的父事务,层次结构如下: + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-10_094653.jpg) + +子事务的层次描述用一个栈链实现, 栈顶元素拥有一个指向其父事务的指针。 当启动一个新的子事务时, 系统调用`PushTransaction`函数将该子事务的`TransactionState`结构变量压入栈中。 + +相关的几个切换父子事务关系的函数, 修改栈的状态并更新`CurrentTransactionState`: +- PushTransaction +- PopTransaction + +### 7.4.2 子事务 +主要作用: 实现保存点, 增强事务操作的灵活性。 + +子事务相关函数: +- StartSubTransaction +- CommitSubTransaction +- CleanupSubTransaction +- AbortSubTransaction + + +## 7.5 两阶段提交 +实现分布式事务处理的关键是两阶段提交协议。 + +### 7.5.1 预提交阶段 +在分布式事务处理中存在一个协调者的角色, 负责协调和远端数据库之间的事务同步。 + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-10_101456.jpg) + +> 我的小问题: 在事务的恢复操作过程中, `Redo`和`Undo`有什么区别? + +### 7.5.2 全局提交阶段 +本地数据库根据协调者的消息, 对本地事务进行`COMMIT`或者`ABORT`操作。 + +## 7.6 PostgreSQL的并发控制 +利用多版本并发控制来维护数据的一致性。 +同时有表和行级别的锁定机制、会话锁机制。 + +4个事务隔离级别: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-10_102221.jpg) + +事务隔离级别所涉及的最小实体是元组, 所以对元组的操作需要实施访问控制,他们是通过锁操作以及`MVCC`相关的操作来实现的. + +- 读已提交 +一个 `SELECT`查询只能看到查询开始之前提交的数据,而永远无法看到未提交的数据或者是在查询执行时其他并行的事务提交所做的改变。 + +- 可串行化 +提供最严格的事务隔离。 + + +举例说明以上两种级别的差异: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-10_103112.jpg) + +## 7.7 PostgreSQL中的三种锁 +### 7.7.1 SpinLock +机器相关的实现`s_lock.c` :使用`TAS`指令集实现 +机器不相关的实现`Spin.c` + +### 7.7.2 LWLock(轻量级锁) +主要提供对共享存储器的数据结构的互斥访问。 +通过`SpinLock`实现 +特点: 有等待队列、 无死锁检测、 能自动释放锁。 + +```cpp +typedef struct LWLock +{ + slock_t mutex; /* Protects LWLock and queue of PGPROCs */ + bool releaseOK; /* T if ok to release waiters */ + char exclusive; /* # of exclusive holders (0 or 1) */ + int shared; /* # of shared holders (0..MaxBackends) */ + PGPROC *head; /* head of list of waiting PGPROCs */ + PGPROC *tail; /* tail of list of waiting PGPROCs */ + /* tail is undefined when head is NULL */ +} LWLock; +``` + +使用`SpinLock`对`mutex`进行加解锁即可实现一个对`LWLock`的互斥访问。 +系统在共享内存中用一个全局数组`LWLockArray`来管理所有的`LWLock`。 + +### 7.7.2 LWLock的主要操作 +- LWLock的空间分配 +- LWLock的创建 +- LWLock的分配 + - `LWLockAssign` +- LWLock的锁的获取 + - `LWLockAcquire` + - `LWLockConditionalAcquire` +- LWLock的锁的释放 + - `LWLockRelease` + +### 7.7.3 RegularLock +通过`LWLock`实现 +特点: 有等待队列、 有死锁检测、 能自动释放锁。 + +`RegularLock`支持的锁模式有`8`种, 按级别从低到高分别是: +```cpp +#define NoLock 0 + +#define AccessShareLock 1 /* SELECT */ +#define RowShareLock 2 /* SELECT FOR UPDATE/FOR SHARE */ +#define RowExclusiveLock 3 /* INSERT, UPDATE, DELETE */ +#define ShareUpdateExclusiveLock 4 /* VACUUM (non-FULL),ANALYZE, CREATE + * INDEX CONCURRENTLY */ +#define ShareLock 5 /* CREATE INDEX (WITHOUT CONCURRENTLY) */ +#define ShareRowExclusiveLock 6 /* like EXCLUSIVE MODE, but allows ROW + * SHARE */ +#define ExclusiveLock 7 /* blocks ROW SHARE/SELECT...FOR + * UPDATE */ +#define AccessExclusiveLock 8 /* ALTER TABLE, DROP TABLE, VACUUM + * FULL, and unqualified LOCK TABLE */ +``` + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-10_111316.jpg) + +`RegularLock`的粒度可以是数据库的表、元组、页等, 请求锁的主体既可以是事务, 也可以是跨事务的会话。 + +`RegularLock`的主要操作: +- 初始化: `InitLocks` +- 申请: `LockAcquire` +- 释放: `LockRelease` +- 释放本地锁: `RemoveLocalLock` +- 锁的冲突检测: `LockCheckConflicts` + + +## 7.8 锁管理机制 +### 7.8.1 表粒度的锁操作 +### 7.8.2 页粒度的锁操作 +### 7.8.3 元组粒度的锁操作 +### 7.8.4 事务粒度的锁操作 +### 7.8.5 一般对象的锁操作 +## 7.9 死锁处理机制 +### 7.9.1 死锁处理相关数据结构 +### 7.9.2 死锁处理相关操作 + +## 7.10 多版本并发控制 +引子: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-10_113357.jpg) + +### 7.10.1 MVCC相关数据结构 +更新数据时并非使用新值覆盖旧值, 而是在表中另外开辟一片空间来存放新的元组, 让新值和旧值同时存放在数据库中, 同时设置一些参数让系统识别他们。 +元组的描述信息使用数据结构`HeapTupleFields`(存储元组的事务、命令控制信息), +`HeapTupleHeaderData`(存储元组的相关控制信息)。 + +```cpp +typedef struct HeapTupleFields +{ + TransactionId t_xmin; /* inserting xact ID */ + TransactionId t_xmax; /* deleting or locking xact ID */ + + union + { + CommandId t_cid; /* inserting or deleting command ID, or both */ + TransactionId t_xvac; /* old-style VACUUM FULL xact ID */ + } t_field3; +} HeapTupleFields; + +typedef struct HeapTupleHeaderData +{ + union + { + HeapTupleFields t_heap; + DatumTupleFields t_datum; + } t_choice; + // 一个新元组被保存在磁盘中的时候, 其`t_ctid`就被初始化为它自己的实际存储位置。 + ItemPointerData t_ctid; /* current TID of this or newer tuple */ + /* Fields below here must match MinimalTupleData! */ + uint16 t_infomask2; /* number of attributes + various flags */ + // 当前元组的事务信息 + uint16 t_infomask; /* various flag bits, see below */ + uint8 t_hoff; /* sizeof header incl. bitmap, padding */ + /* ^ - 23 bytes - ^ */ + bits8 t_bits[1]; /* bitmap of NULLs -- VARIABLE LENGTH */ + /* MORE DATA FOLLOWS AT END OF STRUCT */ +} HeapTupleHeaderData; +``` + +一个新元组被保存在磁盘中的时候, 其`t_ctid`就被初始化为它自己的实际存储位置。 +一个元组是最新版本的充要条件: 当且仅当它的`xmax`为空或者它的`t_ctid`指向它自己。 +`t_ctid`是一个链表结构, 可以通过遍历这个版本链表来找到某个元组的最新版本。 + +`MVCC`的基本原理: + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-10_115142.jpg) + +根据`C`和`C'`的头信息中的`Xmin`和`Xmax`以及`t_infomask`来判断出`C`为有效版本, `C'`为无效版本。 + +### 7.10.2 MVCC相关操作 +MVCC操作所需要调用的相关函数, 核心功能是用来判断元组的状态, 包括元组的有效性、可见性、可更新性等。 + +#### 7.10.2.1 判断元组对于自身信息是否有效 +```cpp +// TRUE: 该元组有效 +bool HeapTupleSatisfiesSelf(HeapTuple htup, Snapshot snapshot, Buffer buffer) +``` + +`HeapTupleSatisfiesSelf`函数的逻辑: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-10_171434.jpg) + +#### 7.10.2.2 判断元组对当前时刻是否有效 +```cpp +// TRUE: 该元组有效 +bool HeapTupleSatisfiesNow(HeapTuple htup, Snapshot snapshot, Buffer buffer); +``` + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-18_163930.jpg) + +其他判断条件出口为不可见。 + +#### 7.10.2.3 判断当前元组是否已脏 + +如果为脏, 则需要立即从磁盘重新读取该元组, 获得其有效版本。 +```cpp +static bool HeapTupleSatisfiesDirty(HeapTuple htup, Snapshot snapshot, Buffer buffer); +``` + +#### 7.10.2.4 判断元组对MVCC某一版本是否有效 + +#### 7.10.2.5 判断元组是否可更新 +```cpp +typedef enum +{ + HeapTupleMayBeUpdated, + HeapTupleInvisible, + HeapTupleSelfUpdated, + HeapTupleUpdated, + HeapTupleBeingUpdated +} HTSU_Result; + +HTSU_Result +HeapTupleSatisfiesUpdate(HeapTupleHeader tuple, CommandId curcid, + Buffer buffer); +``` + +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-18_165520.jpg) + +`HeapTupleSatisfiesUpdate`函数返回`HeapTupleMayBeUpdated`的判断过程, 如下所示: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-18_165703.jpg) + +### 7.10.3 MVCC与快照 +快照`Snapshot`记录了数据库当前某个时刻的活跃事务列表。 +其数据结构如下: +```cpp +typedef struct SnapshotData +{ + // 7.10.2 MVCC相关操作函数 + SnapshotSatisfiesFunc satisfies; /* tuple test function */ + + /* + * The remaining fields are used only for MVCC snapshots, and are normally + * just zeroes in special snapshots. (But xmin and xmax are used + * specially by HeapTupleSatisfiesDirty.) + * + * An MVCC snapshot can never see the effects of XIDs >= xmax. It can see + * the effects of all older XIDs except those listed in the snapshot. xmin + * is stored as an optimization to avoid needing to search the XID arrays + * for most tuples. + */ + TransactionId xmin; /* all XID < xmin are visible to me */ + TransactionId xmax; /* all XID >= xmax are invisible to me */ + TransactionId *xip; /* array of xact IDs in progress */ + uint32 xcnt; /* # of xact ids in xip[] */ + /* note: all ids in xip[] satisfy xmin <= xip[i] < xmax */ + int32 subxcnt; /* # of xact ids in subxip[] */ + TransactionId *subxip; /* array of subxact IDs in progress */ + bool suboverflowed; /* has the subxip array overflowed? */ + bool takenDuringRecovery; /* recovery-shaped snapshot? */ + bool copied; /* false if it's a static snapshot */ + + /* + * note: all ids in subxip[] are >= xmin, but we don't bother filtering + * out any that are >= xmax + */ + CommandId curcid; /* in my xact, CID < curcid are visible */ + uint32 active_count; /* refcount on ActiveSnapshot stack */ + uint32 regd_count; /* refcount on RegisteredSnapshotList */ +} SnapshotData; +``` + +## 7.11 日志管理 +`XLOG` : 事务日志, 记录事务对数据更新的过程和事务的最终状态。 +`CLOG` : 事务提交日志, 记录了事务的最终状态, 是`XLOG`的一种辅助形式。 +为了降低写入日志带来的`I/O`开销, 数据库系统在实现时往往设置了日志缓冲区, 日志缓冲区满了以后以块为单位向磁盘写出。 + +`pg`中有四种日志管理器: +- `XLOG`: 事务日志 +- `CLOG`: 事务提交日志, 使用`SLRU`缓冲池来实现日志管理 +- `SUBTRANS`: 子事务日志, 使用`SLRU`缓冲池来实现日志管理 +- `MULTIXACT`: 组合事务日志, 使用`SLRU`缓冲池来实现日志管理 + +### 7.11.1 SLRU缓冲池 +对`CLOG`和`SUBTRANS`日志的物理存储组织方法,每个物理日志文件定义成一个段(`32`个`8KB`大小的磁盘页面)。 通过二元组``可以定位日志页在哪个段文件以及在该文件中的偏移位置。 + +### 7.11.1.1 SLRU缓冲池的并发控制 +使用轻量级锁实现对SLRU缓冲池的并发控制。 + +### 7.11.1.2 SLRU缓冲池相关数据结构 +`SlruCtlData`提供缓冲池的控制信息。 +```Cpp +typedef struct SlruCtlData +{ + SlruShared shared; + + /* + * This flag tells whether to fsync writes (true for pg_clog and multixact + * stuff, false for pg_subtrans and pg_notify). + */ + bool do_fsync; + + /* + * Decide which of two page numbers is "older" for truncation purposes. We + * need to use comparison of TransactionIds here in order to do the right + * thing with wraparound XID arithmetic. + */ + bool (*PagePrecedes) (int, int); + + /* + * Dir is set during SimpleLruInit and does not change thereafter. Since + * it's always the same, it doesn't need to be in shared memory. + */ + char Dir[64]; +} SlruCtlData; +``` + +`SlruFlushData`记录打开的物理磁盘文件, 当进行刷写操作时, 将对多个文件的更新数据一次性刷写到磁盘。 +```cpp +typedef struct SlruFlushData +{ + int num_files; /* # files actually open */ + int fd[MAX_FLUSH_BUFFERS]; /* their FD's */ + int segno[MAX_FLUSH_BUFFERS]; /* their log seg#s */ +} SlruFlushData; +``` + +### 7.11.1.3 SLRU缓冲池的主要操作 +- 缓冲池的初始化 +- 缓冲池页面的选择 +- 页面的初始化 +- 缓冲池页面的换入换出 +- 缓冲池页面的删除 + +### 7.11.2 CLOG日志管理器 +CLOG是系统为整个事务管理流程所建立的日志,主要用于记录事务的状态,同时通过`SUBTRANS`日志记录事务的嵌套关系。 + +#### 7.11.2.1 相关数据结构 +事务的最终状态分成以下四种: +```CPp +#define TRANSACTION_STATUS_IN_PROGRESS 0x00 +#define TRANSACTION_STATUS_COMMITTED 0x01 +#define TRANSACTION_STATUS_ABORTED 0x02 +#define TRANSACTION_STATUS_SUB_COMMITTED 0x03 +``` + +通过一个4元组``可定位一条CLOG日志记录。 +通过一个事务的事务`ID`可以获得其日志对应的4元组。 + +- TransactionIdToPage(xid) +- TransactionIdToPgIndex(xid) +- TransactionIdToByte(xid) +- TransactionIdToBIndex(xid) + +`CLOG`的控制信息如下所示: +```cpp +/* + * Link to shared-memory data structures for CLOG control + */ +static SlruCtlData ClogCtlData; + +#define ClogCtl (&ClogCtlData) +``` + +#### 7.11.2.2 CLOG日志管理器主要操作 +- 初始化 +`CLOGShmemInit` +- 写操作 +`TransactionIdSetStatusBit` +- 读操作 +`TransactionIdGetStatus` +- CLOG日志页面的初始化 +`ZeroCLOGPage` +- CLOG日志的启动 +`StartupCLOG` +- CLOG日志的关闭 +`ShutdownCLOG` +- CLOG日志的扩展 +`ExtendCLOG`, 为新分配的事务ID创建CLOG日志空间。 +- CLOG日志的删除 +`TruncateCLOG` +- 创建检查点时CLOG日志的操作 +`CheckPointCLOG` :执行一个CLOG日志检查点,在XLOG执行检查点时被调用。 + +> 我的小问题: `CheckPoint`是什么?完成什么工作? + +`CheckPoint`是事务日志序列中的一个点, 在该点上所有数据文件都已经被更新为日志中反映的信息,所有数据文件将被刷写到磁盘。 + +检查点是在事务序列中的点,这种点保证被更新的堆和索引数据文件的所有信息在该检查点之前已被写入。在检查点时刻,所有脏数据页被刷写到磁盘,并且一个特殊的检查点记录将 被写入到日志文件(修改记录之前已经被刷写到WAL文件)。在崩溃时,崩溃恢复过程检查 最新的检查点记录用来决定从日志中的哪一点(称为重做记录)开始REDO操作。在这一点 之前对数据文件所做的任何修改都已经被保证位于磁盘之上。因此,完成一个检查点后位于 包含重做记录的日志段之前的日志段就不再需要了 + +> 我的小问题: `WAL`文件的物理组织形式是什么? 它是相对于日志文件有什么性能上的优势? + +磁盘盘片的写入处理可能因为电力失效在任何时候失败, 这意味有些扇区写入了数据,而有些没有。为了避免这样的失效,`PostgreSQL`在修改磁盘上的实际页面之前, 周期地把整个页面的映像写入永久`WAL`存储。 + +Checkpoints are points in the sequence of transactions at which it is guaranteed that the heap and index data files have been updated with all information written before the checkpoint. +At checkpoint time, all dirty data pages are flushed to disk and a special checkpoint record is written to the log file. +(The changes were previously flushed to the `WAL` files.) +In the event of a crash, the crash recovery procedure looks at the latest checkpoint record to determine the point in the log (known as the redo record) from which it should start the REDO operation. +Any changes made to data files before that point are guaranteed to be already on disk. Hence, after a checkpoint, log segments preceding the one containing the redo record are no longer needed and can be recycled or removed. (When WAL archiving is being done, the log segments must be archived before being recycled or removed.) + +**CheckPoint中的工程折中问题考虑:** +`CheckPoint`的IO活动会对性能产生较大负担, 所以`CheckPoint`的活动会被有所限制以减少对性能的影响。 +降低`checkpoint_timeout`和/或`max_wal_size`会导致检查点更频繁地发生。这使得崩溃后恢复更快,因为需要重做的工作更少。但是,我们必须在这一点和增多的刷写脏数据页开销之间做出平衡。 + +-------- + +### 7.11.3 SUBTRANS日志管理器 +通过`SUBTRANS`日志记录事务的嵌套关系, 管理一个缓冲池, 存储每一个事务的父事务ID。 + +### 7.11.4 MULTIXACT日志管理器 +用来记录组合事务ID的一种日志, 同一个元组相关联的事务ID可能有多个, 为了在加锁的时候统一操作, PostgreSQL将与该元组相关联的多个事务ID组合起来用`MULTIXACT`代替来管理。 + +### 7.11.5 XLOG日志管理器 +每个`XLOG`文件被分为一个个大小为`16MB`的XLOG段文件来存放。 +XLOG文件号和段文件号可以用来唯一地确定这个段文件, 使用一个`XLOG`文件号和日志记录在该文件内的偏移量可以确定一个日志文件内的一个日志记录。 +#### 7.11.5.1 XLOG日志管理器相关数据结构 +- **日志页面头部信息** +```cpp +typedef struct XLogPageHeaderData +{ + uint16 xlp_magic; /* magic value for correctness checks */ + uint16 xlp_info; /* flag bits, see below */ + TimeLineID xlp_tli; /* TimeLineID of first record on page */ + XLogRecPtr xlp_pageaddr; /* XLOG address of this page */ +} XLogPageHeaderData; +``` + +对于一个长的XLOG日志记录, 可能在当前页面中没有足够空间来存储, 系统允许将剩余的数据存储到下一个页面, 即一个XLOG记录分开存储到不同的页面。 + +```cpp +/* + * When the XLP_LONG_HEADER flag is set, we store additional fields in the + * page header. (This is ordinarily done just in the first page of an + * XLOG file.) The additional fields serve to identify the file accurately. + */ +typedef struct XLogLongPageHeaderData +{ + XLogPageHeaderData std; /* standard header fields */ + uint64 xlp_sysid; /* system identifier from pg_control */ + uint32 xlp_seg_size; /* just as a cross-check */ + uint32 xlp_xlog_blcksz; /* just as a cross-check */ +} XLogLongPageHeaderData; + +``` + +- **日志记录控制信息** +XLOG日志记录结构: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-19_103543.jpg) + +```cpp +typedef struct XLogRecord +{ + pg_crc32 xl_crc; /* CRC for this record */ + XLogRecPtr xl_prev; /* ptr to previous record in log */ + TransactionId xl_xid; /* xact id */ + uint32 xl_tot_len; /* total len of entire record */ + uint32 xl_len; /* total len of rmgr data */ + uint8 xl_info; /* flag bits, see below */ + RmgrId xl_rmid; /* resource manager for this record */ + + /* Depending on MAXALIGN, there are either 2 or 6 wasted bytes here */ + + /* ACTUAL LOG DATA FOLLOWS AT END OF STRUCT */ + +} XLogRecord; +``` + +通过`xl_rmid`和`xl_info`获悉对源数据做的操作类型, 迅速正确地调用对应的函数。 + +- **日志记录数据信息** +```cpp +typedef struct XLogRecData +{ + char *data; /* start of rmgr data to include */ + uint32 len; /* length of rmgr data to include */ + Buffer buffer; /* buffer associated with data, if any */ + bool buffer_std; /* buffer has standard pd_lower/pd_upper */ + struct XLogRecData *next; /* next struct in chain, or NULL */ +} XLogRecData; + +/* 备份数据块头部信息 */ +typedef struct BkpBlock +{ + RelFileNode node; /* relation containing block */ + ForkNumber fork; /* fork within the relation */ + BlockNumber block; /* block number */ + uint16 hole_offset; /* number of bytes before "hole" */ + uint16 hole_length; /* number of bytes in "hole" */ + + /* ACTUAL BLOCK DATA FOLLOWS AT END OF STRUCT */ +} BkpBlock; + +/* 指向XLOG中某个位置的指针 */ +typedef struct XLogRecPtr +{ + uint32 xlogid; /* log file #, 0 based */ + uint32 xrecoff; /* byte offset of location in log file */ +} XLogRecPtr; +``` + +- **XLOG控制结构** +```cpp +typedef struct XLogCtlData +{ + /* Protected by WALInsertLock: */ + XLogCtlInsert Insert; + + /* Protected by info_lck: */ + XLogwrtRqst LogwrtRqst; /* 在该值之前的日志记录都已经刷新到磁盘 */ + uint32 ckptXidEpoch; /* nextXID & epoch of latest checkpoint */ + TransactionId ckptXid; + XLogRecPtr asyncXactLSN; /* LSN of newest async commit/abort */ + uint32 lastRemovedLog; /* latest removed/recycled XLOG segment */ + uint32 lastRemovedSeg; + …………………… + slock_t info_lck; /* locks shared variables shown above */ +} XLogCtlData; +``` + +- **XLOG日志的checkpoint策略** +```cpp +typedef struct CheckPoint +{ + XLogRecPtr redo; /* next RecPtr available when we began to + * create CheckPoint (i.e. REDO start point) */ + TimeLineID ThisTimeLineID; /* current TLI */ + bool fullPageWrites; /* current full_page_writes */ + uint32 nextXidEpoch; /* higher-order bits of nextXid */ + TransactionId nextXid; /* next free XID */ + Oid nextOid; /* next free OID */ + MultiXactId nextMulti; /* next free MultiXactId */ + MultiXactOffset nextMultiOffset; /* next free MultiXact offset */ + TransactionId oldestXid; /* cluster-wide minimum datfrozenxid */ + Oid oldestXidDB; /* database with minimum datfrozenxid */ + pg_time_t time; /* time stamp of checkpoint */ + + /* + * Oldest XID still running. This is only needed to initialize hot standby + * mode from an online checkpoint, so we only bother calculating this for + * online checkpoints and only when wal_level is hot_standby. Otherwise + * it's set to InvalidTransactionId. + */ + TransactionId oldestActiveXid; +} CheckPoint; +``` + +检查点策略如下: +1) 标记当前日志文件中可用的日志记录点,该过程不允许其他事务写日志。 +2) 刷新所有修改过的数据缓冲区, 该过程允许其他事务写日志。 +3) 插入检查点日志记录, 并把日志记录冲刷到日志文件中。 + +#### 7.11.5.2 XLOG日志管理器的主要操作 +> 参考: [深入理解缓冲区(十三)](https://blog.csdn.net/iteye_2060/article/details/82170788) + + +- 日志的启动 +`BootStrapCLOG` +- 日志文件的创建 +`InstallXLogFileSegment` +- 日志的插入 +`XLogInsert` : 事务执行插入、删除、更新、提交、终止或者回滚命令时需要调用该函数。 +- 日志文件的归档 +`XLogWrite` : 将日志写到磁盘日志文件中, 调用本函数时需要持有`WALWriteLock`(日志写锁) +- 日志文件的刷新 +`XLogFlush` : 确保到达给定位置的所有XLOG数据都被刷写回磁盘。 + +> 我的小问题: `XLogWrite`和`XLogFlush`有什么区别? + +实际上`XLogWrite`会调用`XLogFlush`。 + +- 日志的打开操作 +`XLogFileInit` +- 日志文件的拷贝 +`XLogFileCopy` +- 日志备份块的恢复 +`RestoreBackupBlock` +- 日志记录的读取 +`ReadRecord` +- 创建`checkpoint` +`CreateCheckPoint` +- 创建`RestartPoint` +系统在恢复过程中创建的日志恢复检查点。 + +#### 7.11.5.3 日志恢复策略 +系统崩溃重启后调用`StartXlog`入口函数, 该函数扫描`global/pg_control`读取系统的控制信息, 然后扫描XLOG日志目录的结构读取到最新的日志checkpoint记录, 判断系统是否处于非正常状态下, 若系统处于非正常状态, 则触发恢复机制进行恢复。 +以下三种情况需要执行恢复操作: +- 日志文件中扫描到`bakeup_label`文件 +- 根据`ControlFile`记录的最新checkpoint读取不到记录日志 +- 根据`ControlFile`记录的最新checkpoint与通过该记录找到的检查点日志中的Redo位置不一致。 + +恢复操作的具体步骤: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-19_144711.jpg) + +### 7.11.6 日志管理器总结 +`CLOG`是系统为整个事务管理流程所建立的日志,主要用于记录事务的状态,同时通过`SUBTRANS`日志记录事务的嵌套关系。 +`XLOG`是数据库日志的主体,记录数据库中所有数据操作的变化过程, `XLOG`日志管理器模块提供了日志操作的`API`供其他模块调用。 + +日志处理模块关系图: +![](https://suzixinblog.oss-cn-shenzhen.aliyuncs.com/2021-02-19_085737.jpg) + +## 7.12 小结 diff --git a/_posts/2021-07-30-dragonbook.md b/_posts/2021-07-30-dragonbook.md new file mode 100644 index 0000000..983e7d0 --- /dev/null +++ b/_posts/2021-07-30-dragonbook.md @@ -0,0 +1,649 @@ +--- +title: dragonbook +layout: post +categories: 编译器 +tags: 编译器 书籍 +excerpt: dragonbook-c1 +--- + # 书本资源 +- 对应大学课程:[编译原理哈工大课程](https://www.icourse163.org/course/HIT-1002123007) +- [电子书](https://github.com/iBreaker/book.git) +- [源码](https://suif.stanford.edu/dragonbook/dragon-front-source.tar) +- [课后答案](https://github.com/fool2fish/dragon-book-exercise-answers) + +# Introduction +一个语言处理系统的基本结构: +![lBQYM4.png](https://s2.ax1x.com/2020/01/05/lBQYM4.png) + +## 1.1 Language Processors +> A compiler that translates a high-level language into another high-level +language is called a *source-to-source* translator. + +这种源到源翻译器可以用来做建模分析和性能优化。 + +## 1.2 The Structure of a Compiler +编译器的各个步骤: +![lBv0fK.png](https://s2.ax1x.com/2020/01/05/lBv0fK.png) + +一个翻译的例子: +![lBxnje.png](https://s2.ax1x.com/2020/01/05/lBxnje.png) + +### 1.2.1 Lexical Analysis +词法分析的输出:`(token-name, attribute-value)` +其中,`token-name`是语法分析步骤使用的抽象符号,`attribute-value`指向符号表中关于这个词法单元的条目。 + +### 1.2.2 Syntax Analysis +**从输入输出的角度来说**,语法分析使用由词法分析器生成的各个词法单元的第一个分量来创建树形的中间表示。 +**从文法的角度来描述**,接收一个终结符号串作为输入,找出从文法的开始符号推到出这个串的方法,如果找不到这样的方法, 则报告语法错误。 + +### 1.2.3 Semantic Analysis +语义分析使用语法树和符号表中的信息来检查源程序是否和语言定义的语义一致。 +在这个阶段会执行一些检查,例如类型检查,如果不比配可能会做自动类型转换。 + +### 1.2.4 Intermediate Code Generation +在源程序的语法分析和语义分析完成后,编译器生成一个明确的低级的或者是类机器语言的中间表示。 + +### 1.2.5 Code Optimization +机器无关的代码优化步骤:改进中间代码,以生成更好的目标代码。 + +### 1.2.6 Code Generation +对源程序中的标识符进行存储分配、寄存器进行合理分配,生成可重定位的机器代码。 + +### 1.2.7 Symbol-Table Management +记录源程序中所使用的变量、函数的名字。 + +### 1.2.8 The Grouping of Phases into Passes +多个步骤的活动可以组合成一趟。 + +### 1.2.9 Compiler-Construction Tools +常见的编译工具如下所示: +1. **Parser generators** that automatically produce syntax analyzers from a +grammatical description of a programming language. +2. **Scanner generators** that produce lexical analyzers from a regular-expression +description of the tokens of a language. +3. **Syntax-directed translation engines** that produce collections of routines +for walking a parse tree and generating intermediate code. +4. **Code-generator generators** that produce a code generator from a collection +of rules for translating each operation of the intermediate language into +the machine language for a target machine. +5. **Data-flow analysis engines** that facilitate the gathering of information +about how values are transmitted from one part of a program to each +other part. Data-flow analysis is a key part of code optimization. +6. **Compiler-construction toolkits** that provide an integrated set of routines +for constructing various phases of a compiler. + +## 1.5 Applications of Compiler Technology +除了构造编译器以外还有其他很多应用 + +### 1.5.1 Implement at ion of High-Level Programming Languages +在动态编译中(如`Java`的`JIT`)尽可能降低编译时间是很重要的, 因为编译时间也是运行开销的一部分。一个常用的技术是只编译和优化那些经常运行的程序片段。 + +### 1.5.2 Optimizations for Computer Architectures +- 并行性 + - 编译器对指令重新排布,提高指令的并行性 + - 指令级并行:微处理器流水线自身的指令级并行 +- 内存层次结构 + - 通过改变数据的布局或数据访问代码的顺序来提高内存层次结构的效率 + - 通过改变代码的布局来提高指令高速缓存的效率。 + +### 1.5.3 Design of New Computer Architectures +- RISC +- CISC +使用高性能x86机器的最有效方式是仅使用它的简单指令。 + +### 1.5.4 Program Translations +编译:从一个高级语言到机器语言的翻译过程。 +实际上可以延伸为:不同种类语言之间的翻译。 +例如二进制翻译——直接将x86的二进制代码翻译成其他架构下的二进制代码。 + +### 1.5.5 Software Productivity Tools +使用编译技术开发的静态检查工具。 + +## 1.6 Programming Language Basics +### 1.6.1 The Static/Dynamic Distinction +静态:不需要运行程序就可以决定的问题 +动态:只允许在运行程序的时候做出决定的问题 + +### 1.6.2 Environments and States +从一个变量名得到它对应的值实际上有两个步骤:`environment`和`state`。 +![lDpDUA.png](https://s2.ax1x.com/2020/01/05/lDpDUA.png) +首先根据当前的`environment`判断这个变量名所对应的内存地址,然后再从这个内存地址(也有可能是寄存器)得到这个变量的值。 + +### 1.6.3 Static Scope and Block Structure +一个例子如下所示: +![lD90MT.png](https://s2.ax1x.com/2020/01/05/lD90MT.png) + +![lD96o9.png](https://s2.ax1x.com/2020/01/05/lD96o9.png) + +### 1.6.4 Explicit Access Control +`c++`中可以通过`public`、`private`和`protected`这样的关键字来提供对类中的成员的显示访问控制。 +声明告诉我们事务的类型,定义告诉我们它们的值。 + +### 1.6.5 Dynamic Scope +定义:一个作用域策略依赖于一个或多个只有在程序执行时刻才能知道的因素 +例子: +```c +#define a (x+1) +int x = 2; +void b() { int x = I ; printf (ll%d\nlla), ; } +void c() { printf("%d\nI1, a);} +void main() { b(); c(); } +``` +静态作用域和动态作用域的类比:动态规则处理时间的方式类似于静态作用域处理空间的方式。 +![lDCmmF.png](https://s2.ax1x.com/2020/01/05/lDCmmF.png) + + +### 1.6.6 Parameter Passing Mechanisms +- 值调用 +- 引用调用 +- 名调用(仅在早起使用) + +### 1.6.7 Aliasing +一个例子: +```c +#include +void q(int x[], int y[]) { + x[10] = 2; + printf("y[10] = %d\n", y[10]); +} + +int main() { + int a[20]; + q(a, a); + printf("a[10] = %d\n", a[10]); + return 0; +} +``` + +很多时候我们必须确认某些变量之间不是别名之后才可以优化程序。 + +# 2 A Simple Syntax-Directed Translator +目标:把源码转换成中间代码(三地址码) + +## 2.1 Introduction +**语法**:描述了程序语言的程序的正确形式 +**语义**:定义了程序的含义——即每个程序在运行时做什么事情。 + +编译器前端模型如下所示: +![lss5RK.png](https://s2.ax1x.com/2020/01/06/lss5RK.png) + +两种中间代码形式如下所示: +![lgY7Os.png](https://s2.ax1x.com/2020/01/08/lgY7Os.png) + +## 2.2 Syntax Definition +解决如何描述一套语法的问题。 + +### 2.2.1 Definition of Grammars +**文法**:用于描述程序设计语言语法的表示方法,被用于组织编译器前端。 +**上下文无关文法**是文法的一种 + +其定义如下所示: +[![lsyEiq.md.png](https://s2.ax1x.com/2020/01/06/lsyEiq.md.png)](https://imgchr.com/i/lsyEiq) + +词法分析的输出:`(token-name, attribute-value)` +其中,`token-name`是语法分析步骤使用的抽象符号,`attribute-value`指向符号表中关于这个词法单元的条目。 +我们常常把`token-name`称为终结符号,因为他们在描述程序设计语言的文法中是以终结符号的形式出现的。 + +### 2.2.2 Derivations +**推导**:从开始符号出发,不断将某个非终结符替换为该非终结符号的某个产生式的体。 +**语言**:从开始符号推导得出的所有终结符号串的集合。 + +### 2.2.3 Parse Trees +语法分析树使用图形的方式展现了从文法的开始符号推导出相应语言中的符号串的过程,所以一个语法分析树的叶子节点从左到右构成了树的结果。 + +每个内部结点和它的子节点对应于一个产生式,内部节点对应于产生式的头,它的子节点对应于产生式的体。 +**语法分析**:为一个给定的终结符号串构建一颗语法分析树的过程称为对该符号传进行语法分析。 + +### 2.2.4 Ambiguity +一个文法可能有多颗语法分析树能够生成同一个给定的终结符号串。 +> 我的小问题:如何通过数学上的证明方法判断文法具有二义性?(除了举反例以外) + +对于任意一个上下文无关文法,不存在一个算法,判定它是无二义性的;但能给出一组充分条件 +满足这组充分条件的文法是无二义性的 +- 满足,肯定无二义性 +- 不满足,也未必就是有二义性的 + +----------------- + +### 2.2.5 Associativity of Operators +运算符的结合性:左结合/右结合。 +> 我的小问题:如何通过文法来设计这种左/右结合性? + +通过把迭代的非终结符符号放在表达式的右侧。例如`a=b=c`可以由以下文法产生。 +![lscBGt.png](https://s2.ax1x.com/2020/01/06/lscBGt.png) + +--------------- + +### 2.2.6 Precedence of Operators +例`2.6`展示了包含`+-*/`优先性考虑的文法,如下所示: +![lsR1Gn.png](https://s2.ax1x.com/2020/01/06/lsR1Gn.png) + +在这里我们可以看到一个规律:非终结符号的数目正好比表达式中优先级的层数多`1`。 + +概念延伸: +- `factor`:不能被任何运算符分开的表达式 +- `term`:可能被高优先级的运算符分开,但不能被低优先级运算符分开的表达式。 + +简单来说,在使用这种表达方式的时候,一个表达式就是一个由`+`或`-`分隔开的`term`的列表,而`term`是由`*`或`/`分隔的因子`factor`的列表。 + +例`2.7`展示了`java`语法的例子。 + +## 2.3 Syntax-Directed Translation +**语法制导**技术是一种面向文法的编译技术。 +**语法制导翻译**是通过向一个文法的产生式附加一些规则或程序片段而得到的,相关概念 +- **属性(Attributes)**:表示与某个程序构造相关的任意的量。 +- **(语法指导的)翻译方案(Syntax-directed)**:将程序片段附加到一个文法的各个产生式上的表示法。 + +例如,对于`expr -> expr1 + term`,可以使用如下伪代码来翻译: +``` +translate expr1; +translate term; +handle +; +``` + +### 2.3.1 Postfix Notation +例`2.8`以从常规表达式翻译成后缀表达式为例,解释了语法指导翻译的基本过程 +例`2.9`解释了后缀表达式的计算过程。 + +### 2.3.2 Synthesized Attributes +**语法制导(`Syntax-Directed`)**:把每个文法符号和一个属性集合相关联,并且把每个产生式和一组语义规则相关联,这些规则用于计算与该产生式中符号相关联的属性值。 + +**注释语法分析树**:一颗语法分析树的各个结点上标记了相应的属性值。 +例`2.10`介绍了如何根据图`2.10`中的语法制定定义得到一颗注释分析树。 +图`2.10`就是对产生式附加的规则。 +![lswZRg.png](https://s2.ax1x.com/2020/01/06/lswZRg.png) +![lsN3gs.png](https://s2.ax1x.com/2020/01/06/lsN3gs.png) + +### 2.3.3 Simple Syntax-Directed Definitions +**简单语法制导**:对于这种情形,要得到代表产生式头部的非终结符符号的翻译结果的字符串,只需要将产生式体中各非终结符号的翻译结果按照它们在非终结符号中的出现顺序连接起来,并在其中穿插一些附加的串即可。 + +### 2.3.5 Translation Schemes +**语法制导翻译方案**是一种在文法产生式中附加一些程序片段来描述翻译结果的表示方法。 + +可以看到例`2.12`和例`2.10`是两种不同的翻译方案。 +例`2.12`在例`2.10`的基础上添加了实时打印结点的语义动作。如下所示: +![lsDfm9.png](https://s2.ax1x.com/2020/01/06/lsDfm9.png) +![lsB7rj.png](https://s2.ax1x.com/2020/01/06/lsB7rj.png) + +注意例`2.10`和例`2.12`构造结果的过程上的区别。 +例`2.10`是把字符串作为属性附加到语法分析树中的结点上,而例`2.12`通过语义动作把翻译结果以增量方式打印出来。 + +## 2.4 Parsing +**词法分析**是决定如何使用一个文法生成一个终结符号串的过程,在讨论这个问题时。**可以想象正在构造一个语法分析树**,这样有助于理解分析的问题。 + +### 2.4.1 Top-Down Parsing +**自顶向下分析方法**:构造从根节点开始,逐步向叶子结点方向进行。一般来说,为一个非终结符号选择产生式是一个尝试并犯错的过程。 +如以下的例子所示。 +文法: +![l6gEWR.png](https://s2.ax1x.com/2020/01/07/l6gEWR.png) +语法分析树: +![lymAfK.png](https://s2.ax1x.com/2020/01/06/lymAfK.png) + +输入中当前被扫描的终结符号通常称为向前看(`lookahead`)符号,如图中箭头所示。 + +### 2.4.2 Predictive Parsing +**递归下降分析方法**(recursive-descent)是一种自顶向下的语法分析方法,它使用一组递归过程来处理输入。 +递归下降分析法的一种简单形式是**预测分析法**。 + +将`FIRST(a)`定义为可以由`a`生成的一个或多个终结符号串的第一个符号的集合。 +对于`2.16`的文法,其`FIRST`的计算如下: + +``` +FIRST(stmt) = {expr, if, for, other) +FIRST(expr; ) = {expr} +``` + +预测分析法的伪代码如下所示: +``` +void stmt() { + switch ( lookahead ) { + case expr: + match(expr); match(' ; '); break; + case if: + match(if); match(' (I); match (expr); match(') '); stmt (); + break; + case for: + match (for); match (' (') ; + optexpr (); match(' ; I); optexpr(); match(' ; '); optexpr(); + match(') '); stmt (); break; + case other; + match (ot her) ; break; + default: + report ("synt ax error"); + } +} + +void optexpro { + if ( lookahead == expr ) match(expr); +} + +void match(termina1 t) { + if ( Eookahead == t ) Eookahead = nextTermina1; + else report ("syntax error If); +} +``` + +### 2.4.3 When to Use 6-Productions +我们的预测分析器在没有其他产生式可用时,将`ε`产生式作为默认选择使用。 + +### 2.4.4 Designing a Predictive Parser +一个预测分析器构造过程可以理解为是`2.4.2`的伪代码的一个抽象。 +一个预测分析器程序由各个非终结符对应的过程组成。 +- 检查`lookahead`符号,决定使用A的哪个产生式 +- 然后,这个过程模拟被选中的产生式的体。 + +我们可以基于以上基本过程扩展得到一个语法制导的翻译器。 + +### 2.4.5 Left Recursion +`A->Aa`的右部的最左符号是`A`自身,非终结符号`A`和它的产生式就称为**左递归**的。 +`A->Aa | β`也是左递归的。 + +> 如何消除左递归? + +可改写成如下形式 +``` +A -> βR +R -> aR | ε +``` +-------------------- + +同理,形如`R->aR`的产生式被称为右递归的。 +左递归的产生式在使用递归下降分析方法时可能会出现无限循环,右递归的产生式对包含了左结合运算符的表达式的翻译较为困难。 + +> 左递归这个概念和前面提到的运算符的左结合有何关联? + +-------- + + +### 2.4.6 Exercises for Section 2.4 +这几个练习蛮好,值得练下。 + +## 2.5 A Translator for Simple Expressions +基本的过程: +- 首先使用易于翻译的文法 +- 再小心地对这个文法进行转换,使之能够支持语法分析。 + +### 2.5.1 Abstract and Concrete Syntax +在抽象(Abstract)语法树中,内部节点代表的是程序构造;而在语法(Concrete)分析树中,内部节点代表的是非终结符号。 + +### 2.5.2 Adapting the Translation Scheme +例`2.13`: +![l6zdBT.png](https://s2.ax1x.com/2020/01/07/l6zdBT.png) +以上产生式是左递归的, 其抽象形式如下 + +``` +A = expr +α = + term { print ('+') } +β = - term { print('-') } +γ = term + +A -> Aα | Aβ | γ +``` + +消除左递归,更改为 +``` +A -> γR +R -> αR | βR | ε +``` + +展开如下所示: +![lc9MB6.png](https://s2.ax1x.com/2020/01/07/lc9MB6.png) + +### 2.5.3 Procedures for the Nonterminals +```c +void expro { + term () ; rest () ; +} + +void rest() { + if ( lookahead == '+' ) { + match('+') ; term () ; print ('+'); rest () ; + } else if ( lookahead == '-' ) { + match('-'); term(); print ('-'); rest (); + } else { } /* do nothing with the input */ ; +} + +void term() { + if ( lookahead is a digit ) { + t = lookahead, match(1ookahead); print (t); + else report (" syntax error") ; + } +} +``` + +### 2.5.4 Simplifying the Translator +- 将尾递归改成循环 +- 将多余的函数接口合一 + +### 2.5.5 The Complete Program +```java +import java.io.*; +class Parser { + static int lookahead; + public Parser() throws IOException { + lookahead = System.in.read(); + } + + void expro throws IOException { + term() ; + while (true) { + if ( lookahead == '+' ) { + match('+') ; term() ; System.out.write('+'); + } + else if ( lookahead == '-' ) { + match('-'); term(); System.out + } + else return; + } + } + + void term() throws IOException { + if ( Character. isDigit ((char) lookahead)) { + System. out. write( (char) lookahead) ; match(1ookahead) ; + } + else throw new Error("syntax error"); + } + + void match(int t) throws IOException { + if ( lookahead == t ) lookahead = System. in.read() ; + else throw new Error("syntax error") ; + } +} + +public class Postfix { + public static void main(StringC1 args) throws IOException { + Parser parse = new Parser(); + parse.expr() ; System.out. write('\n') ; + } +} +``` + +## 2.6 Lexical Analysis +一个词法分析器从输入中读取字符,并将他们组成**词法单元对象**。 + +> 词法单元和终结符号的关系是什么? + +除了用于语法分析的终结符号之外,一个词法单元对象还包含一些附加信息,这些信息以属性值的形式出现。 + +--------------------- +### 2.6.5 A Lexical Analyzer +用一个哈希表`word`来保存关键字和标识符,程序中类的继承关系如下图所示: + +![lgnh7Q.png](https://s2.ax1x.com/2020/01/08/lgnh7Q.png) + +伪代码如下所示: +```java +Token scan() { + skip white space, as in Section 2.6.1; + handle numbers, as in Section 2.6.3; + handle reserved words and identifiers, as in Section 2.6.4; + /* if we get here, treat read-ahead character peek as a token */ + Token t = new Token(peelc); + peek = blank /* initialization, as discussed in Section 2.6.2 */ ; + return t; +} +``` +完整代码见`front/lexer`。 + +## 2.7 Symbol Tables +**符号表**是一种供编译器用于保存有关源程序构造的各种信息的数据结构。 +**作用域**:起该声明作用的那一部分程序,为了实现作用域,我们将为每个作用域建立一个单独的符号表,这个作用域中的每个声明都在此符号表中有一个对应的条目。 + +> 谁来创建符号表? + +符号表条目是在分析阶段由词法分析器、语法分析器和语义分析器创建并使用的。 +在书中的这个例子中,作者使用语法分析器来创建这些条目。 + +小目标: +输入: +```c +{ + int x; + char y; + { + bool y; + x; + y; + } + x; + y; +} +``` + +输出: +```c +{ + { + x:int; + y:bool; + } + x:int; + y:char; +} +``` + +### 2.7.1 Symbol Table Per Scope +文法声明如下所示: +![lgMpy6.png](https://s2.ax1x.com/2020/01/08/lgMpy6.png) + +例`2.15`用下标来区分对同一标识符的不同声明: +![lg3Qvn.png](https://s2.ax1x.com/2020/01/08/lg3Qvn.png) + +例`2.16`显示了例`2.15`中伪代码的符号表: +![lg3t5F.png](https://s2.ax1x.com/2020/01/08/lg3t5F.png) + +对应链接符号表的代码实现见`front/symbols/Env.java`。 + +```java +package symbols; +import java.util.*; import lexer.*; import inter.*; + +public class Env { + + private Hashtable table; + protected Env prev; + + public Env(Env n) { table = new Hashtable(); prev = n; } + + public void put(Token w, Id i) { table.put(w, i); } + + public Id get(Token w) { + for( Env e = this; e != null; e = e.prev ) { + Id found = (Id)(e.table.get(w)); + if( found != null ) return found; + } + return null; + } +} + +``` +### 2.7.2 The Use of Symbol Tables +![lg8pZV.png](https://s2.ax1x.com/2020/01/08/lg8pZV.png) + +## 2.8 Intermediate Code Generation + +### 2.8.1 Two Kinds of Intermediate Representations +- 树形结构 +- 线性表示形式 + +### 2.8.2 Construction of Syntax Trees +![lgaQB9.png](https://s2.ax1x.com/2020/01/08/lgaQB9.png) +主要可以分为: +- 语句的抽象语法树 +- 在抽象语法树中表示语句块 +- 表达式的语法树 + +### 2.8.3 Static Checking +**静态检查**是指在编译过程中完成的各种一致性的检查。包括 +- 语法检查 +它们并没有包括在用于语法分析的文法中,语法检查的一个例子是左值和右值检查 +- 类型检查 +类型检查规则按照抽象语法中运算符/运算分量的结构进行描述,主要运用了**实际类型和期望类型相匹配**的思想。 + +### 2.8.4 Three-Address Code +通过遍历语法树来生成三地址代码。 +- 语句的翻译 +举了翻译`if`语句的例子: +从代码结构来说,每一个类内部包含一个构造函数和生成三地址代码的函数`gen`。 +![lgXqk4.png](https://s2.ax1x.com/2020/01/08/lgXqk4.png) +![lgXOh9.png](https://s2.ax1x.com/2020/01/08/lgXOh9.png) +- 表达式的翻译 +![lgjB4J.png](https://s2.ax1x.com/2020/01/08/lgjB4J.png) +![lgjyg1.png](https://s2.ax1x.com/2020/01/08/lgjyg1.png) + - 例`2.19`:翻译`a[2*k]` + - 例`2.20`:翻译`a[i] = 2*a[j-k]` + +优化策略:首先使用一个简单的中间代码生成方法,然后依靠代码优化器来消除不必要的指令。 + +## 2.9 Summary of Chapter 2 +另一个关于本章工作的例子: +![l285gH.png](https://s2.ax1x.com/2020/01/08/l285gH.png) + +# 6 Intermediate-Code Generation +## 6.1 Variants of Syntax Trees +通过`DAG`来代替语法树,表示程序的拓扑结构 +### 6.1.1 Directed Acyclic Graphs for Expressions +### 6.1.2 The Value-Number Method for Constructing DAG's +### 6.1.3 Exercises for Section 6.1 + + +# 8 Code Generation +## 8.5 Optimization of Basic Blocks +如何使用`DAG`来做局部代码优化 +### 8.5.1 The DAG Representation of Basic Blocks +按照特定的方式为每一个基本块构造`DAG`,再之后根据这个`DAG`表示可以对基本块所表示的代码进行一些转换。 + +### 8.5.2 Finding Local Common Subexpressions +一个例子: +``` +a = b + c +b = a - d +c = b + c +d = a - d +``` +`d`这个节点是多余的,可以并入`b`中,如下所示: +![1iYPTP.png](https://s2.ax1x.com/2020/01/20/1iYPTP.png) + +### 8.5.3 Dead Code Elimination +从一个`DAG`上删除所有没有附加活跃变量的根节点(没有父节点的节点)。 + +### 8.5.4 The Use of Algebraic Identities +- 代数恒等式 +- 局部强度消减(reduction in strength) +- 常量合并(constant folding) + +### 8.5.5 Representation of Array References +### 8.5.6 Pointer Assignments and Procedure Calls +### 8.5.7 Reassembling Basic Blocks From DAG's +### 8.5.8 Exercises for Section 8.5 + +# 9 Machine-Independent Optimizations 583 +## 9.1 The Principal Sources of Optimization +### 9.1.1 Causes of Redundancy +### 9.1.2 A Running Example: Quicksort +### 9.1.3 Semantics-Preserving Transformations +### 9.1.4 Global Common Subexpressions +### 9.1.5 Copy Propagation +### 9.1.6 Dead-Code Elimination +### 9.1.7 Code Motion +### 9.1.8 Induction Variables and Reduction in Strength diff --git a/_posts/2021-07-30-language-implementation-patterns.md b/_posts/2021-07-30-language-implementation-patterns.md new file mode 100644 index 0000000..b6faba1 --- /dev/null +++ b/_posts/2021-07-30-language-implementation-patterns.md @@ -0,0 +1,295 @@ +--- +title: 编程语言实现模式 +layout: post +categories: 编译器 +tags: 编译器 书籍 +excerpt: 编程语言实现模式 +--- +> [编程语言实现模式](https://github.com/Lucifier129/language-implementation-patterns/blob/master/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%AE%9E%E7%8E%B0%E6%A8%A1%E5%BC%8F.pdf) +[Language Implementation Patterns : Create Your Own Domain-Specific and General Programming Languages](https://doc.lagout.org/programmation/Pragmatic%20Programmers/Language%20Implementation%20Patterns.pdf) + +相比于龙书,这本书更浅显易懂,实操性更强一些。而且这本书不止于编译器,它包括了语言类的应用。 +这本书介绍了语言应用常见的`31`种模式,在构造自己的应用时,可以使用搭积木一样的方式来选择这些模式。 + +# 1. 初探语言应用 +## 1.1. 大局观 +![1HEdj1.png](https://s2.ax1x.com/2020/02/12/1HEdj1.png) + +语言应用的分类: +- 文件读取器 +- 生成器 +收集内部数据结构的信息然后产生输出。 +- 翻译器 +文件读取器和生成器的组合,汇编器和编译器都属于翻译器 +- 解释器 +计算器、编程语言java、python + +## 1.2. 模式概览 +本书会介绍`31`种不同的模式。 + +- 解析输入语句 +- 构建语法树 +`IR`实际上就是处理过了的输入内容。 +- 遍历树 + 两种方法: + - 把方法内置于每个节点的类里 + - 把方法封装在外部访问者里 +- 弄清输入的含义 + 符号表是符号的字典,符号有它的作用域,具体可分为: + - 单一作用域 + - 嵌套作用域 + - C结构体作用域 + - 类作用域 +- 解释输入语句 +介绍了常见解释器的模式 +- 翻译语言 +计算输出和产生输出这两个阶段还是应该分开进行。 + +## 1.3. 深入浅出语言应用 +介绍几个语言应用的架构。 +### 1.3.1. 字节码解释器 +![1HnAJO.png](https://s2.ax1x.com/2020/02/12/1HnAJO.png) + +解释器用软件模拟出硬件处理器,因此也被称为虚拟机。 +解释器需要完成的工作: +- 取指令-解码-执行 +- 产生输出的指令 + +### 1.3.2. Java查错程序 +这里举例说明设计一个查找自赋值错误的查错程序的步骤。 +输入:源代码文件,比如以下的一个错误函数 +```java +void setX(int y) {this.x = x;} +``` +输出:bug list + +![1HM61S.png](https://s2.ax1x.com/2020/02/12/1HM61S.png) +![1HlcZj.png](https://s2.ax1x.com/2020/02/12/1HlcZj.png) +采用多次扫描还能支持前向引用。 + +### 1.3.3. Java查错程序2 +输入:`.class`文件 +输出:bug list + +![1H8bwQ.png](https://s2.ax1x.com/2020/02/12/1H8bwQ.png) + +### 1.3.4. C编译器 +编译器最复杂的地方是语义分析和优化。 + +![1HYQJA.png](https://s2.ax1x.com/2020/02/12/1HYQJA.png) +![1HdPvn.png](https://s2.ax1x.com/2020/02/12/1HdPvn.png) + +### 1.3.5. 借助C编译器实现C++语言 +这也是C++之父创造C++的过程: +![1H01AK.png](https://s2.ax1x.com/2020/02/12/1H01AK.png) + +## 1.4. 为语言应用选择合适的模式 +程序员常常做的两种事情: +- 实现某种`DSL`(`Domain Specific Languages`) +- 处理或翻译`GPPL`(`General-Purpose Programming Language`) + +解析器是语法分析的必要工具,符号表对于分析输入内容的语义也有至关重要的地位。 +syntax tells us what to do, and semantics tells us what to do it to. + +# 2. 第2章 基本解析模式 +**文法**可以看做是语法解析器的功能说明书或设计文档。 +文法可以看成是某种`DSL`编写的能执行的程序。`ANTLR`等解析器的生成工具能够根据文法自动生成解析器,自动生成的解析器代码少,稳定,不容易出错。 + +## 2.1. 识别式子的结构 +**解析(Parse)**:将线性的词法单元序列组成带有结构的解析树。 + +## 2.2. 构建递归下降语法解析器 +**语法解析器**可以检查句子的结构是否符合语法规范。 +解析器的任务是遇到某种结构,就执行某些操作。 +`LL(1)`:向前看一个词法单元的自顶向下解析器。 +手写解析器的一个例子: +```cpp +/** To parse a statement, call stat(); */ +void stat() { returnstat(); } +void returnstat() { match("return" ); expr(); match(";" ); } +void expr() { match("x" ); match("+" ); match("1" ); } +``` +手写解析器挺无聊的,所以考虑用`DSL`来构建语法解析器。 + +## 2.3. 使用文法DSL来构建语法解析器 +**文法**:用专用的`DSL`来描述语言。 +**解析器生成器**:能将文法翻译为解析器的工具。 +**`ANTLR`**:(全名:ANother Tool for Language Recognition)是基于`LL(*)`算法实现的语法解析器生成器(`parser generator`),用Java语言编写,使用自上而下(`top-down`)的递归下降`LL`剖析器方法。由旧金山大学的`Terence Parr`博士等人于1989年开始发展。 +举例如下: +``` +stat : returnstat // "return x+0;" or + | assign // "x=0;" or + | ifstat // "if x<0 then x=0;" +; +returnstat : 'return' expr ';' ; // single-quoted strings are tokens +assign : 'x' '=' expr ; +ifstat : 'if' expr 'then' stat ; +expr : 'x' '+' '0' // used by returnstat + | 'x' '<' '0' // used by if conditional + | '0' // used in assign + ; +``` + +可以使用`ANTLRWorks`的图形输入界面来输入产生式: +![1qE6CF.png](https://s2.ax1x.com/2020/02/13/1qE6CF.png) + +## 2.4. 词法单元和句子 +**词法解析器**:能处理输入字符流的解析器 +**例子**: +识别形如`[a,b,c]` 、`[a,[b,c],d]`的列表。 +语法: +``` +grammar NestedNameList; +list : '[' elements ']' ; // match bracketed list +elements : element (',' element)* ; // match comma-separated list +element : NAME | list ; // element is name or nested list +NAME : ('a'..'z' |'A'..'Z' )+ ; // NAME is sequence of >=1 letter +``` + +对应生成的解析树: +![1qVaGD.png](https://s2.ax1x.com/2020/02/13/1qVaGD.png) + +### 2.4.1. 模式1:从文法到递归下降识别器 +#### 2.4.1.1. 目的 +此模式介绍了直接根据**文法**生成**识别器**的方法。 +局限性: +- 对左递归文法无能为力 + +#### 2.4.1.2. 实现 +```java +public class G extends Parser { // parser definition written in Java + «token-type-definitions» + «suitable-constructor» + «rule-methods» +} +``` + +#### 2.4.1.3. 规则的转换 +```java +public void r() { + ... +} +``` + +#### 2.4.1.4. 词法单元的转换 +- 定义词法单元类型`T` +- 如果规则出现了类型`T`,则调用`match(T)`来`eat`掉这个`Token` + +#### 2.4.1.5. 子规则的转换 +**例子**: +- 子规则 +`(«alt1 »|«alt2 »|..|«altN »)` +- 控制流 +![1que9f.png](https://s2.ax1x.com/2020/02/13/1que9f.png) +- 一般实现方式 +```cpp +if ( «lookahead-predicts-alt1 » ) { «match-alt1 » } +else if ( «lookahead-predicts-alt2 » ) { «match-alt2 » } +... +else if ( «lookahead-predicts-altN » ) { «match-altN » } +else «throw-exception» // parse error (no viable alternative) +``` + +- `switch`实现方式 +```cpp +switch ( «lookahead-token» ) { +case «token1-predicting-alt1 » : +case «token2-predicting-alt1 » : +... +«match-alt1 » +break; +case «token1-predicting-alt2 » : +case «token2-predicting-alt2 » : +... +«match-alt2 » +break; +... +case «token1-predicting-altN » : +case «token2-predicting-altN » : +... +«match-altN » +break; +default : «throw-exception» +} +``` + +#### 2.4.1.6. 转换子规则操作符 +- `optional` +`if ( «lookahead-is-T » ) { match(T); } // no error else clause` + +- `one_or_more` +```cpp +do { + «code-matching-alternatives» +} while ( «lookahead-predicts-an-alt-of-subrule» ); +``` + +- `zero_or_more` +```cpp +while ( «lookahead-predicts-an-alt-of-subrule» ) { + «code-matching-alternatives» +} +``` + +### 2.4.2. 模式2:`LL(1)`递归下降的词法解析器 +#### 2.4.2.1. 目的 +该词法解析器能识别字符流中的模式,生成词法单元流。 + + +第3章 高阶解析模式 49 +3.1 利用任意多的向前看符号进行解析 50 +3.2 记忆式解析 52 +3.3 采用语义信息指导解析过程 52 +第2部分 分析输入 +第4章 从语法树构建中间表示 73 +4.1 为什么要构建树 75 +4.2 构建抽象语法树 77 +4.3 简要介绍ANTLR 84 +4.4 使用ANTLR文法构建AST 86 +第5章 遍历并改写树形结构 101 +5.1 遍历树以及访问顺序 102 +5.2 封装访问节点的代码 105 +5.3 根据文法自动生成访问者 107 +5.4 将遍历与匹配解耦 110 +第6章 记录并识别程序中的符号 131 +6.1 收集程序实体的信息 132 +6.2 根据作用域划分符号 134 +6.3 解析符号 139 +第7章 管理数据聚集的符号表 155 +7.1 为结构体构建作用域树 156 +7.2 为类构建作用域树 158 +第8章 静态类型检查 181 +第3部分 解释执行 +第9章 构建高级解释器 219 +9.1 高级解释器存储系统的设计 220 +9.2 高级解释器中的符号记录 222 +9.3 处理指令 224 +第10章 构建字节码解释器 239 +10.1 设计字节码解释器 241 +10.2 定义汇编语言语法 243 +10.3 字节码机器的架构 245 +10.4 如何深入 250 +第4部分 生成输出 +第11章 语言的翻译 278 +11.1 语法制导的翻译 280 +11.2 基于规则的翻译 281 +11.3 模型驱动的翻译 283 +11.4 创建嵌套的输出模型 291 +第12章 使用模板生成DSL 312 +12.1 熟悉StringTemplate 313 +12.2 StringTemplate的性质 316 +12.2 从一个简单的输入模型生成模板 317 +12.4 在输入模型不同的情况下复用模板 320 +12.5 使用树文法来创建模板 323 +12.6 对数据列表使用模板 330 +12.7 编写可改变输出结果的翻译器 336 +第13章 知识汇总 348 +13.1 在蛋白质结构中查找模式 348 +13.2 使用脚本构建三维场景 349 +13.3 处理XML 350 +13.4 读取通用的配置文件 352 +13.5 对代码进行微调 353 +13.6 为Java添加新的类型 354 +13.7 美化源代码 355 +13.8 编译为机器码 356 + diff --git a/_posts/2021-07-30-perfbook-c1.md b/_posts/2021-07-30-perfbook-c1.md new file mode 100644 index 0000000..54e1d58 --- /dev/null +++ b/_posts/2021-07-30-perfbook-c1.md @@ -0,0 +1,36 @@ +--- +title: perfbook-c1 +layout: post +categories: perfbook +tags: perfbook 并行编程 书籍 +excerpt: perfbook-c1 +--- +> [Is Parallel Programming Hard, And, If So, What Can You Do About It?](https://mirrors.edge.kernel.org/pub/linux/kernel/people/paulmck/perfbook/perfbook.html) +[《深入理解并行编程》](http://ifeve.com/wp-content/uploads/2013/05/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E5%B9%B6%E8%A1%8C%E7%BC%96%E7%A8%8BV1.0.pdf) + +# 1 How To Use This Book +> 不同的程序会有不同的性能瓶颈。并行计算只能淡化某些瓶颈。 +比如,假设你的程序花费最多的时间在等待磁盘驱动的数据。在这种情况下,让你的程序在多 CPU 下运行并不大可能会改善性能。实际上,如果进程正在读取一个旋转的磁盘上的大型顺序文件,并行设计程序也许会使它变得更慢。相反,你应该添加更多的磁盘、 优化数据以使这个文件能变得更小(因此读的更快),或者,如果可能的话,避免读取如此多的数据。 + +不是所有的程序都适合使用并行来提高性能,前提依赖于这个程序有良好的并行化设计。 + +> This book is a handbook of widely applicable and heavily used design techniques, rather than a collection of optimal algorithms with tiny areas of applicability. + +前言中的这句话体现了`Paul`的技术格局和胸怀,要搞通用的输出而不是一些极个别领域的最佳算法和优化措施。 + + +> Don’t get me wrong, passively reading the material can be quite valuable, but gaining full problem-solving capability really does require that you practice solving problems. +Finally, the most common learning disability is thinking that you already know. The quick quizzes can be an extremely effective cure. + +作者认为比起被动阅读而言,带着问题去阅读,去尝试解决问题,能够对知识有更深的印象。`Quick Quizzes`,是作者在读博时学习到的很棒的学习方式。 + +`1.3`同类书籍推荐: +其中介绍了一些c++相关的优秀并行编程书籍 + +## 测试程序编译 +- 编译安装依赖 +[Userspace RCU](https://github.com/urcu/userspace-rcu) + +问题: +- lockdeq.c:70:23: error: field ‘chain’ has incomplete type +struct cds_list_head chain; diff --git a/_posts/2021-07-30-perfbook-c11.md b/_posts/2021-07-30-perfbook-c11.md new file mode 100644 index 0000000..2b5c538 --- /dev/null +++ b/_posts/2021-07-30-perfbook-c11.md @@ -0,0 +1,362 @@ +--- +title: perfbook-c11 +layout: post +categories: perfbook +tags: perfbook 并行编程 书籍 测试 +excerpt: perfbook-c11 +--- +# 11 Validation +## 11.1 Introduction +### 11.1.1 Where Do Bugs Come From? +计算机和人的思维方式的重大差异: + +1.计算机缺乏常识性的东西。几十年来,人工智能总是无功而返。 + +2.计算机通常无法理解人类意图,或者更正式的说,计算机缺少心理理论。 + +3.计算机通常不能做局部性的计划,与之相反,它需要你将所有细节和每一个可能的场景都一一列出。 + +>小问题11.1:仔细权衡一下,在什么时候,遵从局部性计划显得尤其重要? + +答案:有很多这样的情形。但是,最重要的情形可能是,当没有任何人曾经创建过与将要开发的程序类似的任何东西时。在这种情况下,创建一个可信计划的唯一方法就是实现程序、创建计划,并且再次实现它。但是无论是谁首次实现该程序,除了遵循局部性计划外都没有其他选择。因为在无知的情况下创建的任何详细计划,都不能在初次面对真实世界时得以幸存。 + +也许,这也是如下事实的原因之一,为什么那些疯狂乐观的人,也是那些喜欢遵循局部性计划的人。 + +### 11.1.2 Required Mindset +当你在进行任何验证工作时,应当记住以下规则。 + +1.没有BUG的程序,仅仅是那种微不足道的程序。 + +2.一个可靠的程序,不存在已知的BUG。 + +> 谁不折磨自己的代码,代码将会反过来折磨自己。 + +![Mw0eht.png](https://s2.ax1x.com/2019/11/16/Mw0eht.png) + + +### 11.1.3 When Should Validation Start? +如果你正在从事一个新类型的项目,从某种意义上来说,其需求也是不明确的。在这种情况下,最好的方法可能是快速地原型化一些粗略的方案,尽力将它们搞出来,然后看看哪些方案能最优的运行。 +### 11.1.4 The Open Source Way +> 只要有足够多的眼球,所有BUG都是浅显的。 +开源开发的第二个原则,密集测试。 + +同`Eric`在大教堂与集市一书中的表述。 + +> 如果你的维护者只有在你已经测试了代码时,才会接受代码,这将形成死锁情形,你的代码需要测试后才能被接受,而只有被接受后才能被测试。 + +## 11.2 Tracing +介绍追踪的工具: +- `printf`/`printk` +- `gdb`/`kgdb` +以上调试手段都存在负载太高的问题。 + +## 11.3 Assertions +> 在并行代码中,可能发生的一个特别糟糕的事情是,某个函数期望在一个特定的锁保护下运行,但是实际上并没有获得该锁。 + +## 11.4 Static Analysis +静态分析是一种验证技术,其中一个程序将第二个程序作为输入,它报告第二个程序的错误和漏洞。非常有趣的是,几乎所有的程序都通过它们的编译器和解释器进行静态分析。这些工具远远算不上完美,但是在过去几十年中,它们定位错误的能力得到了极大的改善。部分原因是它们现在拥有超过64K内存进行它们的分析工作。 + +> 早期的UNIX [lint](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.56.1841)工具是非常有用的,虽然它的很多功能都被并入C编译器了。目前仍然有类似lint的工具在开发和使用中。 + +- 介绍了lint的算法及数据结构 +- 测试方法 + +[sparse](https://lwn.net/Articles/87538/)静态分析查找Linux内核中的高级错误,包括: + +1.对指向用户态数据结构的指针进行误用。 + +2.从过长的常量接收赋值。 + +3.空的switch语句。 + +4.不匹配的锁申请、释放原语。 + +5.对每CPU原语的误用。 + +6.在非RCU指针上使用RCU原语,反之亦然。 + +虽然编译器极可能会继续提升其静态分析能力,但是sparse静态分析展示了编译器外静态分析的优势,尤其是查找应用特定BUG的优势。 + +## 11.5 Code Review +> If a man speaks of my virtues, he steals from me; if he speaks of my vices, then he is my teacher. + +### 11.5.1 Inspection +传统意义上来说,正式的代码审查采取面对面会谈的形式,会谈者有正式定义的角色:主持人、开发者及一个或者两个其他参与者。开发者通读整个代码,解释做了什么,以及它为什么这样运行。一个或者两个参与者提出疑问,并抛出问题。而主持人的任务,则是解决冲突并做记录。这个过程对于定位BUG是非常有效的,尤其是当所有参与者都熟悉手头代码时,更加有效。 +在审查时,查阅相关的提交记录、错误报告及LWN文档等相关文档是有价值的。 + +### 11.5.2 Walkthroughs +典型的走查小组包含一个主持人,一个秘书(记录找到的BUG),一个测试专家(生成测试用例集),以及一个或者两个其他的人。这也是非常有效的,但是也非常耗时。 +典型的过程: +1.测试者提供测试用例。 + +2.主持人使用特定的用例作为输入,在调试器中启动代码。 + +3.在每一行语句执行前,开发者需要预先指出语句的输出,并解释为什么这样的输出是正确的。 + +4.如果输出与开发者预先指出的不一致,这将被作为一个潜在BUG的迹象。 + +5.在并发代码走查中,一个“并发老手”将提出问题,什么样的代码会与当前代码并发运行,为什么这样的并行没有问题? + +### 11.5.3 Self-Inspection +#### `Paul`的开发工作流: + +1.写出包含需求的设计文档、数据结构图表,以及设计选择的原理。 + +2.咨询专家,如果有必要就修正设计文档。 + +3.用笔在纸张上面写下代码,一边写代码一边修正错误。抵制住对已经存在的、几乎相同的代码序列的引用,相反,你应该复制它们。 + +4.如果有错误,用笔在干净的纸张上面复制代码,一边做这些事情一边修正错误。一直重复,直到最后两份副本完全相同。 + +5.为那些不是那么显而易见的代码,给出正确性证明。 + +6.在可能的情况下,自底向上的测试代码片断。 + +7.当所有代码都集成后,进行全功能测试和压力测试。 + +8.一旦代码通过了所有测试,写下代码级的文档,也许还会对前面讨论的设计文档进行扩充。 + +> 小问题11.5:为什么谁都不喜欢在用笔在纸张上面复制现有代码?这不是增加抄写错误的可能性吗? +小问题11.6:这个过程是荒谬的过度设计!你怎么能够期望通过这种方式得到合理数量的软件? + +1.如果你正在抄写大量代码,那么你可能是没有获得抽象的好处。抄写代码的工作可以为你提供极大的抽象动机。 + +2.抄写代码的工作,给予你一个机会来思考代码在新的设置环境中是否真的能正常运行。是否有一个不明显的约束,例如需要禁止中断或者持有某些锁? + +3.抄写代码的工作,也给你一些时间来考虑是否有更好的途径来完成任务。 + +事实上,反复的手工抄写代码是费力而且缓慢的。但是,当将重载压力和正确性证明结合起来时,对于那些复杂的,并且其最终性能和可靠性是必要的,而难于调试的并行代码来说,这种方法是极其有效的。Linux内核RCU实现就是一个例子。 + +#### `Paul`的`review`工作流: +1.使用你喜欢的文档工具(LATEX、HTML、OpenOffice,或者直接用ASCII),描述问题中所述代码的高层设计。使用大量的图来表示数据结构,以及这些如何被修改的。 + +2.复制一份代码,删除掉所有注释。 + +3.用文档逐行记录代码是干什么的。 + +4.修复你所找到的BUG。 + +虽然后面的过程也是一种真正理解别人代码的好方法,但是在很多情况下,只需要第一步就足够了。 + +#### `Paul`的`no-paper`工作流: +1.通过扩展使用已有并行库函数,写出一个顺序程序。 + +2.为并行框架写出顺序执行的插件。例如地图渲染、BIONC或者WEB应用服务器。 + +3.做如下优秀的并行设计,问题被完整的分割,然后仅仅实现顺序程序,这些顺序程序并发运行而不必相互通信。 + +4.坚守在某个领域(如线性代数),这些领域中,工具可以自动对问题进行分解及并行化。 + +5.对并行原语进行极其严格的使用,这样最终代码容易被看出其正确性。但是请注意,它总是诱使你打破“小步前进”这个规则,以获得更好的性能和扩展性。破坏规则常常导致意外。也就是说,除非你小心进行了本节描述的纸面工作,否则就会有意外发生。 + +## 11.6 Probability and Heisenbugs +![MDK7D0.png](https://s2.ax1x.com/2019/11/17/MDK7D0.png) +现在的问题是,需要多少测试以确定你真的修复了BUG,而不仅仅是降低故障发生的几率,或者说仅仅修复了几个相关BUG中的某一个BUG,或者是干了一些无效的、不相关的修改。 +- 离散测试 +离散测试以良好定义的、独立的测试用例为特征。功能测试往往是离散的。 + +- 连续测试 +`rcutorture`测试的持续时间,将比启动、停止它的次数更令人关注。所以说,`rcutorture`是一个持续测试的例子,这类测试包含很多压力测试。 + +> 小问题11.7:假如在你的权限范围内,能够支配大量的系统。例如,以目前的云系统价格,你能够以合理的低价购买大量的CPU时间。为什么不使用这种方法,来为所有现实目标获得其足够的确定性? + +答案:这个方法可能是你的验证武器库中一个有价值的补充。但是它有一些限制。 +1.某些BUG有极其低的发生概率。但是仍然需要修复。例如,假设Linux内核的RCU实现有一个BUG,它在一台机器上平均一个世纪才出现一次。即使在最便宜的云平台上,一个世纪的时间也是十分昂贵的。但是我们可以预期,截止2011年,世界上超过1亿份Linux实例中,这个BUG会导致超过2000次错误。 +2.BUG可能在你的测试设置中,发生概率为0,这意味着无论你花费多少机器时间来测试它,都不会看到BUG发生。 +当然,如果你的代码足够小,形式验证可能是有用的。正如在第12章所讨论的一样。但是请注意,对于如下错误,你的前提假设、对需求的误解、对所用软件/硬件原语的误解,或者认为不需要进行证明的错误,形式验证将不能发现它们。 +### 11.6.1 Statistics for Discrete Testing +![MDYtb9.png](https://s2.ax1x.com/2019/11/17/MDYtb9.png) +将f=0.1及Fn=0.99代入公式11.6,得到结果43.7,这表示我们需要44次连续成功运行测试用例,才能有99%的把握确保对故障的修复真的生效。 +从30%的错误比例提升一个数量级,就是3%的错误率。将这些数值代入公式11.6。 +`30%`的错误率对应的测试次数是`13`次,`3%`的错误率对应的测试次数是`151.2`次。 +因此,要提升一个数量级,我们大约需要增加约一个数量级的测试。想要完全确认没有问题,这是不可能的。 + +### 11.6.2 Abusing Statistics for Discrete Testing +将离散测试统计的方法论用在持续测试统计中,也是一种误差在 `10%`以内的可用来代替泊松分布的简单办法。 + +> 假如你有一个持续测试,它大约每十个小时失败三次,并且你修复了引起故障的BUG。需要在没有故障的情况下,运行多长时间的测试,才能给你99%的把握确认自己已经减小了故障发生的几率? + +在不做过多统计的情况下,我们可以简单定义一小时的运行测试为一个离散测试,在这一个小时内,有30%的几率运行失败。从前一节的结果可以得出结论,如果这个测试运行13个小时而没有错误,就有99%的信心认为我们的修复措施已经提升了程序的可靠性。 + +`11.6.3`将给出从数学上来说更准确的解法。 +### 11.6.3 Statistics for Continuous Testing +**泊松分布:** +在测试中,单位时间内预期`λ`次失败的情况下,`m`次失败的可能性是 +![MyE84H.png](https://s2.ax1x.com/2019/11/18/MyE84H.png) +Here Fm is the probability of m failures in the test and λ is the expected failure rate per unit time. +测试成功的概率:`F0 = e ^ -λ`,为了解决`11.6.2`中提出的问题,我们令`F0 = 0.01`,得`λ = 4.6`,回到`λ`的定义:单位时间内预期失败的次数。 +为了完成这个单位时间对应的失败次数`4.6`,我们实际需要的测试小时数为`λ / 0.3 = 4.6 / 0.3 = 14.3`。 + +一般而言,如果我们在单位时间内,有`n`次失败,并且我们希望有`P%`的信心确信,某次修复减小了失败几率,我们可以使用公式 +![MDdCSx.png](https://s2.ax1x.com/2019/11/17/MDdCSx.png) + +如果一个给定的测试每小时失败一次,但是经过一次BUG修复以后,24小时测试运行仅仅错误两次。假设导致错误的故障是随机发生的,那么第二次运行是由于随机错误引起的,其可能是多大? + +泊松累积分布函数解决这个问题: +![MDDaaF.png](https://s2.ax1x.com/2019/11/17/MDDaaF.png) +这里,m是在长时间测试运行过程中的错误次数(在本例中,是2),λ是长时间运行过程中,预期会失败的次数(在本例中,是24)。将m=2,λ=24代入表达式,得到两次或者更少次数的可能性是1.2×10^-8。 + +### 11.6.4 Hunting Heisenbugs +“海森堡BUG”这个名字,来源于量子力学的海森堡不确定性原理,该原理指出,在任何特定时间点,不可能同时精确计量某个粒子的位置和速度[Hei27]。任何试图更精确计量粒子位置的手段,都将会增加速度的不确定性。 +为什么不构造反-爱森堡 BUG 的东西来消灭爱森堡BUG呢? +针对海森堡BUG构造反-海森堡BUG,这更像是一项艺术,而不是科学。 +本节描述一些手段来实现这一点: + +#### 为竞争区域增加延迟 + +这依赖于如何首先找到竞争条件。这有点像暗黑艺术,但是也存在一些找到它们的方法——通过有条理的添加并移除延迟(例如,二分法查找),会更进一步了解到竞争条件是如何运行的。 + +> 但是我在进行二分法查找时,最终发现有一个太大的提交。我该怎么办呢? + +这就是你的答案:将提交分拆成大小适合的块,并对块进行二分查找。根据我的经验,将提交进行拆分的动作,足以使得BUG变得明显。 + +#### 增加负载强度 + +1.增加更多的CPU。 + +2.如果程序用到了网络,增加更多网络适配卡,以及更多、更快的远程系统。 + +3.如果在问题出现的时候,程序正在做重载I/O,要么是:(1)增加更多存储设备,(2)使用更快的存储设备,例如将磁盘换为SSD,或者是(3)为大容量存储而使用基于RAM的文件系统,以替代主存储设备。 + +4.改变其尺寸,例如,如果在进行并行矩阵运算,改变矩阵的大小。更大的问题域可能引入更多复杂性,而更小的问题域通常会增加竞争水平。如果你不确信是否应该加大尺寸,还是减小尺寸,就两者都尝试一下。 + +#### 独立的测试可疑子系统 + +实际上,当创建并行程序时,明智的做法是分别对组件进行压力测试。创建这样的组件级别的压力测试,看起来是浪费时间,但是少量的组件级测试可以节省大量系统级的调试。 + +#### 模拟不常见的事件 + +例如,不直接调用malloc(),而是调用一个封装函数,该函数使用一个随机数,以确定是否无条件返回NULL,或者真正调用malloc()并返回结果指针。包含伪失败事件是一种在顺序程序及并行程序中实现鲁棒性的好方法。 + +#### 对`near-miss`事件进行计数 +通常的方法是,将对不频繁发生的错误计数替换为对更常见的`near-miss`事件进行计数,可以确信,这些`near-miss`事件与错误相关。 +例如:如果有两个操作,应当使用同一把锁,进行不同的获取操作以保护两个操作,在这两个操作之间的延迟过短。这是第一个`near-miss`事件 + +## 11.7 Performance Estimation + +### 11.7.1 Benchmarking +模拟实际应用场景,来测试不同实现的应用对应的性能结果。 +其目标如下: +> 1.对不同的竞争性实现进行比较,提供公正的框架。 +2.将竞争力聚集于实现用户关心的部分。 +3.将基准实现作为示范使用。 +4.作为营销手段,突出软件与竞争对手相比的强项。 + +### 11.7.2 Profiling +一个老套但是十分有效的跟踪性能及扩展性BUG的方法,是在调试器下面运行程序,然后定期中断它,并记录下每次中断时所有线程的堆栈。其原理是,如果某些地方使程序变慢,它就必然在线程执行过程中可见。 + +### 11.7.4 Microbenchmarking +微基准的一个常用方法是测量其时间,在测试状态下运行一定次数的代码迭代,然后再次测量其时间。两次时间之间的差异除以迭代次数。同`glibc`中的测试方式。 + +不幸的是,这种测量方法会有任意数量的误差,包括: + +1.测量过程包含一些测量时间的开销。这个误差源可以通过增加迭代次数来将其减小到任意小的值。 + +2.最初几次测试迭代可能有缓存缺失或者(更为糟糕)缺页错误,这将影响测量值。这个误差源也可以通过增加迭代次数来减小。通常也可以在开始测量前,预先运行几次迭代来将误差源完全消除。 + +3.某些类型的干扰,例如随机内存错误,这是罕见的,并且可以通过运行一定数量的测试集合来处理。如果干扰级别有显著的统计特征,则明显不合理的性能数据可以被剔除。 + +4.任意某次测试迭代可能被系统中的其他活动所干扰。干扰可能包括其他应用、系统实用程序及守护程序、设备中断、固件中断(包括系统管理中断、SMI)、虚拟化、内存错误,以及其他一些活动。如果这些干扰是随机发生的,其影响可能通过减少迭代次数来将其最小化。 + +> 小问题11.18:其他误差源呢?例如,由于缓存及内存布局之间的相互影响? + +内存布局确实会导致执行时间的减少。例如,如果一个特定的应用几乎总是超过L0缓存关联大小,但是通过正确的内存布局,它完全与缓存匹配。如果这确实是一个要顾虑的问题,可以考虑使用巨页(在内核或者裸机上)来运行你的微基准,以完全的控制你的内存布局。 + +### 11.7.5 Isolation +#### CPU调度的干扰 +将一组CPU与外界干扰进行隔离的方法: +- taskset +- POSIX sched_setaffinity()系统调用 +以上方法对每CPU内核线程没有什么作用。 + +对每CPU内核线程,则需要以高优先级实时任务的方式来运行你的测试。例如,使用`POSIX sched_setscheduler()`系统调用。 +#### 中断的干扰 +通过中断绑核来屏蔽。 + +### 11.7.6 Detecting Interference +#### 11.7.6.1 Detecting Interference Via Measurement +在基于Linux的系统中,这个上下文切换将出现在`/proc//sched`的`nr_switches`字段中。 +对于一个特定线程来说,可以使用`getrusage()`系统调用以得到上下文切换次数,如下实例所示: +如果在测试过程中发生了上下文切换,就舍弃这次测试。 +```c +#include +#include +/* Return 0 if test results should be rejected. */ +int runtest(void) +{ + struct rusage ru1; + struct rusage ru2; + if (getrusage(RUSAGE_SELF, &ru1) != 0) { + perror("getrusage"); + abort(); + } + /* run test here. */ + if (getrusage(RUSAGE_SELF, &ru2 != 0) { + perror("getrusage"); + abort(); + } + return (ru1.ru_nvcsw == ru2.ru_nvcsw && + ru1.runivcsw == ru2.runivcsw); +} + +``` + +#### 11.7.6.2 Detecting Interference Via Statistics +核心:对于良好数据来说,其测量结果的误差程度是已知的。这个事实允许我们在误差范围内接受测量结果。如果干扰的影响大于误差,表示它是受影响的数据,可以简单的剔除它。 + +作者写了个脚本来实现上述的功能: +```shell +divisor=3 +relerr=0.01 +trendbreak=10 +while test $# -gt 0 +do + case "$1" in + --divisor) + shift + divisor=$1 + ;; + --relerr) + shift + relerr=$1 + ;; + --trendbreak) + shift + trendbreak=$1 + ;; + esac + shift +done + +awk -v divisor=$divisor -v relerr=$relerr \ + -v trendbreak=$trendbreak ’{ + for (i = 2; i <= NF; i++) + d[i - 1] = $i; + asort(d); + i = int((NF + divisor - 1) / divisor); + delta = d[i] - d[1]; + maxdelta = delta * divisor; + maxdelta1 = delta + d[i] * relerr; + if (maxdelta1 > maxdelta) + maxdelta = maxdelta1; + for (j = i + 1; j < NF; j++) { + if (j <= 2) + maxdiff = d[NF - 1] - d[1]; + else + maxdiff = trendbreak * \ + (d[j - 1] - d[1]) / (j - 2); + if (d[j] - d[1] > maxdelta && \ + d[j] - d[j - 1] > maxdiff) + break; + } + n = sum = 0; + for (k = 1; k < j; k++) { + sum += d[k]; + n++; + } + min = d[1]; + max = d[j - 1]; + avg = sum / n; + print $1, avg, min, max, n, NF - 1; +}’ +``` diff --git a/_posts/2021-07-30-perfbook-c12.md b/_posts/2021-07-30-perfbook-c12.md new file mode 100644 index 0000000..c3fa064 --- /dev/null +++ b/_posts/2021-07-30-perfbook-c12.md @@ -0,0 +1,387 @@ +--- +title: perfbook-c12 +layout: post +categories: perfbook +tags: perfbook 并行编程 书籍 形式化验证 +excerpt: perfbook-c12 +--- +# 12 Formal Verification +正确性检查是有意义的,但是与原始算法一样,易于出现人工错误。而且,不能期望正确性证明找到你的需求前提、需求缺陷,对底层软件、硬件原语的误解,或者没有考虑到进行证明的地方所引起的错误。这意味着形式验证的方法不能代替测试。 +拥有一个工具来找到所有竞争条件是非常有用的。一些这样的工具已经存在: +- 对通用目的的状态空间搜索工具`Promela`语言及它的编译器`Spin` +- 特殊目的的`ppcmem`及`cppmem`工具 +- `SAT`求解器 + +## 12.1 State-Space Search +### 12.1.1 Promela and Spin +**基本原理:** +用类似于C语言的Promela来重新编写算法,并进行正确性验证。然后使用Spin来将其转化为可以编译运行的C程序。最终的程序执行对你的算法进行一个全状态搜索,它要么确认通过验证,要么发现包含在Promela程序中的断言的反例。 +**适用范围:** +在算法复杂但是小的并行程序。 +可以考虑使用此工具来构造测试用例。 +**缺点**: +Promela并不理解内存模型及任何类型的乱序语义。 + +> [Spin](http://spinroot.com/spin/whatispin.html) +[相关参考书籍](http://spinroot.com/spin/books.html) + +#### 12.1.1.1 Promela Warm-Up: Non-Atomic Increment +测试代码`atomicincrement.spin`: +```c +#define NUMPROCS 2 + +byte counter = 0; +byte progress[NUMPROCS]; + +proctype incrementer(byte me) +{ + // me是进程编号,由后面的初始化代码进行设置 + int temp; + + atomic { + temp = counter; + counter = temp + 1; + } + progress[me] = 1; +} + +init { + int i = 0; + int sum = 0; + + atomic { + i = 0; + do + :: i < NUMPROCS -> + progress[i] = 0; + run incrementer(i); + i++ + :: i >= NUMPROCS -> break + od; + } + atomic { + i = 0; + sum = 0; + do + :: i < NUMPROCS -> + sum = sum + progress[i]; + i++ + :: i >= NUMPROCS -> break + od; + assert(sum < NUMPROCS || counter == NUMPROCS) + } +} + +``` + +测试输出结果: +``` +pan: assertion violated ((sum<2)||(counter==2)) (at depth 20) +pan: wrote increment.spin.trail +(Spin Version 4.2.5 -- 2 April 2005) +Warning: Search not completed ++ Partial Order Reduction +Full statespace search for: +never claim - (none specified) +assertion violations + +cycle checks - (disabled by -DSAFETY) +invalid end states + +State-vector 40 byte, depth reached 22, errors: 1 +45 states, stored +13 states, matched +58 transitions (= stored+matched) +51 atomic steps +hash conflicts: 0 (resolved) +2.622 memory usage (Mbyte) +``` + +对输出的`trail`文件进行分析: +``` +Starting :init: with pid 0 +1: proc 0 (:init:) line 20 "increment.spin" (state 1) [i = 0] +2: proc 0 (:init:) line 22 "increment.spin" (state 2) [((i<2))] +2: proc 0 (:init:) line 23 "increment.spin" (state 3) [progress[i] = 0] +Starting incrementer with pid 1 +3: proc 0 (:init:) line 24 "increment.spin" (state 4) [(run incrementer(i))] +3: proc 0 (:init:) line 25 "increment.spin" (state 5) [i = (i+1)] +4: proc 0 (:init:) line 22 "increment.spin" (state 2) [((i<2))] +4: proc 0 (:init:) line 23 "increment.spin" (state 3) [progress[i] = 0] +Starting incrementer with pid 2 +5: proc 0 (:init:) line 24 "increment.spin" (state 4) [(run incrementer(i))] +5: proc 0 (:init:) line 25 "increment.spin" (state 5) [i = (i+1)] +6: proc 0 (:init:) line 26 "increment.spin" (state 6) [((i>=2))] +7: proc 0 (:init:) line 21 "increment.spin" (state 10) [break] +8: proc 2 (incrementer) line 10 "increment.spin" (state 1) [temp = counter] +9: proc 1 (incrementer) line 10 "increment.spin" (state 1) [temp = counter] +10: proc 2 (incrementer) line 11 "increment.spin" (state 2) [counter = (temp+1)] +11: proc 2 (incrementer) line 12 "increment.spin" (state 3) [progress[me] = 1] +12: proc 2 terminates +13: proc 1 (incrementer) line 11 "increment.spin" (state 2) [counter = (temp+1)] +14: proc 1 (incrementer) line 12 "increment.spin" (state 3) [progress[me] = 1] +15: proc 1 terminates +16: proc 0 (:init:) line 30 "increment.spin" (state 12) [i = 0] +16: proc 0 (:init:) line 31 "increment.spin" (state 13) [sum = 0] +17: proc 0 (:init:) line 33 "increment.spin" (state 14) [((i<2))] +17: proc 0 (:init:) line 34 "increment.spin" (state 15) [sum = (sum+progress[i])] +17: proc 0 (:init:) line 35 "increment.spin" (state 16) [i = (i+1)] +18: proc 0 (:init:) line 33 "increment.spin" (state 14) [((i<2))] +18: proc 0 (:init:) line 34 "increment.spin" (state 15) [sum = (sum+progress[i])] +18: proc 0 (:init:) line 35 "increment.spin" (state 16) [i = (i+1)] +19: proc 0 (:init:) line 36 "increment.spin" (state 17) [((i>=2))] +20: proc 0 (:init:) line 32 "increment.spin" (state 21) [break] +spin: line 38 "increment.spin", Error: assertion violated +spin: text of failed assertion: assert(((sum<2)||(counter==2))) +21: proc 0 (:init:) line 38 "increment.spin" (state 22) [assert(((sum<2)||(counter==2)))] +spin: trail ends after 21 steps +#processes: 1 +counter = 1 +progress[0] = 1 +progress[1] = 1 +21: proc 0 (:init:) line 40 "increment.spin" (state 24) +3 processes created +``` +可见,初始化块的第一部分创建两个递增的进程,其中第一个进程获得计数器,然后两个进程都递增并保存它,这丢失了一个计数。随后断言被触发,然后最终状态被显示出来。 + +#### 12.1.1.2 Promela Warm-Up: Atomic Increment +将`12.1.1.1`中的递增更换成原子操作可修复该`bug`,随着线程数量的增大, 该工具占用内存会变多,如下所示: +![MyQrIP.png](https://s2.ax1x.com/2019/11/18/MyQrIP.png) +这是`Promela`的局限所在,但在服务器端不存在这个问题。 + + +### 12.1.2 How to Use Promela +``` +spin -a qrcu.spin +``` +给定一个源文件`qrcu.spin`,创建对应的c文件`pan.c` +``` +cc -DSAFETY -o pan pan.c +``` +`pan.c`编译生成的状态机搜索程序。`-DSAFETY`可提高程序运行的速度 +``` +./pan +``` +运行状态机程序`pan`。 +``` +spin -t -p qrcu.spin +``` +生成某次运行产生的跟踪文件,该运行遇到某些错误,并输出产生错误的步骤。 + +#### 12.1.2.1 Promela Peculiarities +注意`Promela`语言和`C`语言的差异,作者在此提供了一些`Promela`编程的建议,值得参考。 + +#### 12.1.2.2 Promela Coding Tricks +- **构造内部乱序用例**: +```c +if +:: 1 -> r1 = x; + r2 = y +:: 1 -> r2 = y; + r1 = x +fi +``` + +- **状态压缩**: +如果有复杂的断言,在atomic块内赋值。 + +- Promela不提供函数。必须使用C预处理宏来代替。 + + +### 12.1.3 Promela Example: Locking +使用`Promela`的语言写的自旋锁如下`lock.h`所示: +```c +#define spin_lock(mutex) \ + do \ + :: 1 -> atomic {\ + if \ + :: mutex == 0 -> \ + mutex = 1; \ + break \ + :: else -> skip \ + fi \ + } \ + od + +#define spin_unlock(mutex) \ + mutex = 0 +``` +不需要内存屏障,因为Promela假设是强序的。 +正如之前提示及随后的例子将所看到的那样,弱序模型必须显式地编码。 + +测试该锁: +```c +#include "lock.h" + +#define N_LOCKERS 3 + +bit mutex = 0; +bit havelock[N_LOCKERS]; +int sum; + +proctype locker(byte me) +{ + do + :: 1 -> + spin_lock(mutex); + havelock[me] = 1; + havelock[me] = 0; + spin_unlock(mutex) + od +} + +init { + int i = 0; + int j; + +end: do + :: i < N_LOCKERS -> + havelock[i] = 0; + run locker(i); + i++ + :: i >= N_LOCKERS -> + sum = 0; + j = 0; + atomic { + do + :: j < N_LOCKERS -> + sum = sum + havelock[j]; + j = j + 1 + :: j >= N_LOCKERS -> + break + od + } + assert(sum <= 1); + break + od +} +``` + +### 12.1.4 Promela Example: QRCU +`QRCU`的作用: +QRCU是SRCU的变体,如果没有读者,优雅周期将在1μs内检测出来。与之相对的是,绝大部分其他RCU实现则有几毫秒的延迟。 +使用`Promela`实现`QRCU`算法并验证。 + +### 12.1.5 Promela Parable: dynticks and Preemptible RCU +在最近的Linux内核中,可抢占RCU可能使最近的Linux内核失去了一个有价值的节能功能。 +`Steve`编写`rcu_irq_enter()`和`rcu_irq_exit()`接口,`Paul`从2007年10月到2008年2月反复审查代码,每次几乎都能够找到至少一个BUG。 +于是他决定使用`Promela`和 `Spin`的帮助一次性把`bug`都找出来。 +下面描述该模块的各个接口。 +12.1.5.1 Introduction to Preemptible RCU and dynticks +12.1.5.2 Task Interface +12.1.5.3 Interrupt Interface +12.1.5.4 Grace-Period Interface + +### 12.1.6 Validating Preemptible RCU and dynticks +本节一步一步地开发一个用于dynticks和RCU之间接口的Promela模型,每一节都举例说明每一个步骤,将进程级的dynticks 进入、退出代码及优雅周期处理代码翻译为Promela。 + +12.1.6.1 Basic Model +12.1.6.2 Validating Safety +12.1.6.3 Validating Liveness +12.1.6.4 Interrupts +在`Promela`中模拟中断的方式: +12.1.6.5 Validating Interrupt Handlers +12.1.6.6 Validating Nested Interrupt Handlers +12.1.6.7 Validating NMI Handlers +12.1.6.8 Lessons (Re)Learned +从下一节开始将针对总结发现的内容,重新设计数据结构和接口。 + +------------------------------------------------------------ +12.1.6.9 Simplicity Avoids Formal Verification +12.1.6.10 State Variables for Simplified Dynticks Interface +12.1.6.11 Entering and Leaving Dynticks-Idle Mode +12.1.6.12 NMIs From Dynticks-Idle Mode +12.1.6.13 Interrupts From Dynticks-Idle Mode +12.1.6.14 Checking For Dynticks Quiescent States +12.1.6.15 Discussion + +## 12.2 Special-Purpose State-Space Search +此部分的内容更详细的内容见作者在[LWN](https://lwn.net/Articles/470681/)中发布的文章。 +PPCMEM工具,它由剑桥大学的Peter Sewell和Susmit Sarkar,INRIA 的Luc Maranget、Francesco Zappa Nardelli和Pankaj Pawan,牛津大学的Jade Alglave与IBM的Derek Williams合作完成。 +这个组将Power、ARM、ARM及C/C++11标准的内存模型进行了形式化,并基于Power和ARM形式设计了PPCMEM工具([ppcmem在线测试工具](https://www.cl.cam.ac.uk/~pes20/ppcmem/))。 + +> 小问题12.22:但是x86是强序的。为什么你需要将它的内存模式形式化? + +实际上,学术界认为x86内存模型是弱序的,因为它允许将存储和随后的加载进行重排。从学术的观点来看,一个强序内存模型绝不允许任何重排,这样所有的线程都将对它看到的所有操作顺序达成完全一致。 + + +### 12.2.1 Anatomy of a Litmus Test +这里举了个`powerpc`下的汇编例子。 +``` +PPC SB+lwsync-RMW-lwsync+isync-simple +"" +{ +0:r2=x; 0:r3=2; 0:r4=y; 0:r10=0 ; 0:r11=0; 0:r12=z ; +1:r2=y; 1:r4=x; +} + P0 | P1 ; + li r1,1 | li r1,1 ; + stw r1,0(r2) | stw r1,0(r2) ; + lwsync | sync ; + | lwz r3,0(r4) ; + lwarx r11,r10,r12 | ; + stwcx. r11,r10,r12 | ; + bne Fail1 | ; + isync | ; + lwz r3,0(r4) | ; + Fail1: | ; + +exists +(0:r3=0 /\ 1:r3=0) + +``` +### 12.2.2 What Does This Litmus Test Mean? +其对应的c语言如下所述: +```c +// 初始化r3 +int r3 = 2; +void P0(void) +{ + int r3; + x = 1; /* Lines 8 and 9 */ + /* int atomic_add_return (int i, atomic_t * v); + * 注意到以下的用法和内核中的对应接口不符。 + */ + atomic_add_return(&z, 0); /* Lines 10-15 */ + r3 = y; /* Line 16 */ +} +void P1(void) +{ + int r3; + + y = 1; /* Lines 8-9 */ + smp_mb(); /* Line 10 */ + r3 = x; /* Line 11 */ +} +``` +### 12.2.3 Running a Test +调试界面如下图: +![M6QPm9.png](https://s2.ax1x.com/2019/11/18/M6QPm9.png) + +可以单步调试各个线程,也可以点击`Auto`跑全状态空间。 +也可以直接搭建环境在本地服务器跑全状态空间测试,源码在[此处](https://www.cl.cam.ac.uk/~pes20/ppcmem/help.html)下载。 +### 12.2.4 PPCMEM Discussion +仍然存在以下局限性: +- 这些工具不构成IBM或者ARM在其CPU架构上的官方声明。 +- 这些工具仅仅处理字长度的访问(32位),并且要访问的字必须正确对齐。而且,工具不处理ARM内存屏障的某些更弱的变种,也不处理算法。 +- 这些工具对于复杂数据结构来说,做得不是太好 + +## 12.3 Axiomatic Approaches +使用PPCMEM做`IRIW`测试,大大提高了调试的效率。 +![Mc80OO.png](https://s2.ax1x.com/2019/11/18/Mc80OO.png) + +但是仍然使用了`14`个小时。 +其中,许多跟踪事件彼此之间是类似的。这表明将类似跟踪事件作为一个事件进行对待,这种方法可能提高性能。一种这样的方法是Alglave等人的公理证明方法[ATM14],它创建一组表示内存模型的公理集,然后将Litmus测试转换为可以证明或者反驳这些公理的定理。得到的工具称为“herd”,方便的采用PPCMEM相同的Litmus测试作为输入,包括图12.28所示的IRIW Litmus测试。在PPCMEM需要14小时 CPU时间来处理IRIW的情况下,herd仅仅需要17ms。 +12.3.1 Axiomatic Approaches and Locking +12.3.2 Axiomatic Approaches and RCU +## 12.4 SAT Solvers +任何具有有限循环和递归的有限程序,都可以被转换为逻辑表达式,这可以将程序的断言以其输入来表示。对于这样的逻辑表达式,非常有趣的是,知道任何可能的输入组合是否能够导致某个断言被触发。如果输入被表达为逻辑变量的组合,这就是SAT,也称为可满足性问题。 +业界实例有[cbmc](https://www.cprover.org/cbmc/)。 +如下图所示: +![McGtHg.png](https://s2.ax1x.com/2019/11/18/McGtHg.png) + +相比这下,需要对特殊用途的语言进行繁琐转换的传统工具(如[Spin](http://spinroot.com/spin/whatispin.html)),仅限于设计时验证。 + +12.5 Stateless Model Checkers +## 12.6 Summary +- 不管几十年来对形式验证的关注情况怎样,测试仍然属于大型并行软件的验证工作。 +- 绝大多数形式验证技术仅仅能用于非常小的代码片断。 +- 大量的相关BUG,也就是在生产中可能碰到的BUG,是通过测试找到的。 diff --git a/_posts/2021-07-30-perfbook-c14.md b/_posts/2021-07-30-perfbook-c14.md new file mode 100644 index 0000000..3e67f38 --- /dev/null +++ b/_posts/2021-07-30-perfbook-c14.md @@ -0,0 +1,792 @@ +--- +title: perfbook-14 +layout: post +categories: perfbook +tags: perfbook 并行编程 书籍 并发控制 +excerpt: perfbook-c14 +--- +# 14 Advanced Synchronization +## 14.1 Avoiding Locks +第五章中一些无锁实现的算法: +- count_stat +- count_stat_eventual + +最好是将无锁技术隐藏在良好定义的API之中,如inc_count(),memblock_alloc(),rcu_read_lock(),等等。其原因是,随意使用无锁技术,它是创建大BUG的良好途径。 +许多无锁技术的关键部分是内存屏障,这将在随后的章节中介绍。 +## 14.2 Memory Barriers + +### 14.2.1 Memory Ordering and Memory Barriers +编译器和同步原语(如锁和RCU)有责任通过对内存屏障(例如,在Linux内核中的smp_mb())的使用来维护这种用户在执行顺序方面的直觉。 +![MgSMod.png](https://s2.ax1x.com/2019/11/19/MgSMod.png) + +### 14.2.2 If B Follows A, and C Follows B, Why Doesn’t C Follow A? +一个典型的用例: +```c +#define barrier() __asm__ __volatile__("": : :"memory") +thread0(void) +{ + A = 1; + smp_wmb(); + B = 1; +} +thread1(void) +{ + while (B != 1) + continue; + barrier(); + C = 1; +} + +thread2(void) +{ + while (C != 1) + continue; + barrier(); + assert(A != 0); +} +``` + +如果在真实世界的弱序硬件(一个1.5GHz 16-CPU POWER 5系统)上运行这个代码超过10M次的话,将导致断言被触发16次。**显然,任何想编写内存屏障代码的人,需要做一些极限测试。** +当然,在`x86`中不会触发断言, + +修改方法: +```c +#define barrier() __asm__ __volatile__("": : :"memory") +thread0(void) +{ + A = 1; + smp_wmb(); + B = 1; +} +thread1(void) +{ + while (B != 1) + continue; + smp_mb(); + C = 1; +} + +thread2(void) +{ + while (C != 1) + continue; + smp_mb(); + assert(A != 0); +} +``` + +### 14.2.3 Variables Can Have More Than One Value +```c +state.variable = mycpu; +lasttb = oldtb = firsttb = gettb(); +while (state.variable == mycpu) { + /* 记录变量将时间计数器赋予本CPU的时间长度 */ + lasttb = oldtb; + oldtb = gettb(); + if (lasttb - firsttb > 1000) + break; +} +``` +> 小问题14.3:是什么假定条件使得上面的代码片断在实际的硬件中不再正确? + +答案:代码假设,某个指定的CPU只要看不到自己的值,它将立即看到最后的新值。在实际的硬件中,某些CPU可能在收敛到最终的值以前,会看到几个中间的结果。 +关键在于每个CPU看到的`state.variable`是不一样的。通过以上代码可以统计在每个CPU中看到的`state.variable`的值及其对应的时间。 + +![Mgux4P.png](https://s2.ax1x.com/2019/11/19/Mgux4P.png) + +每一个水平条表示一个特定CPU观察到变量的时间,左边的黑色区域表示相应的CPU第一次计数的时间。 + +### 14.2.4 What Can You Trust? +**最基本的内存屏障语义:所有 CPU架构都遵从如下规则** +1. All accesses by a given CPU will appear to that CPU to have occurred in program order. +一个特定CPU的所有访问操作都与该CPU上的编程顺序一致。 + +2. All CPUs’ accesses to a single variable will be consistent with some global ordering of stores to that variable. +所有CPU对单个变量的访问都与存储这个变量的全局顺序多少有一些一致性。 + +3. Memory barriers will operate in a pair-wise fashion. +内存屏障以成对的形式进行操作。 + +4. Operations will be provided from which exclusive locking primitives may be constructed. +内布屏障操作能够由互斥锁原语所构建的语义提供。 + +#### 14.2.4.1 Self-References Are Ordered +一个特定CPU将以“编程顺序”那样看到它对内存的访问操作。 + +#### 14.2.4.2 Single-Variable Memory Consistency +对于单个变量来说,终将会对其值序列达成一致。如图`14.5`所示。 +与之相对的,如果有多个变量,就目前所见的商业计算机系统来说,则需要内存屏障,以使CPU达成顺序一致。 + +#### 14.2.4.3 Pair-Wise Memory Barriers +``` +CPU 1 CPU 2 +access(A); access(B); +smp_mb(); smp_mb(); +access(B); access(A); +``` +基本内存模型如上所示,其中`A`和`B`的初值为`0`,`access`可能是`load`或`store`,因此有`16`种可能,如下表所示: +![Mg5VXt.png](https://s2.ax1x.com/2019/11/19/Mg5VXt.png) + +> ears => 多次加载 +mouth => 多次存储 + + +#### 14.2.4.4 Pair-Wise Memory Barriers: Portable Combinations +- **pairing 1** +``` +CPU 1 CPU 2 +A=1; Y=B; +smp_mb(); smp_mb(); +B=1; X=A; +``` + +在CPU1中执行一对被内存屏障分隔开的`store`指令,在CPU2中执行一对被内存屏障分隔开的`load`指令,在该语境下,有`if (Y == 1) assert(X == 1)`。 + +- **pairing 2** + +``` +CPU 1 CPU 2 +X=A; Y=B; +smp_mb(); smp_mb(); +B=1; A=1; +``` +对应结果如下所示: +```c +if (X == 1) assert(Y == 0) +if (Y == 1) assert(X == 0) +``` + +- **pairing 3** + +``` +CPU 1 CPU 2 +X=A; B=2; +smp_mb(); smp_mb(); +B=1; A=1; + +``` +对应结果如下所示: +```c +if (X == 1) assert(B == 1); // CPU1对B的存储将重置CPU2对B的存储 +if (X == 0) // B可能为1,也可能为2。 +``` + +#### 14.2.4.5 Pair-Wise Memory Barriers: Semi-Portable Combinations +> 21stcentury hardware would guarantee that at least one of the loads saw the value stored by the corresponding store (or some later value for that same variable). + +至少有一个加载将看到相应存储的值。 + +- **Ears to Mouths**: +``` +CPU 0 CPU 1 +A=1; B=1; +smp_mb(); smp_mb(); +r1=B; r2=A; +``` + +对应结果如下所示: +```c +if (r1 == 0) assert(r2 == 1); +if (r2 == 0) assert(r1 == 1); +``` +至少r1和r2中的某一个必然不是0,这意味着至少有一个加载将看到相应存储的值。 + +- **Stores “Pass in the Night”** +``` +CPU 1 CPU 2 +A=1; B=2; +smp_mb(); smp_mb(); +B=1; A=2; +``` +假设`CPU1`持有`B`的cacheline,`CPU2`持有`A`的cacheline,可能会出现`B==1 && A==2`同时出现的情况。 +详见附录C中的表述。 + + + +#### 14.2.4.6 Pair-Wise Memory Barriers: Dubious Combinations +![Mg5VXt.png](https://s2.ax1x.com/2019/11/19/Mg5VXt.png) + +- **Ears to Ears** +如果我们知道CPU 2对B的加载,返回一个更新的值,该值比CPU 1对B的加载值更新,那么我们将知道,CPU 2对A的加载,其返回值要么与CPU 1对A的加载值相同,要么是其更新的值。 + +- **Mouth to Mouth, Ear to Ear** + +> Because it is not possible for one load to see the results of the other, it is not possible to detect the conditional ordering provided by the memory barrier. +If additional load from B is executed after both CPUs 1 and 2 complete, and if it turns out that CPU 2’s store to B happened last, then we know that CPU 2’s load from A returned either the same value as CPU 1’s load from A or some later value. + +- **Only One Store** + +在这种情况中,没有办法检测到由内存屏障提供的某种条件的顺序。 + +对于表14.1中的组合1,CPU 1对A的加载返回CPU 2对A的存储的值。那么我们知道CPU 1对B的加载,要么返回CPU 2对A的加载的值,要么返回其更新的值。 + +对于组合2,如果CPU 1对B的`load`看到CPU 2对B的存储之前的值,那么我们知道CPU 2对A的`load`将返回CPU 1对A的`load`相同的值,或者随后的值。 + +对于组合4,如果CPU 2对于B的`load`看到CPU 1对B的存储,那么我们知道CPU 2对A的`load`将返回CPU 1对A的`load`相同的值,或者随后的值。 + +对于组合8,如果CPU 2对于A的`load`看到CPU 1对于A的存储,那么我们知道CPU 1对于B的加载将返回CPU 2对A的加载同样的值,或者随后的值。 + + +#### 14.2.4.7 Semantics Sufficient to Implement Locking +锁的语义: + +1.每一个CPU和硬件线程按编程顺序看到它自己的`store`和`load`操作。 + +```c +a = 1; +b = 1 + a; +assert(b == 2); +``` +在这种语义下,上述代码的断言不会被触发。 + +2.申请、释放锁操作,必须看起来是以单一全局顺序被执行。 + +```c +spin_lock(&mylock); +if (p == NULL) + p = kmalloc(sizeof(*p), GFP_KERNEL); +spin_unlock(&mylock); +``` +如果该语义不成立,上述代码可能造成内存泄露。 + +3.如果一个特定变量在正在执行的临界区中,还没有被存储,那么随后在临界区中执行的加载操作,必须看到在最后一次临界区中,对它的存储。 +也就是说在临界区实际包含了通用内存屏障的语义。 + +### 14.2.5 Review of Locking Implementations +```c +void spin_lock(spinlock_t *lck) +{ + /* 如果当前的old_value==1,说明锁被其他线程占用 + * 那么循环读取锁的状态,直到其空闲。 + */ + while (atomic_xchg(&lck->a, 1) != 0) + while (atomic_read(&lck->a) != 0) + continue; +} +void spin_unlock(spinlock_t lck) +{ + smp_mb(); + atomic_set(&lck->a, 0); +} +``` + +![M2RO0O.png](https://s1.ax1x.com/2019/11/19/M2RO0O.png) + +以上逻辑的执行时序示例见上图所示。 + +下面继续解释`14.2.4.7`中锁的语义3: + +这种情况下,成对的内存屏障足以恰当的维护两个临界区。CPU 2的`atomic_xchg(&lck->a, 1)`已经看到CPU 1 `lck->a=0`,因此CPU 2临界区的任何地方都必然看到CPU 1在之前临界区中所做的任何事情。 + +### 14.2.6 A Few Simple Rules +对内存屏障规则的一些简单的概括: + +1.每一个 CPU按顺序看到它自己的内存访问。 + +2.如果一个单一共享变量被不同CPU加载、保存,那么被一个特定CPU看到的值的序列将与其他CPU看到的序列是一致的。并且,至少存在一个这样的序列:它包含了向这个变量存储的所有值,而每个CPU序列将与这个序列一致。 + +> 我的小问题:对这一点不是太理解, 例子见`14.5`。 + +以下`3`个概括的更多内容参照内存屏障成对使用的`16`种组合。 + +3.如果一个CPU按顺序存储变量A和B并且如果第二个CPU按顺序`load`B和A,那么,如果第二个CPU对B的`load`得到了第一个CPU对它存储的值,那么第二个CPU对A的`load`也必然得到第一个CPU对它存储的值。 + +4.如果一个CPU在对B进行存储前,对A执行一个加载操作,并且,如果第二个CPU在对A进行存储前,执行一个对B的`load`,并且,如果第二个CPU对B的加载操作得到了第一个CPU对它的存储结果,那么第一个CPU对A的加载操作必然不会得到第二个CPU对它的存储。 + +5.如果一个CPU在对B进行存储前,进行一个对A的加载操作,并且,如果第二个CPU在对A进行存储前,对B进行存储,并且,如果第一个CPU对A的加载得到第二个CPU对它的存储结果,那么第一个CPU对B的存储必然发生在第二个CPU对B的存储之后,因此被第一个CPU保存的值被保留下来。 + +### 14.2.7 Abstract Memory Access Model +![M25F2j.png](https://s1.ax1x.com/2019/11/19/M25F2j.png) + +针对这个抽象模型,举两个例子不带屏障的例子,阐述输出结果的可能性 + +### 14.2.8 Device Operations +在设备操作中,假设一个有一些内部寄存器的以太网卡,它通过一个地址端口寄存器(A)和一个数据端口寄存器(D)访问。要读取内部寄存器5,可能会使用以下代码。 +```c +*A = 5; +x = *D; +``` +可能会按照以下两种顺序之一来执行。 +```c +STORE *A = 5, x = LOAD *D +x = LOAD *D, STORE *A = 5 +``` + +第二种情况几乎可以确定会出现故障,因为它在试图读取寄存器后才设置地址。 + +### 14.2.9 Guarantees +#### 可以保证的行为 +1. 在一个给定的CPU上,有依赖的内存访问将按序运行 +2. 在特定CPU中,交叉的加载存储操作将在CPU内按顺序运行 +3. 对单个变量的存储序列将向所有CPU呈现出一个单一的序列,虽然这个序列可能不能被代码所看见,实际上,在多次运行时,其顺序可能发生变化。 + +#### 不能保证的行为 +1. It must not be assumed that independent loads and stores will be issued in the order given. +2. It must be assumed that overlapping memory accesses may be merged or discarded. + +### 14.2.10 What Are Memory Barriers? + +#### 14.2.10.1 Explicit Memory Barriers +**1. Write (or store) memory barriers** + +一个写内存屏障提供这样的保证,从系统中的其他组件的角度来说,在屏障之前的写操作看起来将在屏障后的写操作之前发生 + +**2. Data dependency barriers** + +数据依赖屏障是一种弱的读屏障形式。当两个`load`操作中,第二个依赖于第一个的结果时(如,第一个`load`得到第二个`load`所指向的地址),需要一个数据依赖屏障,以确保第二个`load`的目标地址将在第一个地址`load`之后被更新。 + +数据依赖屏障仅仅对相互依赖的加载进行排序。它对任何存储都没有效果,对相互独立的加载或者交叉加载也没有效果。 + + +**3. Read (or load) memory barriers** + +读屏障是一个数据依赖屏障,并加上如下保证,对于系统中其他组件的角度来说,所有在屏障之前的加载操作将在屏障之后的加载操作之前发生。 + +读内存屏障隐含数据依赖屏障,因此可以替代它。 + +**4. General memory barriers** + +通用内存屏障保证,对于系统中其他组件的角度来说,屏障之前的加载、存储操作都将在屏障之后的加载、存储操作之前发生。 + +#### 14.2.10.2 Implicit Memory Barriers + +**LOCK operations** +一个LOCK操作充当了一个单方面屏障的角色。 + +它确保:对于系统中其他组件的角度来说,所有锁操作后面的内存操作看起来发生在锁操作之后。 + +LOCK操作之前的内存操作可能发生在它完成之后。 +> 我的小问题:对这一点不太理解,**在它完成之后**是一个什么概念?难道说锁操作之前的内存操作可以塞到临界区里面吗? + +LOCK操作几乎总是与 UNLOCK 操作配对。 + +**UNLOCK Operations** +UNLOCK 操作,UNLOCK 操作也充当了一个单方面屏障的角色。 + +确保对于系统中其他组件的角度来说,在UNLOCK操作之前的所有内存操作看起来发生在UNLOCK之前。 + +LOCK 和 UNLOCK 操作确保相互之间严格按顺序执行。 + +LOCK 和 UNLOCK 操作的用法通常可以避免再使用其他类型的内存屏障。 + +#### 14.2.10.3 What May Not Be Assumed About Memory Barriers? + +1.不能保证在内存屏障之前的内存访问将在内存屏障指令完成时完成;屏障仅仅用来在CPU的访问队列中做一个标记,表示相应类型的访问不能被穿越。 + +2.不能保证在一个CPU中执行一个内存屏障将直接影响另外一个CPU或者影响系统中其他硬件。其间接效果是第二个CPU所看到第一个CPU的内存访问顺序,但是请参照下一点。 + +3.不能保证一个CPU将看到第二个CPU的访问操作的正确顺序,即使第二个CPU使用一个内存屏障也是这样,除非第一个CPU也使用一个配对的内存屏障(参见14.2.10.6节“SMP 屏障对”)。 + +4.不能保证某些CPU片外硬件不会重排对内存的访问。CPU 缓存一致性机制将在CPU之间传播内存屏障的间接影响,但是可能不会按顺序执行这项操作。 + +#### 14.2.10.4 Data Dependency Barriers +**例1** +初始状态:{A = 1, B = 2, C = 3,P = &A, Q = &C}: + +执行序: +```c +CPU 1 CPU 2 +B = 4; + +P = &B; + Q = P; + D = *Q; +``` +从直觉来看,结果有如下可能: +``` +(Q == &A) implies (D == 1) +(Q == &B) implies (D == 4) +``` + +例如,预期的过程如下: +``` +(Q == &B) -> B == 4 \ + > + -> D == B -> D == 4 +``` + +实际可能出现: + +``` +(Q == &B) implies (D == 2) +``` + +> `Paul`的解释如下: +> +> 注意这种极端违反直觉的情形在分离缓存机器中非常容易出现。例如,一个缓存带处理偶数编号的缓存行,另一个缓存带处理奇数编号的缓存行。指针P可能存储在奇数编号的缓存行,变量B存储在偶数编号的缓存行。那么,如果在读操作所在的CPU的缓存中,其偶数编号的缓存带异常繁忙,而奇数编号的缓存带空闲,就可以出现指针P的新值被看到(`&B`),而变量B的旧值被看到(`2`)。 +> + + +> 个人理解:`CPU2`中的两个`load`操作可能被打乱顺序,此时`D = *P`不成立。 +所以也可能出现这种情况: + +``` +(Q == &B) implies (D == 3) +``` +此时有: +``` +(Q == &B) -> B == 4 + -> D == *Q == C == 3 +``` + +修改方法: + +``` +CPU 1 CPU 2 +B = 4; + +P = &B; + Q = P; + + D = *Q; +``` +在`CPU2`的两个`load`操作之间添加数据依赖屏障,此时可以解决此问题。 + +**例2** +初始值{M[0] = 1, M[1] = 2, M[3] = 3, P = 0, Q = 3}: +``` +CPU 1 CPU 2 +M[1] = 4; + +P = 1; + Q = P; + + D = M[Q]; +``` +A number is read from memory and then used to calculate the index for an array access. + +**例3** +数据依赖屏障对于Linux内核的RCU系统来说非常重要。 + +`include/linux/rcupdateh`中的`rcu_dereference`函数,允许RCU指针的当前目标被替换成一个新值,而不会使得要替换的目标看起来没有被全部初始化。 + +#### 14.2.10.5 Control Dependencies +控制依赖需要一个完整的读内存屏障,而不是简单的数据依赖屏障来使它正常运行,如下所示: + +```c +q = &a; +if (p) + q = &b; + +x = *q; +``` + +这不会达到期望的效果,因为这实际上不是数据依赖,而是一个控制依赖。在该控制中,CPU在实际执行时,可能通过试图预取结果的方法来走捷径。这种情况下,实际需要如下代码。 + +```c +q = &a; +if (p) + q = &b; + +x = *q; +``` + +#### 14.2.10.6 SMP Barrier Pairing +一个写屏障应当总是与数据依赖屏障或者读屏障配对,虽然通用屏障也是可以的。类似的,一个读屏障或者数据依赖屏障总是应当与至少一个写屏障配对使用,虽然,通用屏障也是可以的。 +如下两种基本使用范例: +![MWmf0J.png](https://s2.ax1x.com/2019/11/20/MWmf0J.png) + +加入屏障后的数据更新流图如下所示: + +![MWmX0H.png](https://s2.ax1x.com/2019/11/20/MWmX0H.png) + +#### 14.2.10.7 Examples of Memory Barrier Pairings +> 本节使用的示意图值得学习。 + +**例1** +```c +STORE A = 1 +STORE B = 2 +STORE C = 3 + +STORE D = 4 +STORE E = 5 +``` +对应时序说明图如下所示: + +![MWlAFH.png](https://s2.ax1x.com/2019/11/20/MWlAFH.png) + +**例2** +初始条件:`{B = 7, X = 9, Y = 8, C = &Y}:` +```c +CPU 1 CPU 2 +A = 1; +B = 2; + +C = &B; LOAD X +D = 4; LOAD C (gets &B) + LOAD *C (reads B) +``` + +对应说明图如下所示: +![MWlYpn.png](https://s2.ax1x.com/2019/11/20/MWlYpn.png) + +此处存在`load`的乱序执行,可见此时的执行序是`LOAD C; LOAD *C; LOAD X;`。 +由于缺乏`data barrier`在`LOAD *C`即`LOAD B`时读到的是`old value`。 +所以在这个例子中,CPU2感知到的`*C == 7`。 + +为了解决这个问题,做如下修改: +```c +CPU 1 CPU 2 +A = 1; +B = 2; + +C = &B; LOAD X +D = 4; LOAD C (gets &B) + + LOAD *C (reads B) +``` + +此时CPU的执行序如下所示: +![MW1O2R.png](https://s2.ax1x.com/2019/11/20/MW1O2R.png) + +可以看出,尽管此时在``之前的两条`LOAD`指令仍然被乱序执行,但是影响关键功能的`LOAD B`操作被放到了`LOAD X`以及`LOAD C`之后执行,这样就保证了在CPU1中存在写屏障的前提下,读到的`B`是新值`2`。 + + +**例3** + +读屏障仅仅对`LOAD`有效。 +例如以下的CPU执行序: +```c +CPU 1 CPU 2 +A = 1; + +B = 2; + LOAD B + LOAD A +``` +这个模型比较简单,做以下修改之后就可以使其按预期工作。 + +```c +CPU 1 CPU 2 +A = 1; + +B = 2; + LOAD B + + LOAD A +``` + +#### 14.2.10.8 Read Memory Barriers vs Load Speculation +举以下的例子来说明CPU的Load Speculation +``` +CPU 1 CPU 2 + LOAD B + DIVIDE + DIVIDE + LOAD A +``` + +![MfSFSA.png](https://s2.ax1x.com/2019/11/20/MfSFSA.png) + +如上图所示,CPU可能在执行`DIVIDE`操作完成之前执行`LOAD A`,这时候`LOAD`得到的值可能是`old value`。 + +所以,添加读屏障或数据屏障,如下所示: + +``` +CPU 1 CPU 2 + LOAD B + DIVIDE + DIVIDE + + LOAD A +``` + +此时CPU执行序可能如下所示: + +![MfSJmV.png](https://s2.ax1x.com/2019/11/20/MfSJmV.png) + +这时冒险内存点没有变化,冒险获得的值将被使用。 + +而如果其他CPU对A进行了更新或者使它无效,那么冒险过程将被中止,A的值将被重新`load`,如下图所示: + +![MfS60K.png](https://s2.ax1x.com/2019/11/20/MfS60K.png) + +### 14.2.11 Locking Constraints +此节内容与`14.2.10.2`有些许重复,不过在这里阐述的更详细。 + +锁原语包含了隐含的内存屏障。这些隐含的屏障提供了如下保障。 + +1.LOCK 操作保证 + +● LOCK之后的内存操作将在LOCK操作完成之后完成。 + +● LOCK操作之前的内存操作可能在LOCK操作完成之后完成。 +> 我的小问题:这是否会带来问题? + +2.UNLOCK 操作保证 + +● UNLOCK之前的内存操作将在UNLOCK 操作完成前完成。 + +● UNLOCK之后的操作可能在UNLOCK 操作完成前完成。 + +3.LOCK vs LOCK 保证 + +● All LOCK operations issued before another LOCK operation will be completed before that LOCK operation. + +4.LOCK vs UNLOCK 保证 + +● All LOCK operations issued before an UNLOCK operation will be completed before the UNLOCK operation. + +● All UNLOCK operations issued before a LOCK operation will be completed before the LOCK operation. + +5.Failed conditional LOCK guarantee + +几种 LOCK 变体操作可能失败,可能是由于不能立即获得锁,也可能是由于在等待锁可用时,接收到一个非阻塞信号或者发生异常。 +失败的锁并不隐含任何类型的屏障。 + +### 14.2.12 Memory-Barrier Examples +#### 14.2.12.1 Locking Examples +**LOCK Followed by UNLOCK** + +如下例所示:`*A`和`*B`的赋值顺序无法保证,因为LOCK操作之前的内存操作可能在LOCK操作完成之后完成,UNLOCK之后的操作可能在UNLOCK 操作完成前完成。 + +```c +*A = a; +LOCK +UNLOCK +*B = b; +``` + +实际操作执行序可能按如下顺序执行。 +```c +LOCK +*B = b; +*A = a; +UNLOCK +``` + +>小问题14.13:什么样的LOCK-UNLOCK操作序列才能是一个全内存屏障? + +答案:两个连续的LOCK-UNLOCK操作序列,或者(稍微有违常规),一个UNLOCK操作后面跟随一个LOCK操作。 如下所示: +```c +*A = a; +UNLOCK +LOCK +*B = b; +``` +下面给出证明: +● UNLOCK之前的内存操作将在UNLOCK 操作完成前完成。 +所以保证了`*A = a`在`UNLOCK`前完成。 +● LOCK之后的内存操作将在LOCK操作完成之后完成。 +所以保证了`*B = b`在`LOCK`后完成。 +● All UNLOCK operations issued before a LOCK operation will be completed before the LOCK operation. +所以保证了`UNLOCK`在`LOCK`之前完成 +综上,`*A = a`在`*B = b`之前完成。 + +在`Intel Itanium`处理器中,会使用这些semi-permeable locking primitives来实现内存屏障的功能。 + +**LOCK-Based Critical Sections** +考虑下面的例子: + +```c +*A = a; +*B = b; +┏┳┓┏┳┓┏┳┓ LOCK ┏┳┓┏┳┓┏┳┓ +*C = c; +*D = d; +┗┻┛┗┻┛┗┻┛ UNLOCK ┗┻┛┗┻┛┗┻┛ +*E = e; +*F = f; +``` + +在实际执行中可能按照下列时序执行: +``` +┏┳┓┏┳┓┏┳┓ LOCK ┏┳┓┏┳┓┏┳┓ +*A = a; *F = f; +*E = e; +*C = c; *D = d; +*B = b; +┗┻┛┗┻┛┗┻┛ UNLOCK ┗┻┛┗┻┛┗┻┛ +``` + +> 小问题14.15:假设大括号中的操作并发执行,表14.2中哪些行对变量“A”到“F”的赋值和LOCK/UNLOCK操作进行乱序是合法的。为什么是,为什么不是? +![MfVLqJ.png](https://s2.ax1x.com/2019/11/20/MfVLqJ.png) + +**Ordering with Multiple Locks** + +包含多个锁的代码仍然看到包含这些锁的顺序约束,但是必须小心的一点是,记下哪一个约束来自于哪一个锁。 + +如下所示: +```c +CPU 1 CPU 2 +A = a; E = e; +LOCK M; LOCK Q; +B = b; F = f; +C = c; G = g; +UNLOCK M; UNLOCK Q; +D = d; H = h; +``` + +对于以上代码: +如`14.2.11 Locking Constraints`中所述,所有CPU必然看到以下的顺序约束。 + +1.LOCK M在 B、C和D之前。 + +2.UNLOCK M 在A、B和 C之后。 + +3.LOCK Q 在F、G和H之前。 + +4.UNLOCK Q在E、F和G之后。 + +**Ordering with Multiple CPUs on One Lock** + +```c +CPU 1 CPU 2 +A = a; E = e; +LOCK M; LOCK M; +B = b; F = f; +C = c; G = g; +UNLOCK M; UNLOCK M; +D = d; H = h; +``` + +在这种情况下,要么CPU 1在CPU 2前申请到M,要么相反。在第一种情况下,对A、B、C的赋值,必然在对F、G、H的赋值之前。另一方面,如果CPU2先申请到锁,那么对E、F、G的赋值必然在对B、C、D的赋值之前。 + +这个例子说明了`14.2.11`锁的约束中的第4点。 +> All LOCK operations issued before an UNLOCK operation will be completed before the UNLOCK operation. + +### 14.2.13 The Effects of the CPU Cache + +![MfnlLD.png](https://s2.ax1x.com/2019/11/20/MfnlLD.png) + +内存屏障可以被认为起到图14.17中垂直线的作用,它确保CPU按适当的顺序向内存展示其值,就像确保它按适当顺序看到其他CPU所做的变化一样。 + +#### 14.2.13.1 Cache Coherency + +虽然缓存一致性协议保证特定CPU按顺序看到自己对内存的访问,并且所有CPU对包含在单个缓存行的单个变量的修改顺序会达成一致,但是不保证对不同变量的修改能够按照相同顺序被其他所有CPU看到,虽然某些计算机系统做出了这样的保证,但是可移植软件不能依赖它们。 + +以下的分离`cache`模型在理解一些违背直觉的场景下很有益。 + +![MfnX6K.png](https://s2.ax1x.com/2019/11/20/MfnX6K.png) + +要明白为什么乱序可能发生,考虑如图14.18所示的2-CPU系统,在这样的系统中,每一个CPU拥有一个分离的缓存,这个系统有以下属性。 + +1.奇数编号的缓存行可能在缓存A、C中,在内存中,或者兼而有之。 + +2.偶数编号的缓存行可能在缓存B、D中,在内存中,或者兼而有之。 + +3.当CPU核正在向它的缓存获取数据,它的其他缓存不必处于静止状态。其他缓存可以响应“使无效”请求,回写脏缓存行,处理CPU内存访问队列中的元素,或者其他。 + +4.每一个缓存都有各自的操作队列,它些队列被缓存用来维护所请求的一致性和顺序属性。 + +5.这些队列不一定在这些队列元素所影响的缓存行元素进行`load`和存储操作时进行刷新。 +比如`Invalidate Queues`,与`store buffer`不同,CPU无法在`load`操作时扫描`Invalidate Queues`。 + +简而言之,如果缓存A忙,但是缓存行B空闲,那么与CPU 2向偶数行`store`相比,CPU1向奇数编号的缓存行`store`会被延迟。在不那么极端的情况下,CPU 2就可以看到CPU1的乱序操作。 + + +### 14.2.14 Where Are Memory Barriers Needed? +仅仅在两个CPU之间或者CPU与设备之间存在需要交互的可能性时,才需要内存屏障。任何代码只要能够保证没有这样的交互,这样代码就不必使用内存屏障。 + +注意,这是最小的保证。正如附录C所讨论的那样,不同体系结构给出了更多保证。但是,不能将代码设计来只运行在特定的体系中。 + +像锁原语、原子数据结构维护原语以及遍历这些,实现原子操作的原语通常在它们的定义中包含了必要的内存屏障。 + +但是,有一些例外,例如在Linux内核中的atomic_inc()。因此请确保查阅了文档,并且,如果可能的话,查阅它在软件环境中实际的实现。 + +最后一个忠告,使用原始的内存屏障原语应当是不得已的选择。使用已经处理了内存屏障的已有原语,几乎总是更好的选择。 + +14.3 Non-Blocking Synchronization +14.3.1 Simple NBS +14.3.2 NBS Discussion diff --git a/_posts/2021-07-30-perfbook-c15.md b/_posts/2021-07-30-perfbook-c15.md new file mode 100644 index 0000000..4d64946 --- /dev/null +++ b/_posts/2021-07-30-perfbook-c15.md @@ -0,0 +1,454 @@ +--- +title: perfbook-c15-附录C +layout: post +categories: perfbook +tags: perfbook 并行编程 书籍 内存序 并发控制 +excerpt: perfbook-c15-附录C +--- +# 15 Advanced Synchronization: Memory Ordering +## 15.1 Ordering: Why and How? +**例1** +以下这个例子在各个平台下都可以触发`exists`,包括x86。 + +```C +/* C-SB+o-o+o-o.litmus */ +P0(int *x0, int *x1) +{ + int r2; + + WRITE_ONCE(*x0, 2); + r2 = READ_ONCE(*x1); +} + + +P1(int *x0, int *x1) +{ + int r2; + + WRITE_ONCE(*x1, 2); + r2 = READ_ONCE(*x0); +} + +exists (1:r2=0 /\ 0:r2=0) + +``` + +### 15.1.1 Why Hardware Misordering? + +![QrvbrV.png](https://s2.ax1x.com/2019/12/11/QrvbrV.png) + +解释了上述的例子中是如何出现`exists (1:r2=0 /\ 0:r2=0)`这一结果的。 + +注意到第`4`行,在`CPU0`和`CPU1`同时发出`read-invalidate`消息之后,两个`CPU`会交换`cacheline`。 + +最后再将`store buffer`中的值写入到新得到的`cacheline`中。 + +### 15.1.2 How to Force Ordering? +**例2** +```C +P0(int *x0, int *x1) +{ + int r2; + + WRITE_ONCE(*x0, 2); + smp_mb(); + r2 = READ_ONCE(*x1); +} + + +P1(int *x0, int *x1) +{ + int r2; + + WRITE_ONCE(*x1, 2); + smp_mb(); + r2 = READ_ONCE(*x0); +} + +exists (1:r2=0 /\ 0:r2=0) + +``` + +以下为`exists (1:r2=2 /\ 0:r2=2)`的时序: + +![Qrzfpj.png](https://s2.ax1x.com/2019/12/11/Qrzfpj.png) + +以下为linux内核中内存序相关的原语: + +![Qspiq0.png](https://s2.ax1x.com/2019/12/11/Qspiq0.png) + +15.1.3 Basic Rules of Thumb + +## 15.2 Tricks and Traps +15.2.1 Variables With Multiple Values +15.2.2 Memory-Reference Reordering +15.2.3 Address Dependencies +15.2.4 Data Dependencies +15.2.5 Control Dependencies +15.2.6 Cache Coherence +15.2.7 Multicopy Atomicity + +15.3 Compile-Time Consternation +15.3.1 Memory-Reference Restrictions +15.3.2 Address- and Data-Dependency Diculties +15.3.3 Control-Dependency Calamities +15.4 Hardware Specifics +15.4.1 Alpha +15.4.2 ARMv7-A/R +15.4.3 ARMv8 +15.4.4 Itanium +15.4.5 MIPS +15.4.6 POWER / PowerPC +15.4.7 SPARC TSO +15.4.8 x86 +15.4.9 z Systems +15.5 Where is Memory Ordering Needed? + + + +# Appendix C Why Memory Barriers? +## C.1 Cache Structure +当一个特定的数据项初次被CPU访问时,它在缓存中还不存在,这称为“缓存缺失”(`cache miss`)。 + +经过一段时间后,CPU的缓存将被填满,后续的缓存缺失很可能需要换出缓存中现有的数据,以便为最近的访问项腾出空间。这种“缓存缺失”被称为“容量缺失”(`capacity miss`),因为它是由于缓存容量限制而造成的。 + +但是,即使此时缓存还没有被填满,大量缓存也可能由于一个新数据而被换出。这是由于大容量缓存是通过硬件哈希表来实现的。 + +因此,在一个特定的CPU写数据前,让所有CPU都意识到数据被修改这一点是非常重要的,因此,它必须首先从其他CPU的缓存中移除,或者叫“使无效”(`invalidated`)。 + +一旦“使无效”操作完成,CPU可以安全的修改数据项。如果数据存在于该CPU缓存中,但是是只读的,这个过程称为“写缺失”(`write miss`)。 + +随后,如果另外某个CPU试图访问数据项,将会引起一次缓存缺失,此时,由于第一个CPU为了写而使得缓存项无效,这种类型的缓存缺失被称为“通信缺失”(`communication miss`)。 + +> 我的小问题:`write miss`和`communication miss`的区别是什么? +`write miss`的定义:如果数据存在于该CPU缓存中,但是是只读的,这个过程称为“写缺失”(`write miss`)。 +这看起来更像是一种状态。 +而`communication miss`是一种事件。 + +## C.2 Cache-Coherence Protocols +详见原文。 + +> 我的小问题:如何理解`read invalidate`消息? + +答案:When the CPU does an atomic readmodify-write operation on a data item that was not present in its cache. It transmits a “read invalidate”, receiving the data via a “read response”. +At the same time, the CPU can complete the transition of MESI status once it has also received a full set of “invalidate acknowledge” responses. +相当于把一个变量的所有权从其他CPU中夺了过来,与排他读的语义相近。 + +## C.3 Stores Result in Unnecessary Stalls + +![MfL474.png](https://s2.ax1x.com/2019/11/20/MfL474.png) + +### C.3.1 Store Buffers + +避免这种不必要的写停顿的方法之一,是在每个CPU和它的缓存之间,增加`store buffer`,如图C.5。通过增加这些`store buffer`区,CPU 0可以简单地将要保存的数据放到`store buffer`区中,并且继续运行。当缓存行最终从CPU1转到CPU0时,数据将从存储缓冲区转到缓存行中。 + +![MfLqc6.png](https://s2.ax1x.com/2019/11/20/MfLqc6.png) + +### C.3.2 Store Forwarding + +每个CPU在执行加载操作时,将先从`store buffer`中获取,如图`C.6`。换句话说,一个特定的CPU存储操作直接转发给后续的读操作,而并不必然经过其缓存。 + +![MfO8DU.png](https://s2.ax1x.com/2019/11/20/MfO8DU.png) + +### C.3.3 Store Buffers and Memory Barriers + +内存屏障`smp_mb`将导致CPU在刷新`store buffer`中后续的`store`操作到`cacheline`之前,前面的`store`操作先被刷新。 + +## C.4 Store Sequences Result in Unnecessary Stalls + +不幸的是,每一个`store buffer`相对而言都比较小,这意味着执行一段较小的存储操作序列的CPU,就可能填满它的`store buffer`(例如,当所有`store`操作都发生了`cache misses`时)。从这一点来看,CPU在能够继续执行前,必须再次等待刷新操作完成,其目的是为了清空它的`store buffer`。 + +相同的情况可能在内存屏障之后发生,内存屏障之后的所有`store`操作指令,都必须等待刷新操作完成,而不管这些后续`store`是否存在缓存缺失。 + +这可以通过使`invalidate acknowledge messages`更快到达CPU来到得到改善。 + + +### C.4.1 Invalidate Queues + +在发送应答前,CPU 不必真正使无效缓存行。它可以将使无效消息入队列。 +如下图所示: +![MfXZM6.png](https://s2.ax1x.com/2019/11/20/MfXZM6.png) + +### C.4.2 Invalidate Queues and Invalidate Acknowledge + +CPU必须在准备发送使无效消息前,引用它的使无效队列。 + +如果一个缓存行相应的条目在使无效队列中,则CPU不能立即发送使无效消息,它必须等待无效队列中的条目被处理。 + +### C.4.3 Invalidate Queues and Memory Barriers + +内存屏障指令能够与使无效队列交互,这样,当一个特定的CPU执行一个内存屏障时,它标记无效队列中的所有条目,并强制所有后续的`load`操作进行等待,直到所有标记的条目都保存到CPU的缓存中。 + +## C.5 Read and Write Memory Barriers + +Roughly speaking,一个“读内存屏障”仅仅标记它的使无效队列,一个“写内存屏障”仅仅标记它的存储缓冲区,而完整的内存屏障同时标记无效队列及存储缓冲区。 + +## C.6 Example Memory-Barrier Sequences +本节提供了一些有趣的、但是稍微有点不一样的内存屏障用法。 +虽然它们能在大多数时候正常工作,但是其中一些只能在特定CPU上运行。 +如果目的是为了产生那些能在所有CPU上都能运行的代码,那么这些用法必须要避免。 +为了更好理解它们之间的微妙差别,我们首先需要关注乱序体系结构。 + +### C.6.1 Ordering-Hostile Architecture +这里作者设计了一个虚构的、最大限度的乱序体系结构,如下图所示: + +![MfXWo4.png](https://s2.ax1x.com/2019/11/20/MfXWo4.png) + +其硬件约束如下: + +1.单个 CPU总是按照编程顺序来感知它自己的内存访问。 + +2.仅仅在操作不同地址时,CPU才对给定的`store`操作进行重新排序。 + +3.一个特定 CPU,在内存屏障之前的所有`load`操作(smp_rmb())将在所有读内存屏障后面的操作之前被所有其他CPU所感知。 + +4.一个特定 CPU,所有在写内存屏障之前的写操作(smp_wmb())都将在所有内存屏障之后的写操作之前,被所有其他CPU所感知。 + +5.一个特定 CPU,所有在内存屏障之前的内存访问(`load`和`store`)(smp_mb())都将在所有内存屏障之后的内存访问之前,被所有其他CPU所感知。 + +> 小问题C.11:针对上述第1点——每个CPU按序看到它自己的内存访问,这样能够确保每一个用户线程按序看到它自己对内存的访问吗?为什么能,为什么不能? + +答案:不能。考虑这样一种情况,一个线程从一个CPU迁移到另外一个CPU,目标CPU感知到源CPU最近对内存的访问是乱序的。为了保证用户态的安全,内核黑客必须在进程切换时使用内存屏障。但是,在进程切换时要求使用的锁操作,已经自动提供了必要的内存屏障,这导致用户态任务将按序看到自己对内存的访问。也就是说,如果你正在设计一个`super-optimized scheduler`,不管是在内核态还是用户态,都要注意这种情形。 + +### C.6.2 Example 1 +初始状态:`a == b == c == 0` + +![MfXWo4.png](https://s2.ax1x.com/2019/11/20/MfXWo4.png) + +![Mfz4HK.png](https://s2.ax1x.com/2019/11/20/Mfz4HK.png) + + +我们假设CPU 0最近经历了太多cache miss,以至于它的message queue满了。CPU 1比较幸运,它操作的内存变量在cacheline中都是exclusive状态,不需要发送和node 1进行交互的message,因此它的message queue是空的。对于CPU0而言,a和b的赋值都很快体现在了NODE 0的cache中(CPU1也是可见的),不过node 1不知道,因为message都阻塞在了cpu 0的message queue中。与之相反的是CPU1,其对c的赋值可以通过message queue通知到node1。这样导致的效果就是,从cpu2的角度看,它先观察到了CPU1上的c=1赋值,然后才看到CPU0上的对a变量的赋值,这也导致了尽管使用了memory barrier,CPU2上仍然遭遇了assert fail。之所以会fail,主要是在cpu2上发生了当z等于1的时候,却发现x等于0。 + +> 我的小问题:这里的`Message Queue`指的是什么? + +我的答案:不是`store buffer`也不是`invalid queue`,因为如文中所述:_a和b的赋值都很快体现在了NODE 0的cache中(CPU1也是可见的)_,说明此时新值已经从`store buffer`中写到了`cache`中,但是`Invalidate`消息还没发出去。 +所以说明这里的`message queue`就是存放的就是本CPU要发送的`message`,可能堆积的是那些已经发出去但是还没得到应答的`message`。 + +> 小问题C.12:这段代码可以通过在CPU1的“while”和对“c”的赋值之间插入一个内存屏障来修复吗?(见`C.6.3 Example 2`)为什么能,为什么不能? + +答案:不能,这样的内存屏障仅仅强制CPU1本地的内存顺序。它对CPU0和CPU1之间的关联顺序没有效果,因此断言仍然会失败。 + +**保证(Guarantee)**:但是所有主流计算机系统提供一种“可传递”机制,这将提供一种直观感觉上的顺序,如果B看见A的操作,并且C看见B的操作,那么C必定也看见A的操作。 + +### C.6.3 Example 2 + +![MfzoND.png](https://s2.ax1x.com/2019/11/20/MfzoND.png) + +分析见上述小问题`C.12`。 + +从原理上来说,编写可移植代码不能用上面的例子,但是,正如前面一样,实际上这段代码可以在大多数主流的计算机正常运行。 + +### C.6.4 Example 3 + +![MfzqgA.png](https://s2.ax1x.com/2019/11/20/MfzqgA.png) + +请注意,不管是CPU 1 还是 CPU 2都要看到CPU0在第三行对“b”的赋值后,才能处理第5行。一旦CPU 1和2已经执行了第4行的内存屏障,它们就能够看到CPU0在第2行的内存屏障前的所有赋值。类似的,CPU0在第8行的内存屏障与CPU1和CPU2在第4行的内存屏障是一对内存屏障,因此CPU0将不会执行第9行的内存赋值,直到它对“a”的赋值被其他CPU可见。因此,CPU2在第9行的assert将不会触发。 + +Linux内核的synchronize_rcu()原语使用了类似于本例中的算法。 + +> 小问题C.14:如果在表C.4的例子中,CPU 2 执行一个断言assert(e==0||c==1),这个断言会被触发吗? + +答案:结果依赖于CPU是否支持“可传递性”。换句话说,通过在CPU 0对“c”的加载和对“e”的存储之间的内存屏障,CPU 0在看到CPU 1对“c”的存储之后,才对“e”进行存储。如果其他CPU看到CPU 0对“e”的存储,那么是否能够保证看到CPU 1的存储? + +**保证(Guarantee)**:所有我关注到的CPU都声称提供可传递性。 + +## C.7 Memory-Barrier Instructions For Specific CPUs + +#### 内存序模型 +每个CPU都有它自己特定的内存屏障指令,这可能会给我们带来一些移植性方面的挑战,如表C.5所示。 + +![MhPjk6.png](https://s2.ax1x.com/2019/11/20/MhPjk6.png) + +实际上,很多软件环境,包括pthreads和Java,简单的禁止直接使用内存屏障,强制要求程序员使用互斥原语,这些互斥原语包含内存屏障,对内存屏障进行所需的扩展。 + +在表C.5中,前`4`列表示CPU是否允许4种加载和存储组合进行重排。 + +第`5-6`列表示CPU是否允许加载/存储操作与原子指令一起进行重排。 + +第`7`列,数据依赖读重排,需要由随后与Alpha CPU相关的章节进行解释。简短的讲,Alpha需要为关联数据之间的读以及更新使用内存屏障。是的,这表示Alpha确实可能在它取得指针本身的值之前,取得指针指向的值。这听起来很奇怪,但是确实是真的。 + +这种极端弱内存序模型的好处是,Alpha可以使用更简单的缓存硬件,因而允许更高的时钟频率。 + +第`8`列表示特定CPU是否拥有一个不一致的指令缓存和流水线。对于自修改代码来说,一些CPU需要执行特殊的指令。 + +带括号的CPU名称表示CPU允许这样的模式,但是实际上很少被使用。 + +> 在2018版的perbook中的`15.4`节提供了最新的内存序图。 +![MhiBH1.png](https://s2.ax1x.com/2019/11/20/MhiBH1.png) + +#### 内核中的内存屏障指令 + +1.smp_mb():同时针对加载、存储进行排序的内存屏障。这表示在内存屏障之前的加载、存储,都将在内存屏障之后的加载、存储之前被提交到内存。 + +2.smp_rmb():仅仅对加载进行排序的读内存屏障。 + +3.smp_wmb():仅仅对存储进行排序的写内存屏障。 + +4.smp_read_barrier_depends():强制将依赖于之前操作的后续操作进行排序。除了ALPHA之外,这个原语在其他体系上都是空操作。 + +5.mmiowb():强制将那些由全局自旋锁保护的MMIO写操作进行排序。在自旋锁已经强制禁止MMIO乱序的平台中,这个原语是空操作。mmiowb()非空的平台包括一些(但是不是全部)IA64、FRV、MIPS和SH。这个原语比较新,因此很少有驱动使用到了它。 +> 我的小问题:什么是`MMIO`?全书多次提到这个概念,把它当成一种非通用场景作为考虑。 + +`smp_`前缀代表这些原语仅仅在SMP内核上才产生代码,它们都存在一个`UP`版本(分别是mb()、rmb()、wmb()和read_barrier_depends()),即使在`UP`内核中,这些原语在也生成代码。 +> 我的小问题:什么是`SMP`内核,什么是`UP`内核。 + + +### C.7.3 ARMv7-A/R +ARM CPU族在嵌入式应用中极为流行,特别是在电源受限的应用中,如移动电话。虽然如此,ARM多核已经存在五年以上的时间了。它的内存模型类似于Power(参见C.7.6节),但是ARM使用了不同的内存屏障指令集: + +1.DMB(数据内存屏障)导致在屏障前的相同类型的操作,看起来先于屏障后的操作先执行。操作类型可以是所有操作,也可能仅限于写操作。 + +> ARM allows cache coherence to have one of three scopes: single processor, a subset of the processors (“inner”) and global (“outer”). + +ARM 允许三种范围的缓存一致性:单处理器,处理器子集(“inner”)以及全局范围内的一致(“outer”)。 + +2.DSB(数据同步屏障)导致特定类型的操作,在随后的任何操作被执行前,真的已经完成。操作类型与DMB相同。在ARM体系早期的版本中,DSB指令被称为DWB(清除写缓冲区还是数据写屏障皆可)。 + +3.ISB(指令同步屏障)刷新CPU流水线,这样所有随后的指令仅仅在ISB指令完成后才被读取。例如,如果你编写一个自修改的程序(如JIT),应当在生成代码及执行代码之间执行一个ISB指令。 + +没有哪一个指令与Linux的rmb()语义完全相符。因此必须将rmb()实现为一个完整的DMB。DMB和DSB指令具有访问顺序方面的递归定义,其具有类似于POWER架构累积性的效果。 + +> ARM also implements control dependencies, so that if a conditional branch depends on a load, then any store executed after that conditional branch will be ordered after the load. + +ARM 也实现了控制依赖,因此,如果一个条件分支依赖于一个加载操作,那么在条件分支后面的存储操作都在加载操作后执行。 + +> However, loads following the conditional branch will not be guaranteed to be ordered unless there is an ISB instruction between the branch and the load. Consider the following example: + +但是,并不保证在条件分支后面的加载操作也是有序的,除非在分支和加载之间有一个ISB指令。如下例: + +```c +r1 = x; +if (r1 == 0) + nop(); +y = 1; +r2 = z; +ISB(); +r3 = z; +``` + +> In this example, `load-store control dependency ordering` causes the load from x on line 1 to be ordered before the store to y on line 4. + +在这个例子中,存储/加载控制依赖导致在第1行的加载x操作被排序在第4行对Y的存储操作之前。 + +> However, ARM does not respect `load-load control dependencies`, so that the load on line 1 might well happen after the load on line 5. + +但是,ARM并不考虑加载/加载控制依赖,因此,第1行的加载也许会在第5行的加载操作后面发生。 + +> On the other hand, the combination of the conditional branch on line 2 and the ISB instruction on line 6 ensures that the load on line 7 happens after the load on line 1. + +另一方面,第2行的条件分支与第6行的ISB指令确保第7行在第1行后面发生。 + +> Note that inserting an additional ISB instruction somewhere between lines 3 and 4 would enforce ordering between lines 1 and 5. + +注意,在第3行和第4行之间插入一个ISB指令将确保第1行和第5行之间的顺序。 如下所示: + +```c +r1 = x; +if (r1 == 0) + nop(); +ISB(); +y = 1; +r2 = z; +ISB(); +r3 = z; +``` + +### 15.4.3 ARMv8 +> ARMv8’s memory model closely resembles its ARMv7 counterpart, but adds load-acquire (LDLARB, LDLARH, and LDLAR) and store-release (STLLRB, STLLRH, and STLLR) instructions. + +ARMv8的内存模型与ARMv7的存储器模型非常相似,但是增加了`load-acquire`(`LDLARB`,`LDLARH`和`LDLAR`)和`store-release`(`STLLRB`,`STLLRH`和`STLLR`)指令。 + +> These instructions act as “half memory barriers”, so that ARMv8 CPUs can reorder previous accesses with a later LDLAR instruction, but are prohibited from reordering an earlier LDLAR instruction with later accesses, as fancifully depicted in Figure 15.14. +![MIkC4I.png](https://s2.ax1x.com/2019/11/21/MIkC4I.png) +Similarly, ARMv8 CPUs can reorder an earlier STLLR instruction with a subsequent access, but are prohibited from reordering previous accesses with a later STLLR instruction. +As one might expect, this means that these instructions directly support the C11 notion of load-acquire and store-release.(此概念的说明可参考[此处](https://juejin.im/post/5daaf3d4f265da5b950a6337)) + +`Read-Acquire`用于修饰内存读取指令,一条 `read-acquire` 的读指令会禁止它后面的内存操作指令被提前执行,即后续内存操作指令重排时无法向上越过屏障,下图直观的描述了这一功能: + +![MInmQI.png](https://s2.ax1x.com/2019/11/21/MInmQI.png) + +`Write-Release`用于修饰内存写指令,一条 `write-release` 的写指令会禁止它上面的内存操作指令被滞后到写指令完成后才执行,即写指令之前的内存操作指令重排时不会向下越过屏障,下图直观描述了这一功能: + +![MInnyt.png](https://s2.ax1x.com/2019/11/21/MInnyt.png) + +> However, ARMv8 goes well beyond the C11 memory model by mandating that the combination of a store-release and load-acquire act as a full barrier under many circumstances. +For example, in ARMv8, given a store followed by a store-release followed a load-acquire followed by a load, all to different variables and all from a single CPU, all CPUs would agree that the initial store preceded the final load. +Interestingly enough, most TSO architectures (including x86 and the mainframe) do not make this guarantee, as the two loads could be reordered before the two stores. + +除了支持`C11`标准中对应的语义之外,ARMv8在许多情况下将`store-release`和`load-acquire`的结合作为`full barrier`。 +例如,在ARMv8中,给出如下操作序列,所有这些都存储在不同的变量中,并且全部来自一个`CPU`,所有CPU都同意第`1`行的`store`在第`4`行的`load`之前执行。 + +```asm +store +store-release +load-acquire +load +``` +有趣的是,大多数`TSO(total store order)`体系结构(包括x86和大型机)都不能保证这一点,因为可以两个`store`之前对这两个`load`进行重新排序。 + +> ARMv8 is one of only two architectures that needs the smp_mb__after_spinlock() primitive to be a full barrier, due to its relatively weak lock-acquisition implementation in the Linux kernel. + +ARMv8是仅有的两种需要smp_mb__after_spinlock()原语成为完整障碍的体系结构之一,这是由于其在Linux内核中的锁获取实现相对较弱。 + +> ARMv8 also has the distinction of being the first CPU whose vendor publicly defined its memory ordering with an executable formal model [ARM17]. + +ARMv8还有一个区别,它是第一个供应商使用可执行的正式模型公开定义其内存顺序的CPU,详见arm白皮书。 + +> 我的小问题:`LDXR/STXR`和`LDAXR/STLXR`的区别? + +答案:`LDXR/STXR`具有`Exclusive`语义,而`LDAXR/STLXR`除此之外还具有`Acquire-Release`语义,用于保证执行顺序。 + +### C.7.8 x86 +由于x86 CPU提供“`process ordering`”,因此所有CPU都会一致性的看到某个特定CPU对内存的写操作。 + +这样smp_wmb()实现为一个空操作。但是,它需要一个如下所示的编译屏障指令`barrier()`,以避免编译器进行性能优化,这样的性能优化导致越过smp_wmb()前后的指令的重排。 +``` +#define barrier() __asm__ __volatile__("": : :"memory") +``` + +从其他方面来说,x86 CPU传统上不保证`load`的顺序,smp_mb()和smp_rmb()被扩展为`lock;addl`。这个原子指令实际上是一个同时针对`load`和`store`的屏障。 + +Intel也为x86发布了一个内存模型白皮书:[Intel® 64 Architecture Memory Ordering White Paper](http://www.cs.cmu.edu/~410-f10/doc/Intel_Reordering_318147.pdf)。 + +事实证明,Intel 实际的CPU执行比前面的规范要求更严的内存序,因此,这个规范事实上只是强制规范早期的行为。 + +>Even more recently, Intel published an updated memory model for x86 [Int11, Section 8.2], which mandates a total global order for stores, although individual CPUs are still permitted to see their own stores as having happened earlier than this total global order would indicate. This exception to the total ordering is needed to allow important hardware optimizations involving store buffers. + +最近一段时间,Intel 发布了一个更新的内存模型,详见[Int11](https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.pdf)的`8.2`节——它要求对`store`来说实现全局序。虽然对单个CPU来说,**仍然允许它看到自己的存储操作**,而这些操作早于全局序发生。这个全局序的例外情况是必要的,因为每个CPU需要其`store buffer`的优化。 + +> In addition, memory ordering obeys causality, so that if CPU 0 sees a store by CPU 1,then CPU 0 is guaranteed to see all stores that CPU 1 saw prior to its store. Software may use atomic operations to override these hardware optimizations, which is one reason that atomic operations tend to be more expensive than their non-atomic counterparts. This total store order is not guaranteed on older processors. + +另外,内存序遵从“传递性”,因此,如果CPU0看到CPU1存储的值,那么CPU0也能看到,在CPU 1的存储操作之前,它所能看到的值。软件可以使用原子操作,来使这些优化无效,这也是原子操作比非原子操作开销更大的原因之一。全局存储序在老的处理器上并不能得到保证。 + +> It is also important to note that atomic instructions operating on a given memory location should all be of the same size [Int11, Section 8.1.2.2]. For example, if you write a program where one CPU atomically increments a byte while another CPU executes a 4-byte atomic increment on that same location, you are on your own. + +有一个特别需要注意的是,在一个特定内存位置的原子指令操作应当对齐相同大小的内存([Int11](https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.pdf)的`8.1.2.2`)。例如,如果你编写一个程序,它在一个CPU上对一个字节进行原子递增,而另一个CPU对同一个地址执行一个4字节原子递增,其后果需要由你自己负责。 + +> However, note that some SSE instructions are weakly ordered (clflush and non-temporal move instructions [Int04a]). CPUs that have SSE can use mfence for smp_mb(), lfence for smp_rmb(), and sfence for smp_wmb(). + +但是,请注意某些SSE 指令是弱序的(clflush及non-temporal搬移指令[Int04a])。有SSE指令的CPU可以用mfence实现smp_mb(),lfence实现smp_rmb(),sfence实现smp_wmb()。 + +>A few versions of the x86 CPU have a mode bit that enables out-of-order stores, and for these CPUs, smp_wmb() must also be defined to be lock;addl. + +某些版本的x86 CPU有一个模式位,允许在存储之间乱序,在这些CPU上,smp_wmb()必须被定义为lock;addl。 + +>Although newer x86 implementations accommodate self-modifying code without any special instructions, to be fully compatible with past and potential future x86 implementations, a given CPU must execute a jump instruction or a serializing instruction (e.g., cpuid) between modifying the code and executing it [Int11, Section 8.1.3] + +虽然较新的x86 实现可以适应自修改代码而不需要任何特殊指令,但是为了兼容旧的及以后的x86实现,一个特定CPU必须在修改代码及执行该代码之间,执行一个跳转指令或者串行化指令(例如cpuid等)[Int11,8.1.3节]。 + + +## C.8 Are Memory Barriers Forever? +讨论未来的CPU设计是否会做出一定的改变使得内存屏障成为历史。 + +## C.9 Advice to Hardware Designers +过去的硬件设计中引入的问题: +1. I/O devices that ignore cache coherence. +2. External busses that fail to transmit cache-coherence data. +3. Device interrupts that ignore cache coherence. +4. Inter-processor interrupts (IPIs) that ignore cache coherence. +5. Context switches that get ahead of cache coherence. +6. Overly kind simulators and emulators. + +# 其他参考资料 +[多进程和多线程的区别](https://blog.csdn.net/linraise/article/details/12979473) diff --git a/_posts/2021-07-30-perfbook-c2.md b/_posts/2021-07-30-perfbook-c2.md new file mode 100644 index 0000000..0de1542 --- /dev/null +++ b/_posts/2021-07-30-perfbook-c2.md @@ -0,0 +1,20 @@ +--- +title: perfbook-c2 +layout: post +categories: perfbook +tags: perfbook 并行编程 书籍 +excerpt: perfbook-c2 +--- +# 2 Introduction +编程的通用性和开发效率之间存在永恒的矛盾。 +![](https://raw.githubusercontent.com/suzixin/cloudimg/master/picture/20191027215221.png) + +将传统的单线程程序改写成并行的程序,主要可以抽象成下面的`4`个活动: +![](https://raw.githubusercontent.com/suzixin/cloudimg/master/picture/20191027215312.png) +- Work Partitioning + 好的`partition`设计有如下标准: + - 数据结构简洁、易懂 + - 通信开销小 +- Parallel Access Control +- Resource Partitioning and Replication +- Interacting With Hardware diff --git a/_posts/2021-07-30-perfbook-c3.md b/_posts/2021-07-30-perfbook-c3.md new file mode 100644 index 0000000..0656023 --- /dev/null +++ b/_posts/2021-07-30-perfbook-c3.md @@ -0,0 +1,27 @@ +--- +title: perfbook-c3 +layout: post +categories: perfbook +tags: perfbook 并行编程 书籍 +excerpt: perfbook-c3 +--- +# 3 Hardware and its Habits +> atomic operations usually apply only to single elements of data. + +原子操作存在其局限性,在数据结构复杂的场景下仍然需要内存屏障。 + + +### 3.1.4 Memory Barriers +Memory barriers will be considered in more detail in Chapter 15 and Appendix C. In the meantime, consider the following simple lock-based critical section: + +``` +spin_lock(&mylock); +a = a + 1; +spin_unlock(&mylock); +``` + +If the CPU were not constrained to execute these statements in the order shown, the effect would be that the variable “a” would be incremented without the protection of “mylock”, which would certainly defeat the purpose of acquiring it. + +**To prevent such destructive reordering, locking primitives contain either explicit or implicit memory barriers.** + +Because the whole purpose of these memory barriers is to prevent reorderings that the CPU would otherwise undertake in order to increase performance, memory barriers almost always reduce performance, as depicted in Figure 3.6. diff --git a/_posts/2021-07-30-perfbook-c4.md b/_posts/2021-07-30-perfbook-c4.md new file mode 100644 index 0000000..67fb53c --- /dev/null +++ b/_posts/2021-07-30-perfbook-c4.md @@ -0,0 +1,297 @@ +--- +title: perfbook-c4 +layout: post +categories: perfbook +tags: perfbook 并行编程 书籍 +excerpt: perfbook-c4 +--- +# 4 Tools of the Trade +## 4.1 Scripting Languages +直接使用脚本后台运行。 + +## 4.2 POSIX Multiprocessing +### 4.2.1 POSIX Process Creation and Destruction +`POSIX`进程的接口: +``` +通过fork()原语创建 +使用kill()原语销毁,也可以用exit()原语自我销毁。 +执行fork()的进程被称为新创建进程的“父进程”。 +父进程可以通过wait()原语等待子进程执行完毕,每次调用wait()只能等待一个子进程。 +It is critically important to note that the parent and child do not share memory. +``` + +### 4.2.2 POSIX Thread Creation and Destruction + +```c +#include +int pthread_create(pthread_t *thread, const pthread_attr_t *attr, + void *(*start_routine) (void *), void *arg); +/* 对fork-join中的wait()的模仿 */ +int pthread_join(pthread_t thread, void **retval); +void pthread_exit(void *status); +``` +线程之间共享内存。 + +### 4.2.3 POSIX Locking +常见接口如下所示: +```c +#include +/* 静态初始化 */ +pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; +/* 动态初始化 */ +int pthread_mutex_init(pthread_mutex_t *restrict mutex, +const pthread_mutexattr_t *restrict attr); +/* 销毁 */ +int pthread_mutex_destroy(pthread_mutex_t *mutex); +/* 获取一个指定的锁 */ +int pthread_mutex_lock(pthread_mutex_t *mutex); +int pthread_mutex_trylock(pthread_mutex_t *mutex); +/* 释放一个指定的锁 */ +int pthread_mutex_unlock(pthread_mutex_t *mutex); +``` +### 4.2.4 POSIX Reader-Writer Locking +```c +/* 静态初始化 */ +pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; +/* 动态初始化 */ +int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, +const pthread_rwlockattr_t *restrict attr); +/* 销毁 */ +int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); +``` +### 4.2.5 Atomic Operations (GCC Classic) +常用的基本原子操作接口 +```c +__sync_fetch_and_sub() +__sync_fetch_and_or() +__sync_fetch_and_and() +__sync_fetch_and_xor() +__sync_fetch_and_nand() +/* all of which return the old value. If you instead need the new value +you can instead use the follow */ +__sync_add_and_fetch() +__sync_sub_and_fetch() +__sync_or_and_fetch() +__sync_and_and_fetch() +__sync_xor_and_fetch() +__sync_nand_and_fetch() +``` + +常用的编译器级别屏障原语 +```c +/* Listing 4.9: Compiler Barrier Primitive (for GCC) */ +#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x)) +#define READ_ONCE(x) \ + ({ typeof(x) ___x = ACCESS_ONCE(x); ___x; }) +#define WRITE_ONCE(x, val) ({ ACCESS_ONCE(x) = (val); }) +#define barrier() __asm__ __volatile__("": : :"memory") +``` + +### 4.2.6 Atomic Operations (C11) +[`C11`标准的原子操作](https://en.cppreference.com/w/c/atomic) + +常见接口如下所示: +``` +atomic_flag_test_and_set +atomic_flag_clear +atomic_init +atomic_is_lock_free +atomic_store +atomic_load +atomic_exchange +atomic_compare_exchange_strong +atomic_compare_exchange_weak +atomic_fetch_add +atomic_fetch_sub +atomic_fetch_or +atomic_fetch_xor +atomic_fetch_and +atomic_thread_fence +atomic_signal_fence +``` + +可以用该接口实现应用级别的内存屏障, +如下所示: + +```c +#define MFENCE std::atomic_thread_fence(std::memory_order::memory_order_acq_rel) +``` + +### 4.2.7 Atomic Operations (Modern GCC) +```c +__atomic_load() +__atomic_load_n() +__atomic_store() +__atomic_store_n() +__atomic_thread_fence() +``` +在这里会用到内存序相关的参数,如下所示: +```c +__ATOMIC_RELAXED +__ATOMIC_CONSUME +__ATOMIC_ACQUIRE +__ATOMIC_RELEASE +__ATOMIC_ACQ_REL +__ATOMIC_SEQ_CST +``` + +### 4.2.8 Per-Thread Variables +```c +pthread_key_create() +pthread_key_delete() +pthread_setspecific() +pthread_setspecific() +``` +可以在类型后跟上`__thread`说明符,C11标准引入了代替这个说明符的`_Thread_local`关键字。 + +## 4.3 Alternatives to POSIX Operations(POSIX接口的替代方法) +### 4.3.2 Thread Creation, Destruction, and Control +内核提供的相关接口: +```c +/* The Linux kernel uses struct task_struct pointers to track kthreads */ +/* create */ +kthread_create() +/* To externally suggest that theystop (which has no POSIX equivalent) */ +kthread_should_stop() +/* wait for them to stop */ +kthread_stop() +/* For a timed wait. */ +schedule_timeout_interruptible() + +/* 以下是用户态的接口 */ +int smp_thread_id(void) +thread_id_t create_thread(void *(*func)(void *), void *arg) +for_each_thread(t) +for_each_running_thread(t) +void *wait_thread(thread_id_t tid) +void wait_all_threads(void) +``` + +### 4.3.3 Locking +```c +void spin_lock_init(spinlock_t *sp); +void spin_lock(spinlock_t *sp); +int spin_trylock(spinlock_t *sp); +void spin_unlock(spinlock_t *sp); +``` +### 4.3.4 Accessing Shared Variables +#### 4.3.4.1 Shared-Variable Shenanigans +这一节主要是讲编译器能对我们的程序做些什么来优化,这种优化常常会导致我们预期之外的逻辑。 + +- Load tearing +Load tearing occurs when the compiler uses multiple load instructions for a single asscess. +- Store tearing +Store tearing occurs when the compiler uses multiple store instructions for a single access. +- Load fusing +Load fusing occurs when the compiler uses the result of a prior load from a given variable instead of repeating the load. +程序实际`Load`的顺序可能与代码顺序不符,带来判断的错误。 +- Store fusing +Store fusing can occur when the compiler notices a pair of successive stores to a given variable with no intervening loads from that variable. +从`4.17`和`4.18`的例子可以看出,由于`Store fusing`机制,第`3`行的`store`操作被优化掉了,导致函数`shut_it_down`以及`work_until_shut_down`都无法退出循环,进入了死锁。 + +- Code reordering +- Invented loads +编译器去掉了中间变量导致了更多的`load`操作,在某些情况可以会增加`cache miss`的概率,带来性能损耗。 + +- Invented stores +复杂的`inline`函数可能会导致寄存器溢出,部分临时变量比如`other_task_ready`的值被覆盖,导致程序执行逻辑错误,这里例子实际应该解读为`UnInvented store`。 +例子`4.19`意在说明编译器的分支优化逻辑:This transforms the if-then-else into an if-then, saving one branch.这个应该是`Invented store`更为常用的场景,在`glibc`的汇编实现里也很多这种逻辑。 + +#### 4.3.4.2 A Volatile Solution +> Although it is now much maligned, before the advent of C11 and C++11 [Bec11], the volatile keyword was an indispensible tool in the parallel programmer’s toolbox. This raises the question of exactly what volatile means, a question that is not answered with excessive precision even by more recent versions of this standard [Smi18].6 This version guarantees that “Accesses through volatile glvalues are evaluated strictly according to the rules of the abstract machine”, that volatile accesses are side effects, that they are one of the four forward-progress indicators, and that their exact semantics are implementation-defined. Perhaps the most clear guidance is provided by this nonnormative note: + +`volatile`关键字是并行程序员工具箱中必不可少的工具。其语义如下: +> volatile暗示编译器避免对使用对象进行积极优化,因为对象的值可能通过实现无法检测到的方式进行更改。此外,对于某些实现,volatile可能指示需要特殊的硬件指令才能访问该对象。 + +通过`volatile`语义定义的宏可以解决`4.3.4.1`中发现的各种问题。 +```c +#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x)) +#define READ_ONCE(x) \ + ({ typeof(x) ___x = ACCESS_ONCE(x); ___x; }) +#define WRITE_ONCE(x, val) ({ ACCESS_ONCE(x) = (val); }) +``` + +对`volatile`语义解决方案的总结 +> To summarize, the volatile keyword can prevent load tearing and store tearing in cases where the loads and stores are machine-sized and properly aligned. It can also prevent load fusing, store fusing, invented loads, and invented stores. However, although it does prevent the compiler from reordering volatile accesses with each other, it does nothing to prevent the CPU from reordering these accesses. Furthermore, it does nothing to prevent either compiler or CPU from reordering non-volatile accesses with each other or with volatile accesses. Preventing these types of reordering requires the techniques described in the next section. + +也就是说`volatile`无法解决所有的问题,还是需要内存屏障,如下小节所示。 + +#### 4.3.4.3 Assembling the Rest of a Solution +- 编译级的屏障 +``` +while (!need_to_stop) { + barrier(); + do_something_quickly(); + barrier(); +} +``` + +- 运行时屏障 +```c +void shut_it_down(void) +{ + WRITE_ONCE(status, SHUTTING_DOWN); + smp_mb(); + start_shutdown(); + while (!READ_ONCE(other_task_ready)) + continue; + smp_mb(); + finish_shutdown(); + smp_mb(); + WRITE_ONCE(status, SHUT_DOWN); + do_something_else(); +} + +void work_until_shut_down(void) +{ + while (READ_ONCE(status) != SHUTTING_DOWN) { + smp_mb(); + do_more_work(); + } + smp_mb(); + WRITE_ONCE(other_task_ready, 1); +} +``` + +#### 4.3.4.4 Avoiding Data Races +使用锁解决并发访问变量的问题,可基本遵循如下原则: +> 1. If a shared variable is only modified while holding a given lock by a given owning CPU or thread, then all stores must use WRITE_ONCE() and non-owning CPUs or threads that are not holding the lock must use READ_ONCE() for loads. The owning CPU or thread may use plain loads, as may any CPU or thread holding the lock. +如果仅在拥有给定的拥有CPU或线程持有给定锁的同时修改共享变量,则所有存储区都必须使用WRITE_ONCE(),而没有拥有锁的非拥有CPU或线程必须使用READ_ONCE() 。 +拥有CPU或线程的线程可以使用普通负载,而持有锁的任何CPU或线程也可以使用。 +> 2. If a shared variable is only modified while holding a given lock, then all stores must use WRITE_ONCE(). CPUs or threads not holding the lock must use READ_ ONCE() for loads. CPUs or threads holding the lock may use plain loads. +如果仅在持有给定锁的情况下修改共享变量,则所有存储区都必须使用WRITE_ONCE()。 +未持有锁的CPU或线程必须使用READ_ ONCE()进行加载。CPU或线程持有锁可以使用普通的负载。 +> 3. If a shared variable is only modified by a given owning CPU or thread, then all stores must use WRITE_ONCE() and non-owning CPUs or threads must use READ_ONCE() for loads. The owning CPU or thread may use plain loads. +如果共享变量仅由给定的拥有CPU或线程修改,则所有存储必须使用WRITE_ONCE(),非拥有CPU或线程必须使用READ_ONCE()进行加载。 +拥有的CPU或线程可能使用普通负载。 + + +### 4.3.5 Atomic Operations +[内核文档atomic_ops.txt](https://github.com/tinganho/linux-kernel/blob/master/Documentation/atomic_ops.txt) + +### 4.3.6 Per-CPU Variables +内核提供了一些接口用于创造、初始化、访问线程独立的变量。 +```c +DEFINE_PER_THREAD() +DECLARE_PER_THREAD() +per_thread() +__get_thread_var() +init_per_thread() +``` + +## 4.4 The Right Tool for the Job: How to Choose? +从简到难,开销从大到小: +- 脚本的`fork`和`exec`接口 +- C的`fork()`和`wait()`接口 +- POSIX threading +- Chapter 9 Deferred Processing +- inter-process communication and message-passing + +针对工作平台、语言的差异,可以选择以下不同的多线程编程接口: +- C11标准中的接口 +- GCC接口 +- 使用`volatile`语义 +- 使用内存屏障 + +设计层面的告诫: +> The smarter you are, the deeper a hole you will dig for yourself before you realize that you are in trouble diff --git a/_posts/2021-07-30-perfbook-c5.md b/_posts/2021-07-30-perfbook-c5.md new file mode 100644 index 0000000..8b9bbfb --- /dev/null +++ b/_posts/2021-07-30-perfbook-c5.md @@ -0,0 +1,547 @@ +--- +title: perfbook-c5 +layout: post +categories: perfbook +tags: perfbook 并行编程 书籍 +excerpt: perfbook-c5 +--- +# 5 Counting +## 5.1 Why Isn’t Concurrent Counting Trivial? +- 并发计数不使用有原子操作的情况下会出错 +- 并发计数在使用原子操作的情况下,随着核数的增多,自增效率越来越低,如下图所示: +![image](https://user-images.githubusercontent.com/21327882/68538980-eee36580-03b7-11ea-9233-4c225bd821bb.png) + + +原因如下图所示: + +![截图](https://user-images.githubusercontent.com/21327882/68539004-2b16c600-03b8-11ea-8cef-62de167e30f5.PNG) + +在做原子操作的时候需要多个CPU的协同确认。 + +## 5.2 Statistical Counters + +### 5.2.1 Design +设计思想如下图所示: + +![image](https://user-images.githubusercontent.com/21327882/68539012-3a960f00-03b8-11ea-9423-5a881fc445b1.png) + +根据加法结合律和加法交化律,在各个CPU各自加,在读的时候将各个`CPU`的值加起来。 + +### 5.2.2 Array-Based Implementation + +代码路径:`count\count_stat.c` + +```c +DEFINE_PER_THREAD(unsigned long, counter); + +static __inline__ void inc_count(void) +{ + unsigned long *p_counter = &__get_thread_var(counter); + + WRITE_ONCE(*p_counter, *p_counter + 1); +} + +static __inline__ unsigned long read_count(void) +{ + int t; + unsigned long sum = 0; + + for_each_thread(t) + sum += READ_ONCE(per_thread(counter, t)); + return sum; +} +``` +对`update`侧友好,对`expense`侧不友好。 +在读的时候需要访问所有的处理器一遍,在这个过程中,实际上各个处理器的进程内变量`counter`还可以继续增加,所以算出来的值实际上是不精确的。 + +### 5.2.3 Eventually Consistent Implementation + +代码路径:`count\count_stat_eventual.c` + +`5.2.2`节介绍的计数算法要求保证**返回的值**在read_count()执行前一刻的计数值和read_count()执行完毕时的计数值之间。`5.2.3`提供了一种弱一些的保证:不调用inc_count()时,调用read_count()最终会返回正确的值。 +```c +/* 定义每线程变量 */ +DEFINE_PER_THREAD(unsigned long, counter); + struct { + __typeof__(unsigned long) v __attribute__((__aligned__(CACHE_LINE_SIZE))); + } __per_thread_counter[NR_THREADS]; + +/* 定义全局counter */ +unsigned long global_count; + +count_init + eventual + while(stopflag < 3) { + unsigned long sum = 0; + for_each_thread(t) sum += READ_ONCE(per_thread(counter, t)); + WRITE_ONCE(global_count, sum); + poll(NULL, 0, 1); + if (READ_ONCE(stopflag)) { + smp_mb(); + WRITE_ONCE(stopflag, stopflag + 1); + } + } + +count_cleanup + stopflag = 1 + +static __inline__ unsigned long read_count(void) +{ + return READ_ONCE(global_count); +} +``` +若`count_cleanup`中置位`stopflag`,`eventual`最后计算`3`次后退出。 +否则,`eventual`一直在计算`sum`并写入`global_count`中。 +对比`5.2.2`中的实现,相当于引入了一个变量`global_count`来做缓冲,每次读的时候只读`global_count`。 + +### 5.2.4 Per-Thread-Variable-Based Implementation + +代码路径:`count\count_end.c` + +```c +/* 使用gcc提供的一个用于每线程存储的_thread存储类 */ +unsigned long __thread counter = 0; //\lnlbl{var:b} +/* 定义数组存放各个线程变量的地址 */ +unsigned long *counterp[NR_THREADS] = { NULL }; +/* 定义全局counter */ +unsigned long finalcount = 0; +/* 全局自旋锁 */ +DEFINE_SPINLOCK(final_mutex); + +/* 自增线程内的计数变量counter */ +static __inline__ void inc_count(void) +{ + WRITE_ONCE(counter, counter + 1); +} + +static __inline__ unsigned long read_count(void) +{ + int t; + unsigned long sum; + + spin_lock(&final_mutex); + sum = finalcount; + for_each_thread(t) + if (counterp[t] != NULL) + sum += READ_ONCE(*counterp[t]); + spin_unlock(&final_mutex); + return sum; +} + +/* 在自旋锁的保护下初始化全局数组,实际上这个自旋锁可以去掉, + * 因为这里访问的位置对各个线程来说是原子独占的, + * 这里只是为了安全起见保留了。 */ +void count_register_thread(unsigned long *p) +{ + int idx = smp_thread_id(); + + spin_lock(&final_mutex); + counterp[idx] = &counter; + spin_unlock(&final_mutex); +} + +/* 在自旋锁的保护下做全局counter的累加,排除了有进程调用 read_count()同时又有进程调用 + * count_unregister_thread()的情况。*/ +void count_unregister_thread(int nthreadsexpected) +{ + int idx = smp_thread_id(); + + spin_lock(&final_mutex); + finalcount += counter; + counterp[idx] = NULL; + spin_unlock(&final_mutex); +} + +``` + +> 小问题5.23:为什么我们需要用像互斥锁这种重量级的手段来保护5.9的read_count()函数中的累加总和操作? + +当一个线程退出时,它的每线程变量将会消失。因此,如果我们试图在线程退出后,访问它的每线程变量,将得到一个段错误。这个锁保护求和操作和线程退出以防止出现这种情况。 + +`count_register_thread()`函数,每个线程在访问自己的计数前都要调用它。 +`count_unregister_thread()`函数,每个之前调用过`count_register_thread()`函数的线程在退出时都需要调用该函数。 + +> **评价**:这个方法让更新者的性能几乎和非原子计数一样,并且也能线性地扩展。另一方面,并发的读者竞争一个全局锁,因此性能不佳,扩展能力也很差。但是,**这不是统计计数器需要面对的问题**,因为统计计数器总是在增加计数,很少读取计数。 + +搞明白问题的范围很重要。 + +> 小问题5.25:很好,但是Linux内核在读取每CPU计数的总和时没有用锁保护。为什么用户态的代码需要这么做? + +一个规避办法是确保在所有线程结束以前,每一个线程都被延迟,如代码`D.2`所示。 + +## 5.3 Approximate Limit Counters +### 5.3.1 Design +每个线程都拥有一份每线程变量`counter`,但同时每个线程也持有一份每线程的最大值`countermax`。 +举个例子,假如某个线程的counter和countermax现在都等于10,那么我们会执行如下操作。 +``` +1.获取全局锁。 +2.给globalcount增加5。 +3.当前线程的counter减少5,以抵消全局的增加。 +4.释放全局锁。 +5.增加当前线程的counter,变成6。 +``` +> 这就提出了一个问题,我们到底有多在意`globalcount`和真实计数值的偏差? +其中真实计数值由`globalcount`和所有每线程变量`counter`相加得出。 + +这个问题的答案取决于真实计数值和计数上限(这里我们把它命名为`globalcountmax`)的差值有多大。这两个值的差值越大,`countermax`就越不容易超过`globalcountmax`的上限。这就代表着任何一个线程的`countermax`变量可以根据当前的差值计算取值。当离上限还比较远时,可以给每线程变量`countermax`赋值一个比较大的数,这样对性能和扩展性比较有好处。当靠近上限时,可以给这些`countermax`赋值一个较小的数,这样可以降低超过统计上限`globalcountmax`的风险。 +> 关于这个设计有如下问题: +差值如何获取,以怎样的频率获取?在获取这个差值的时候是需要真实计数值的,这个过程需要做一个多线程的`read_count`操作,如果这个过程比较频繁,如`1ms`一次,那么这个设计就变成了[5.2.3](###%205.2.3%20Eventually%20Consistent%20Implementation)。 + +从作者的实现中可以看到:在估计`countermax`时使用的数据结构是全局数据结构`globalcount`和`globalreserve`,而不是线程变量`counter`。所以不存在上述的问题。 +```c +countermax = globalcountmax - globalcount - globalreserve; +countermax /= num_online_threads(); +``` + +数据结构如下所示: + +![image](https://user-images.githubusercontent.com/21327882/68528446-c57df780-032d-11ea-8872-16715e94d713.png) + +```c +unsigned long __thread counter = 0; +unsigned long __thread countermax = 0; +unsigned long globalcountmax = 10000; +unsigned long globalcount = 0; +unsigned long globalreserve = 0; +unsigned long *counterp[NR_THREADS] = { NULL }; +DEFINE_SPINLOCK(gblcnt_mutex); +``` +每线程变量`counter`和`countermax`各自对应线程的本地计数和计数上限。 +`globalcountmax`变量代表合计计数的上限。 +`globalcount`变量是全局计数。 +`globalcount`和每个线程的`counter`之和就是真实计数值。 +`globalreserve`变量是所有每线程变量`countermax`的和。 + +### 5.3.2 Simple Limit Counter Implementation + +代码路径:`count\count_lim.c` + +数据结构图如下所示: + +![image](https://user-images.githubusercontent.com/21327882/68537502-18909280-03a0-11ea-9fa7-188d5b598864.png) + +关键算法如下所示: +```c +add_count + /* fast path:本线程资源充足,直接count += dalta + * 如果线程资源有余,直接分配 + * 下面的判断为什么不写成 if(count + data <= countermax) + * 因为这种判断有整型溢出的隐患 + */ + if (countermax - counter >= delta) { + WRITE_ONCE(counter, counter + delta) + return 1; + } + + /* slow path:此时线程内资源不够用了 */ + spin_lock(&gblcnt_mutex); + globalize_count(); + /* 清除线程本地变量,根据需要调整全局变量,这就简化了全局处理过程。 */ + globalcount += counter; + counter = 0; + globalreserve -= countermax; + countermax = 0; + + if (globalcountmax - + globalcount - globalreserve < delta) + spin_unlock(&gblcnt_mutex); + return 0; + } + globalcount += delta; + balance_count(); + /* 动态调整当前线程的资源阈值,并且将counter设置成新阈值的一半 + * 好处: + * (1)允许线程0使用快速路径来减少及增加计数 + * (2)如果全部线程都单调递增直到上限,则可以减少不准确程度。 + */ + countermax = globalcountmax - + globalcount - globalreserve; + countermax /= num_online_threads() + globalreserve += countermax; + counter = countermax / 2; + if (counter > globalcount) + counter = globalcount; + globalcount -= counter; + + spin_unlock(&gblcnt_mutex); + return 1; + +sub_count + /* 基本同`add_count` */ +``` + +### 5.3.3 Simple Limit Counter Discussion +当合计值接近0时,简单上限计数运行得相当快,只在add_count()和sub_count()的快速路径中的比较和判断时存在一些开销。但是,每线程变量`countermax`的使用表明add_count()即使在计数的合计值离`globalcountmax`很远时也可能失败。同样,sub_count()在计数合计值远远大于0时也可能失败。 + +在许多情况下,这都是不可接受的。即使`globalcountmax`只不过是一个近似的上限,一般也有一个近似度的容忍限度。一种限制近似度的方法是对每线程变量`countermax`的值强加一个上限。这个任务将在5.3.4节完成。 + +### 5.3.4 Approximate Limit Counter Implementation + +代码路径:`count\count_lim_app.c` + +在更新数据结构的时候添加一个上限`MAX_COUNTERMAX`的判断 +```c +static void balance_count(void) +{ + countermax = globalcountmax - + globalcount - globalreserve; + countermax /= num_online_threads(); + if (countermax > MAX_COUNTERMAX) + countermax = MAX_COUNTERMAX; + globalreserve += countermax; + counter = countermax / 2; + if (counter > globalcount) + counter = globalcount; + globalcount -= counter; +} +``` +### 5.3.5 Approximate Limit Counter Discussion +- 极大地减小了在前一个版本中出现的上限不准确程度 +- 任何给定大小的`MAX_COUNTERMAX`将导致一部分访问无法进入快速路径,这部分访问的数目取决于工作负荷。 + +### 5.4 Exact Limit Counters +使用原子操作保证精确性 + +### 5.4.1 Atomic Limit Counter Implementation: + +代码路径:`count\count_lim_atomic.c` + +- 数据结构 +```c +atomic_t __thread counterandmax = ATOMIC_INIT(0); +unsigned long globalcountmax = 1 << 25; +unsigned long globalcount = 0; +unsigned long globalreserve = 0; +atomic_t *counterp[NR_THREADS] = { NULL }; +DEFINE_SPINLOCK(gblcnt_mutex); +#define CM_BITS (sizeof(atomic_t) * 4) +#define MAX_COUNTERMAX ((1 << CM_BITS) - 1) +``` + +- 核心逻辑 +```c +int add_count(unsigned long delta) +{ + int c; + int cm; + int old; + int new; + /* CAS操作,因为counterandmax可能被多线程并发访问(其他线程做flush_local_count操作时) */ + do { + split_counterandmax(&counterandmax, &old, &c, &cm); + if (delta > MAX_COUNTERMAX || c + delta > cm) + goto slowpath; + new = merge_counterandmax(c + delta, cm); + } while (atomic_cmpxchg(&counterandmax, old, new) != old); + return 1; +slowpath: + spin_lock(&gblcnt_mutex); + globalize_count(); + // 使用原子接口来对数据结构清零:atomic_set(&counterandmax, old); + if (globalcountmax - globalcount - + globalreserve < delta) { + flush_local_count(); + // 清空每个线程的counterandmax数据结构 + // 将其merge到全局数据结构`globalcount`和`globalreserve` + /* 回收其他线程资源之后再次判断,这个方案的核心所在,这里是它更精确的地方。 */ + if (globalcountmax - globalcount - + globalreserve < delta) { + spin_unlock(&gblcnt_mutex); + return 0; + } + } + globalcount += delta; + balance_count(); + spin_unlock(&gblcnt_mutex); + return 1; +} +``` + +>小问题5.36:为什么一定要将线程的`coutner`和`countermax`变量作为一个整体`counterandmax`同时改变?分别改变它们不行吗? + +原子地将`countermax`和`counter`作为单独的变量进行更新是可行的,但很显然需要非常小心。也是很可能这样做会使快速路径变慢。 + +> 小问题5.40:图5.18第11行那个丑陋的`goto slowpath;`是干什么用的?你难道没听说break语句吗? + +使用break替换goto语句将需要使用一个标志,以确保在第15行是否需要返回,而这并不是在快速路径上所应当干的事情。分支判断会减慢`fastpath`的速度。 + +> 小问题5.43:当图5.20中第27行的flush_local_count()在清零counterandmax变量时,是什么阻止了atomic_add()或者atomic_sub()的快速路径对counterandmax变量的干扰? + +答案:什么都没有。考虑以下三种情况。 + +```c +int add_count(unsigned long delta) +{ + int c; + int cm; + int old; + int new; + /* CAS操作,因为counterandmax被多线程并发访问 */ + do { + split_counterandmax(&counterandmax, &old, &c, &cm); + if (delta > MAX_COUNTERMAX || c + delta > cm) + goto slowpath; + new = merge_counterandmax(c + delta, cm); + } while (atomic_cmpxchg(&counterandmax, + old, new) != old); + return 1; + …… +} + +static void flush_local_count(void) +{ + zero = merge_counterandmax(0, 0); + for_each_thread(t) + if (counterp[t] != NULL) { + old = atomic_xchg(counterp[t], zero + split_counterandmax_int(old, &c, &cm + globalcount += c; + globalreserve -= cm; + } + …… +} +``` +1.如果flush_local_count()的atomic_xchg()在split_counterandmax()之前执行,那么快速路径将看到计数器和counterandmax为0,因此转向慢速路径。 + +2.如果flush_local_count()的atomic_xchg()在split_counterandmax()之后执行,但是在快速路径的atomic_cmpxchg()之前执行,那么atomic_cmpxchg()将失败,导致快速路径重新运行,这将退化为第1种情况。 + +3.如果flush_local_count()的atomic_xchg()在split_counterandmax()之后执行,快速路径在flush_local_count()清空线程counterandmax变量前,它将成功运行。 + +不论哪一种情况,竞争条件将被正确解决。 + +### 5.4.2 Atomic Limit Counter Discussion +快速路径上原子操作的开销,让快速路径明显变慢了。 +可以通过使用信号处理函数从其他线程窃取计数来改善这一点。 +> 小问题5.45:但是信号处理函数可能会在运行时迁移到其他CPU上执行。难道这种情况就不需要原子操作和内存屏障来保证线程和中断线程的信号处理函数之间通信的可靠性了吗? + +答案:不需要。如果信号处理过程被迁移到其他CPU,那么被中断的线程也会被迁移。 + +### 5.4.3 Signal-Theft Limit Counter Design +![image](https://user-images.githubusercontent.com/21327882/68540752-ffa0d500-03d1-11ea-87cb-46ed8369c4e9.png) + +`IDLE`->`REQ`:若本线程的`theftp`被其他线程置成了`THEFT_REQ`,则说明其他线程的本地计数资源不够用了,请求刷新当前本地计数去支援。 + +### 5.4.4 Signal-Theft Limit Counter Implementation + +代码路径:`count\count_lim_sig.c` + +> [信号使用示例](https://www.cnblogs.com/52php/p/5813867.html) + +数据结构: +```c +int __thread theft = THEFT_IDLE; +int __thread counting = 0; +unsigned long __thread counter = 0; +unsigned long __thread countermax = 0; +unsigned long globalcountmax = 10000; +unsigned long globalcount = 0; +unsigned long globalreserve = 0; +/* 每个线程的允许远程访问的counter变量 */ +unsigned long *counterp[NR_THREADS] = { NULL }; +/* 每个线程的允许远程访问的countermax变量 */ +unsigned long *countermaxp[NR_THREADS] = { NULL }; +/* 每个线程的允许远程访问的theft变量 */ +int *theftp[NR_THREADS] = { NULL }; +DEFINE_SPINLOCK(gblcnt_mutex); +#define MAX_COUNTERMAX 100 +``` + +信号量通信过程: +```c +count_init + sa.sa_handler = flush_local_count_sig +``` +count_init()设置flush_local_count_sig()为SIGUSR1的信号处理函数,让flush_local_count()中的pthread_kill()可以调用flush_local_count_sig()。 +```c +add_count + /* fastpath */ + counting = 1; + if (theft == THEFT_IDLE | THEFT_REQ && countermax-counter >= delta) count += delta; + counting = 0; + if (theft == THEFT_ACK) theft = THEFT_READY; + + /* slow path */ + /* when the combination of the local thread’s count and the g`lobal count cannot accommodate the request*/ + flush_local_count + /* 发请求刷新所有线程的本地计数 */ + if (*countermaxp[t] == 0) *theftp[t] = THEFT_READY); + WRITE_ONCE(*theftp[t], THEFT_REQ); + pthread_kill(tid, SIGUSR1); + // 信号接收侧进程收到信号之后 + flush_local_count_sig + if (theft != THEFT_REQ) + /* the signal handler is not permitted to change the state. */ + return; + theft = THEFT_ACK; + if (!counting) theft = THEFT_READY; + /* 循环等待每个线程达到READY状态,然后窃取线程的计数 */ + 更新全局数据结构 + *theftp[t] = THEFT_IDLE; +``` +### 5.4.5 Signal-Theft Limit Counter Discussion +对于使用信号量的算法来说,更新端的性能提升是以读取端的高昂开销为代价的,POSIX信号不是没有开销的。 +原子操作的性能随着不同的计算机体系的不同会有所差异,如果考虑最终的性能,你需要在实际部署应用程序的系统上测试这两种手段。 + +## 5.5 Applying Specialized Parallel Counters +对于`IO`操作可以考虑如下模型: +- 执行`IO`操作 +```c +read_lock(&mylock); +if (removing) { + read_unlock(&mylock); + cancel_io(); +} else { + add_count(1); + read_unlock(&mylock); + do_io(); + sub_count(1); +} +``` + +- 移除`IO`设备 +```c +write_lock(&mylock); +removing = 1; +sub_count(mybias); +write_unlock(&mylock); +while (read_count() != 0) { + poll(NULL, 0, 1); +} +remove_device(); +``` + +## 5.6 Parallel Counting Discussion +### 5.6.1 Parallel Counting Performance +![image](https://user-images.githubusercontent.com/21327882/68940952-81806c00-07df-11ea-9d7c-489345eeb649.png) + +![image](https://user-images.githubusercontent.com/21327882/68940962-88a77a00-07df-11ea-962b-c11573e765a3.png) + + +### 5.6.2 Parallel Counting Specializations +只需要简单地铺一块木板,就是一座让人跨过小溪的桥。但你不能用一块木板横跨哥伦比亚河数千米宽的出海口,也不能用于承载卡车的桥。简而言之,桥的设计必须随着跨度和负载而改变,并且,软件也要能适应硬件或者工作负荷的变化,能够自动进行最好。 + +### 5.6.3 Parallel Counting Lessons +1.分割能够提升性能和可扩展性。 + +2.部分分割,也就是只分割主要情况的代码路径,性能也很出色。 + +3.部分分割可以应用在代码上(5.2节的统计计数器只分割了写操作,没有分割读操作),但是也可以应用在时间上(5.3节和5.4节的上限计数器在离上限较远时运行很快,离上限较按时运行变慢)。 + +4.读取端的代码路径应该保持只读,内存伪共享严重降低性能和扩展性,就像在表5.1的count_end.c中一样。 +> 伪共享:在同一块缓存线中存放多个互相独立且被多个CPU访问的变量。当某个CPU改变了其中一个变量的值时,迫使其他CPU的本地高速缓存中对应的相同缓存线无效化。这种工程实践会显著地限制并行系统的可扩展性。 + +5.经过审慎思考后的延迟处理能够提升性能和可扩展性,见5.2.3节。 + +6.并行性能和可扩展性通常是跷跷板的两端,到达某种程度后,对代码的优化反而会降低另一方的表现。表5.1中的count_stat.c和count_end_rcu.c展现了这一点。 + +7.对性能和可扩展性的不同需求,以及其他很多因素,会影响算法、数据结构的设计。图5.3展现了这一点,原子增长对于双CPU的系统来说完全可以接受,但对8核系统来说就完全不合适。 + +提高并行编程性能的`3`大主要方法: + +![image](https://user-images.githubusercontent.com/21327882/68942100-91e61600-07e2-11ea-8bc2-de844755c92c.png) + +针对并行编程的4个主要部分,可还是如下方法来优化。 +- Batch +- Weaken +- Partition +- Special-purpose hardware diff --git a/_posts/2021-07-30-perfbook-c7.md b/_posts/2021-07-30-perfbook-c7.md new file mode 100644 index 0000000..dbe3395 --- /dev/null +++ b/_posts/2021-07-30-perfbook-c7.md @@ -0,0 +1,699 @@ +--- +title: perfbook-c7 +layout: post +categories: perfbook +tags: perfbook 并行编程 书籍 锁 +excerpt: perfbook-c7 +--- +# 7 Locking +1.很多因锁产生的问题大都在设计层面就可以解决,而且在大多数场合工作良好,比如 + +(a)使用锁层级避免死锁。 + +(b)使用死锁检测工具,比如Linux内核lockdep模块[Cor06]。 + +(c)使用对锁友好的数据结构,比如数组、哈希表、基树,第10章将会讲述这些数据结构。 + +2.有些锁的问题只在竞争程度很高时才会出现,一般只有不良的设计才会让锁竞争如此激烈。 + +3.有些锁的问题可以通过其他同步机制配合锁来避免。包括统计计数(第5章)、引用计数(9.1节)、危险指针(9.1.2节)、顺序锁(9.2节)、RCU(9.3节),以及简单的非阻塞数据结构(14.3节)。 + +4.直到不久之前,几乎所有的共享内存并行程序都是闭源的,所以多数研究者很难知道业界的实践解决方案。 + +## 7.1 Staying Alive + +### 7.1.1 Deadlock +![MTToPH.png](https://s2.ax1x.com/2019/11/22/MTToPH.png) + +从锁指向线程的箭头表示线程持有了该锁,比如,线程B持有锁2和锁4。从线程到锁的箭头表示线程在等待这把锁,比如,线程B等待锁3释放。 + +在图7.3中,死锁循环是线程B、锁3、线程C、锁4,然后又回到线程B。 + +从死锁中恢复的办法: +- 杀掉其中一个线程 +- 从某个线程中偷走一把锁 + +下面介绍避免死锁的策略: + +#### 7.1.1.1 Locking Hierarchies +按层次使用锁时要为锁编号,严禁不按顺序获取锁。 + +线程B违反这个层次,因为它在持有锁4时又试图获取锁3,因此导致死锁的发生。 + +#### 7.1.1.2 Local Locking Hierarchies +> 小问题:锁的层次本质要求全局性,因此很难应用在库函数上。如果调用了某个库函数的应用程序还没有开始实现,那么倒霉的库函数程序员又怎么才能遵从这个还不存在的应用程序里的锁层次呢? + +一种特殊的情况,幸运的是这也是普遍情况,是库函数并不涉及任何调用者的代码。这时,如果库函数持有任何库函数的锁,它绝不会再去获取调用者的锁,这样就避免出现库函数和调用者之间互相持锁的死锁循环。 + +> 小问题7.3:这个规则有例外吗?比如即使库函数永远不会调用调用者的代码,但是还是会出现库函数与调用者代码相互持锁的死锁循环。 + +答案:确实有!这里有几个例子。 + +1.如果库函数的参数之一是指向库函数将要获取的锁的指针,并且如果库函数在获取调用者的锁的同时还持有自身的锁,那么我们可能有一个涉及调用者和库函数的锁的死锁循环。 + +2.如果其中一个库函数返回一个指向锁的指针,调用者获取了该锁,如果调用者在持有库的锁的同时获得自身的锁,我们可以再次有一个涉及调用者和库函数的锁的死锁循环。 + +3.如果其中一个库函数获取锁,然后在返回仍然持有该锁,如果调用者获取了自身的锁,我们又发明了一种产生死锁循环的好办法。 + +4.如果调用者有一个获取锁的信号处理程序,那么死锁循环就会涉及调用者和库函数的锁。不过在这种情况下,库函数的锁是死锁循环中无辜的旁观者。也就是说,在大多数环境中从信号处理程序内获取锁的想法是个馊主意,它不只是一个坏主意,它也是不受支持的用法。 + +不过假设某个库函数确实调用了调用者的代码。比如,qsort()函数调用了调用者提供的比较函数。并发版本的qsort()通常会使用锁,虽然不大可能,但是如果比较函数复杂并且也使用了锁,那么就有可能出现死锁。这时库函数该如何避免死锁? + +出现这种情况时的黄金定律是“**在调用未知代码前释放所有的锁**”。为了遵守这条定律,qsort()函数必须在调用比较函数前释放它持有的全部锁。 +下面举例说明这条黄金定律: +![MTLcBq.png](https://s2.ax1x.com/2019/11/22/MTLcBq.png) + +如图`7.4`所示,其调用关系如下图所示: +``` +foo() { + hold lock A + qsort(cmp_foo) +} + +bar() { + hold lock B + qsort(cmp_bar) +} + +qsort(fun *cmp) { + hold lock C + *cmp(); +} + +cmp_foo() { + hold lock B + // do cmp things +} + +``` +现在假设如下时序,则进入死锁状态: +``` +thread 1 | thread 2 +foo | + hold lock A | bar + qsort(cmp_foo) | hold lock B + hold lock C | qsort(cmp_bar) + cmp_foo(); | try holding lock C + try holding lock B | // can’t get lock C + // cannot get lock B ,waiting | // waiting! + // Dead lock now! | +``` + + +图`7.5`在调用外部未知函数`cmp_foo`的时候参照黄金定律释放了`Lock C`,避免了死锁问题。 + +> 小问题7.4:但是如果qsort()在调用比较函数之前释放了所有锁,它怎么保护其他qsort()线程也可能访问的数据? + +答案:通过私有化将要比较的数据元素(如第8章所述)或通过使用延迟处理机制,如引用计数(如第9章所述)。 + +#### 7.1.1.3 Layered Locking Hierarchies +> 小问题:如果qsort()无法在调用比较函数前释放全部的锁,怎么办? + +见下面例1的解决办法。 + +**例1** + +可以使用**锁的分级**来避免死锁问题,思考图`7.4`带来的问题,因为在`cmp`函数中使用了与`bar`函数相冲突的锁导致了问题,为了改变这一点,我们在`cmp`函数中使用新的锁`D`,这样我们就需要把应用层的锁`A/B`和`D`放在不同的层级,从而使他们相互隔离,没有产生冲突的机会。 + +![MTLov9.png](https://s2.ax1x.com/2019/11/22/MTLov9.png) + +如图`7.6`所示,在这张图上,我们把全局层次锁分成了三级,第一级是锁A和锁B,第二级是锁C,第三级是锁D。 + +**例2:链表迭代器** + +这是另一个无法在调用未知代码时释放所有锁的例子。 +```c +struct locked_list { + spinlock_t s; + struct list_head h; +}; + +/*获取了链表的锁并且返回了第一个元素(假如链表不为空)*/ +struct list_head *list_start(struct locked_list *lp) +{ + /* 获取链表的锁 */ + spin_lock(&lp->s); + return list_next(lp, &lp->h); +} + +/* 要么返回指向下一个元素的指针,要么在链表到达末端时返回NULL */ +struct list_head *list_next(struct locked_list *lp, + struct list_head *np) +{ + struct list_head *ret; + + ret = np->next; + if (ret == &lp->h) { + /* 链表达到末端时释放链表的锁 */ + spin_unlock(&lp->s); + ret = NULL; + } + return ret; +} + +``` + +链表迭代器的一种使用场景如下: +```c +struct list_ints { + struct list_head n; + int a; +}; + +void list_print(struct locked_list *lp) +{ + struct list_head *np; + struct list_ints *ip; + + np = list_start(lp); + while (np != NULL) { + ip = list_entry(np, struct list_ints, n); + printf("\t%d\n", ip->a); + np = list_next(lp, np); + } +} + +``` +只要用户代码在处理每个链表元素时,不要再去获取已经被其他调用list_start()或者list_next()的代码持有的锁——这会导致死锁——那么锁在用户代码里就可以继续保持隐藏。 + +> 我的小问题:自旋锁如何造成死锁? + +答案见[此](https://blog.csdn.net/gl_zyc/article/details/38389901)所示,说明了自旋锁造成死锁的场景以及现代处理器以及操作系统中如何避免这种情况的出现。 + +----------- + +我们在给链接迭代器加锁时通过为锁的层次分级,就可以避免死锁的发生。 + +这种分级的方法可以扩展成任意多级,但是每增加一级,锁的整体设计就复杂一分。这种复杂性的增长对于某些面向对象的设计来说极不方便,会导致一大堆对象将锁毫无纪律地传来传去。这种面向对象的设计习惯和避免死锁的需要之间的冲突,也是一些程序员们认为并行编程复杂的重要原因。 + + +#### 7.1.1.4 Locking Hierarchies and Pointers to Locks + +一般来说设计一个包含着指向锁的指针的API意味着这个设计本身就存在问题。 + +将内部的锁传递给其他软件组件违反了信息隐藏的美学,而信息隐藏恰恰是一个关键的设计准则。 + +> 我的小问题:从这个角度来说,锁的层层封装是否是不合理的? + +> 小问题7.5:请列举出一个例子,说明将指向锁的指针传给另一个函数的合理性。 + +例子1:Locking primitives +例子2:比如说有个函数要返回某个对象,而在对象成功返回之前必须持有调用者提供的锁。 +例子3:POSIX的pthread_cond_wait()函数,要传递一个指向pthread_mutex_t的指针来防止错过唤醒而导致的挂起。 + +#### 7.1.1.5 Conditional Locking +```c +spin_lock(&lock2); +layer_2_processing(pkt); +nextlayer = layer_1(pkt); +//报文在协议栈中从下往上发送时,这里的获取锁操作将导致死锁。 +spin_lock(&nextlayer->lock1); +layer_1_processing(pkt); +spin_unlock(&lock2); +spin_unlock(&nextlayer->lock1); +``` + +在这个例子中,当报文在协议栈中从上往下发送时,必须逆序获取下一层的锁。而报文在协议栈中从下往上发送时,是按顺序获取锁,图中第4行的获取锁操作将导致死锁。 +> 我的小问题:这里是如何导致死锁的? + +答案: +从下面的解决方案来看,是`spin_lock(&nextlayer->lock1)`这个操作的失败导致了死锁。 + +`spin_lock(&nextlayer->lock1)`失败的原因是`nextlayer->lock1`在从下往上发送时已经被`layer_1`持有了,所以这里会失败进入自旋等待,而此时`layer_1`也尝试获取`layer_2`的锁,所以造成了死锁。 + +此时获取自旋锁的正确的顺序应该是先获取`layer_1`的锁在获取`layer_2`的锁。 + +而在这个引发死锁的场景下,先获取了`layer_2`的锁之后再去获取已经被`layer_1`抢占了的`lock1`(比如说这个锁可能被`layer_1`的某个中断处理程序中先获取了),导致失败。 + +----------------- + +> 我的小问题:通常情况下,自旋锁如何造成死锁?在内核中遇到此类的问题该如何定位? + +[一次spinlock死锁故障的定位](https://blog.csdn.net/lqxandroid2012/article/details/53581076) +[单CPU自旋锁导致死锁的问题.](https://blog.csdn.net/gl_zyc/article/details/38389901) + +在本例中避免死锁的办法是先强加一套锁的层次,但在必要时又可以有条件地乱序获取锁,如下代码所示,与上面无条件的获取层1的锁不同,第5行用`spin_trylock`有条件地获取锁。该原语在锁可用时立即获取锁,在锁不可用时不获取锁,返回0。如下所示: + +```c +retry: +spin_lock(&lock2); +layer_2_processing(pkt); +nextlayer = layer_1(pkt); +if (!spin_trylock(&nextlayer->lock1)) { + spin_unlock(&lock2); + spin_lock(&nextlayer->lock1); + spin_lock(&lock2); + if (layer_1(pkt) != nextlayer) { + spin_unlock(&nextlayer->lock1); + spin_unlock(&lock2); + goto retry; + } +} +layer_1_processing(pkt); +spin_unlock(&lock2); +spin_unlock(&nextlayer->lock1); +``` + +如果`if (!spin_trylock(&nextlayer->lock1)`获取成功,处理流程同以上的代码。 + +如果获取失败,第`6`行释放锁,第`7`行和第`8`行用正确的顺序获取锁。 + +系统中可能有多块网络设备(比如 Ethernet 和WiFi),这样`layer_1`函数必须进行路由选择。这种选择随时都可能改变,特别是系统可移动时。所以第9行必须重新检查路由选择,如果发生改变,必须释放锁重新来过。 + +#### 7.1.1.6 Acquire Needed Locks First +条件锁有一个重要的特例,在执行真正的处理工作之前,已经拿到了所有必需的锁。 +在这种情况下,处理不需要是幂等的(idempotent):如果这时不能在不释放锁的情况下拿到某把锁,那么释放所有持有的锁,重新获取。只有在持有所有必需的锁以后才开始处理工作。 + +#### 7.1.1.7 Single-Lock-at-a-Time Designs +如果有一个可以完美分割的问题,每个分片拥有一把锁。然后处理任何特定分片的线程只需获得对应这个分片的锁。 + +#### 7.1.1.8 Signal/Interrupt Handlers +> The trick is to block signals (or disable interrupts, as the case may be) when acquiring any lock that might be acquired within an interrupt handler. Furthermore, if holding such a lock, it is illegal to attempt to acquire any lock that is ever acquired outside of a signal handler without blocking signals. + +其中的诀窍是在任何可能中断处理函数里获取锁的时候阻塞信号(或者屏蔽中断)。不仅如此,如果已经获取了锁,那么在不阻塞信号的情况下,尝试去获取任何可能在中断处理函数之外被持有的锁,这种操作都是非法操作。 + +> 小问题7.10:如果锁A在信号处理函数之外被持有,锁B在信号处理函数之内被持有,为什么在不阻塞信号并且持有锁B的情况下去获取锁A是非法的? + +答案:因为这会导致死锁。假定锁A在信号处理函数之外被持有,并且在持有时没有阻塞该信号,那么有可能在持有锁时处理该信号。相应的信号处理函数可能再去获取锁B,从而同时持有锁A和锁B。因此,如果我们还想在持有锁B的同时获取锁A,将有一个死锁循环等着我们。 + +因此,在不阻塞信号的情况下,如已经持有了从信号处理函数内获取的锁,从信号处理程序外部获取另一把锁是非法的。 + +----------------- + +> 小问题7.11:在信号处理函数里怎样才能合法地阻塞信号? + +答案:最简单和最快速的方法之一是在设置信号时将sa_mask字段传递给sigaction()的struct sigaction。 + +----------------- + +### 7.1.2 Livelock and Starvation +虽然条件锁是一种有效避免死锁机制,但是有可能被滥用。 +```c +void thread1(void) +{ +retry: + spin_lock(&lock1); + do_one_thing(); + if (!spin_trylock(&lock2)) { + spin_unlock(&lock1); + goto retry; + } + do_another_thing(); + spin_unlock(&lock2); + spin_unlock(&lock1); +} + +void thread2(void) +{ +retry: + spin_lock(&lock2); + do_a_third_thing(); + if (!spin_trylock(&lock1)) { + spin_unlock(&lock2); + goto retry; + } + do_a_fourth_thing(); + spin_unlock(&lock1); + spin_unlock(&lock2); +} +``` +1.第4行线程1获取lock1,然后调用do_one_thing()。 + +2.第18行线程2获取lock2,然后调用do_a_third_thing()。 + +3.线程1在第6行试图获取lock2,由于线程2已经持有而失败。 + +4.线程2在第20行试图获取lock1,由于线程1已经持有而失败。 + +5.线程1在第7行释放lock1,然后跳转到第3行的retry。 + +6.线程2在第21行释放lock2,然后跳转到第17行的retry。 + +7.上述过程不断重复,活锁华丽登场。 + +**解决办法:** +活锁和饥饿都属于事务内存软件实现中的严重问题,所以现在引入了竞争管理器这样的概念来封装这些问题。以锁为例,通常简单的指数级后退就能解决活锁和饥饿。 + +指数级后退是指在每次重试之前增加按指数级增长的延迟,如下所示: +```c +void thread1(void) +{ + unsigned int wait = 1; + retry: + spin_lock(&lock1); + do_one_thing(); + if (!spin_trylock(&lock2)) { + spin_unlock(&lock1); + sleep(wait); + wait = wait << 1; + goto retry; + } + do_another_thing(); + spin_unlock(&lock2); + spin_unlock(&lock1); +} + +void thread2(void) +{ + unsigned int wait = 1; + retry: + spin_lock(&lock2); + do_a_third_thing(); + if (!spin_trylock(&lock1)) { + spin_unlock(&lock2); + sleep(wait); + wait = wait << 1; + goto retry; + } + do_a_fourth_thing(); + spin_unlock(&lock1); + spin_unlock(&lock2); +} + +``` + +1.第4行线程1获取lock1,然后调用do_one_thing()。 + +2.第18行线程2获取lock2,然后调用do_a_third_thing()。 + +3.线程1在第6行试图获取lock2,由于线程2已经持有而失败。 + +4.线程2在第20行试图获取lock1,由于线程1已经持有而失败。 + +5.线程1在第7行释放lock1,睡眠`wait`秒,然后跳转到第3行的retry。 + +6.线程2在第21行释放lock2,睡眠`wait`秒,然后跳转到第17行的retry。 + +7.上述过程不断重复,睡眠的时间不甚精确,`thread1`就可能在`thread2`睡眠的情况下醒来获取到它的锁。 + +### 7.1.3 Unfairness + +![MjSNE8.png](https://s2.ax1x.com/2019/11/25/MjSNE8.png) + +锁容易被同一个`numa`内的`CPU`们轮奸。 + +### 7.1.4 Inefficiency +> Locks are implemented using atomic instructions and memory barriers, and often involve cache misses. + +锁是由原子操作和内存屏障实现,并且常常带来高速缓存未命中。开销比较大。 + +不过一旦持有了锁,持有者可以不受干扰地访问被锁保护的代码。获取锁可能代价高昂,但是一旦持有,特别是对较大的临界区来说,CPU的高速缓存反而是高效的性能加速器。 + + +> 小问题7.17:锁的持有者怎么样才会受到干扰? + +答案:如果受锁保护的数据与锁本身处于同一高速缓存行中,那么其他CPU获取锁的尝试将导致持有锁的CPU产生昂贵的高速缓存未命中。这是伪共享的一种特殊情况,如果由不同锁保护的一对变量共享高速缓存行,也会出现这种情况。相反,如果锁和与它保护的数据处于不同的高速缓存行中,则持有锁的CPU通常只会在第一次访问给定的变量时遇到缓存未命中。 + +当然,将锁和数据放入单独的缓存行也有缺点,代码将产生两次高速缓存未命中,而不是在无竞争时的一次高速缓存未命中。 + +## 7.2 Types of Locks +### 7.2.1 Exclusive Locks +互斥锁正如其名,一次只能有一个线程持有该锁。持锁者对受锁保护的代码享有排他性的访问权。 + +> 小问题7.18:如果获取互斥锁后马上释放,也就是说,临界区是空的,这种做法有意义吗 + +互斥锁的语义有两个组成部分: +(1)大家比较熟悉的数据保护语义 +(2)消息语义,释放给定的锁将通知等待获取那个锁的线程。 + +空临界区的做法使用了消息传递的语义,而非数据保护语义。 + +### 7.2.2 Reader-Writer Locks + +读/写锁和互斥锁允许的规则不大相同:互斥锁只允许一个持有者,读/写锁允许任意多个持有者持有读锁(但只能有一个持有写锁)。 + +**实现方式一:** + +经典的读/写锁实现使用一组只能以原子操作方式修改的计数和标志。 + +这种实现和互斥锁一样,对于很小的临界区来说开销太大,获取和释放锁的开销比一条简单指令的开销高两个数量级。然,如果临界区足够长,获取和释放锁的开销与之相比就可以忽略不计了。如下图所示为原子操作的开销。 + +![MjVNyF.png](https://s2.ax1x.com/2019/11/25/MjVNyF.png) + +> 实现锁的开销和临界区中获得的收益需要权衡考虑。 + +**实现方式二:** + +另一个设计读/写锁的方法是使用每线程的互斥锁,这种读/写锁对读者非常有利。线程在读的时候只要获取本线程的锁即可,而在写的时候需要获取所有线程的锁。在没有写者的情况下,每个读锁的开销只相当于一条原子操作和一个内存屏障的开销之和,并且不会有高速缓存未命中,这点对于锁来说非常不错。不过,写锁的开销包括高速缓存未命中,再加上原子操作和内存屏障的开销之和——再乘以线程的个数。 + +简单地说,读/写锁在有些场景非常有用,但各种实现方式都有各自的缺点。**读/写锁的正统用法是用于非常长的只读临界区,临界区耗时几百微秒或者毫秒以上最好。** + +### 7.2.3 Beyond Reader-Writer Locks +锁可能的允许规则有很多,`VAX/VMS`分布式锁管理器就是其中一个例子,如表`7.1`所示。空白格表示兼容,包含`X`的格表示不兼容。 + +![MjwuXF.png](https://s2.ax1x.com/2019/11/25/MjwuXF.png) + +- null +If a thread is not holding a lock, it should not prevent any other thread from acquiring that lock. +- concurrent-read +The concurrent-read mode might be used to accumulate approximate statistics on a data structure, while permitting updates to proceed concurrently. +- concurrent write +The concurrent-write mode might be used to update approximate statistics, while still permitting reads and concurrent updates to proceed concurrently. + +可见`concurrent-read`和`concurrent write`常用于非精确计数场景,可见`5.2`中的相关内容。 + +- protected read +The protected-read mode might be used to obtain a consistent snapshot of the data structure, while permitting reads but not updates to proceed concurrently. +- protected write +The protected-write mode might be used to carry out updates to a data structure that could interfere with protected readers but which could be tolerated by concurrent readers. +- exclusive +The exclusive mode is used when it is necessary to exclude all other accesses. + +It is interesting to note that `exclusive locks` and `reader-writer locks` can be emulated by the VAX/VMS DLM. `Exclusive locks` would use only the `null` and `exclusive` modes, while `reader-writer locks` might use the `null`, `protected-read`, and `protected-write` modes. + +> Quick Quiz 7.19: Is there any other way for the VAX/VMS DLM to emulate a reader-writer lock? + +Answer: +There are in fact several. One way would be to use the `null`, `protected-read`, and `exclusive modes`. Another way would be to use the `null`, `protected-read`, and `concurrent-write` modes. A third way would be to use the `null`, `concurrent-read`, and `exclusive modes`. + +相对于VAX/VMS分布式锁管理器的6个状态,有些数据库中使用的锁甚至可以有30个以上的状态。 +### 7.2.4 Scoped Locking +在面向对象编程领域,可以使用凭借构造函数和析构函数去获取和释放锁。 +> Another approach is to use the object-oriented “resource allocation is initialization” (RAII) pattern. + +好处是方便使用,坏处是让锁的很多有用的用法变得不再可能,破坏了锁的灵活性。 + +**内核中RCU的例子**: + +![MjBVMT.png](https://s2.ax1x.com/2019/11/25/MjBVMT.png) + +其算法如下所示: +```c +void force_quiescent_state(struct rcu_node *rnp_leaf) +{ + int ret; + struct rcu_node *rnp = rnp_leaf; + struct rcu_node *rnp_old = NULL; + for (; rnp != NULL; rnp = rnp->parent) { + ret = (ACCESS_ONCE(gp_flags)) || + !raw_spin_trylock(&rnp->fqslock); + if (rnp_old != NULL) + raw_spin_unlock(&rnp_old->fqslock); + if (ret) + return; + rnp_old = rnp; + } + if (!ACCESS_ONCE(gp_flags)) { + ACCESS_ONCE(gp_flags) = 1; + do_force_quiescent_state(); + ACCESS_ONCE(gp_flags) = 0; + } + raw_spin_unlock(&rnp_old->fqslock); +} +``` +此函数说明了层次锁的常见模式。 +就像之前提到的交互器封装,这个模式很难使用RAII式加锁法实现,因此在可预见的未来我们仍然需要加锁和解锁原语。 + +## 7.3 Locking Implementation Issues + +### 7.3.1 Sample Exclusive-Locking Implementation Based on Atomic Exchange + +```c +typedef int xchglock_t; +#define DEFINE_XCHG_LOCK(n) xchglock_t n = 0 + +void xchg_lock(xchglock_t *xp) +{ + while (xchg(xp, 1) == 1) { + while (*xp == 1) + continue; + } +} + +void xchg_unlock(xchglock_t *xp) +{ + (void)xchg(xp, 0); +} +``` + +> 小问题7.23:为什么需要图7.16中第7、8行的内循环呢?为什么不是简单地重复做第6行的原子交换操作? + +答案:假设锁已被一个线程持有,并且有其他几个线程尝试获取该锁。在这种情况下,如果这些线程都循环地使用原子交换操作,它们会让包含锁的高速缓存行在它们之间“乒乓”,使得芯片互联模块不堪重负。相反,如果这些线程在第7、8行上的内循环中自旋,它们将只在自己的高速缓存中自旋,对互连模块几乎没有影响。 + +> 小问题7.24:为什么不简单地在图7.16第14行的加锁语句中,将变量赋值为0? + +答案:这也是一个合法的实现,但只有当这个赋值操作之前有一个内存屏障并使用ACCESS_ONCE()时才可行。若使用xchg()操作就不需要内存屏障,因为此操作将返回一个值,这意味着它是一个完整的内存屏障。 + +> 我的小问题:x86的`xchg`指令是否自带内存屏障? + +从以上的`7.24`小问题的答案来看是的。 + +**评价:** +在锁竞争不激烈时性能好,并且具有内存占用小的优点。它避免了给不能使用它的线程提供锁,但可能会导致锁分配不公平或者甚至在锁竞争激烈时出现饥饿现象。 + +### 7.3.2 Other Exclusive-Locking Implementations +[`ticket lock`](https://en.wikipedia.org/wiki/Ticket_lock)是内核中实现的一种锁,通过先入先出的处理准则,可以解决上述`CAS`的实现方式带来的不公平问题,但是可能将锁授予给当前无法使用它的线程,并且仍然存在竞争激烈时的性能问题——原因是释放锁的线程必须更新对应的内存地址。在高度竞争时,每个尝试获取锁的线程将拥有高速缓存行的只读副本,因此锁的持有者将需要使所有此类副本无效,然后才能更新内存地址来释放锁。通常,CPU和线程越多,在高度竞争条件下释放锁时所产生的开销就越大。 + +`queued-lock`实现方式可以大大减少了在高度锁竞争时交换锁的开销,但是也增加了其在低度竞争时的开销。 +![image.png](https://rnd-isourceb.huawei.com/images/DG/20191125/31ef0b17-ea4e-4317-ba5c-5534929bbcb2/image.png) + +因此,Beng-Hong Lim和AnantAgarwal将简单的`test-and-set`锁与`queued-lock`相结合,在低度竞争时使`test-and-set`锁,在高度竞争时切换到`queued-lock`,因此得以在低度竞争时获得低开销,并在高度竞争时获得公平和高吞吐量。Browning等人采取了类似的方法,但避免了单独标志的使用,这样`test-and-set`锁的快速路径可以使用`test-and-set`实现所使用的代码。这种方法已经用于生产环境。 + +在高度锁竞争中出现的另一个问题是当锁的持有者受到延迟,特别是当延迟的原因是抢占时,这可能导致**优先级反转**。 + +解决方法是优先级继承或在持有锁时防止抢占。在防止持有锁的线程被抢占的方面,Linux中则使用了`futexes`机制实现这个功能。 + +有趣的是,在锁的实现中原子指令并不是不可或缺的部分。 +以下是一些不采用原子指令实现锁的例子: +> [A new solution of Dijkstra’s concurrent programming problem](https://lamport.azurewebsites.net/pubs/bakery.pdf) +[Solution of a Problem in Concurrent Programming Control](https://www.di.ens.fr/~pouzet/cours/systeme/bib/dijkstra.pdf) + +在Herlihy和Shavit的教科书中可以找到一种锁的漂亮实现,只使用简单的加载和存储。 +> [The Art of Multiprocessor Programming](https://www.e-reading.club/bookreader.php/134637/Herlihy,_Shavit_-_The_art_of_multiprocessor_programming.pdf) + +提到这点的目的是,虽然这个实现没有什么实际应用,但是详细的研究这个实现将非常具有娱乐性和启发性。 + +随着越来越多的人熟悉并行硬件并且并行化越来越多的代码,我们可以期望出现更多的专用加解锁原语。不过,你应该仔细考虑这个重要的安全提示,只要可能,**尽量使用标准同步原语**。标准同步原语与自己开发的原语相比,最大的优点是标准原语通常不太容易出BUG。 + +Gamsa等人描述了一种基于令牌的机制,其中令牌在CPU之间循环。当令牌到达给定的CPU时,它可以排他性地访问由该令牌保护的任何内容。详细的论文如下所示: +> [Tornado: Maximizing Locality and Concurrency in a Shared Memory Multiprocessor Operating System](https://www.usenix.org/events/osdi99/full_papers/gamsa/gamsa.pdf) + +## 7.4 Lock-Based Existence Guarantees +并行编程的一个关键挑战是提供存在保证,使得在整个访问尝试的过程中,可以在保证该对象存在的前提下访问给定对象。在某些情况下,存在保证是隐含的。 + +1.基本模块中的全局变量和静态局部变量在应用程序正在运行时存在。 + +2.加载模块中的全局变量和静态局部变量在该模块保持加载时存在。 + +3.只要至少其中一个函数还在被使用,模块将保持加载状态。 + +4.给定的函数实例的堆栈变量,在该实例返回前一直存在。 + +5.如果你正在某个函数中执行,或者正在被这个函数调用(直接或间接),那么这个函数一定有一个活动实例。 + +虽然这些隐式存在保证非常直白,但是涉及隐含存在保证的故障可是真的发生过。 + +> 小问题7.27:依赖隐式存在保证怎样才会导致故障? + +答案:这里有一些因使用隐式存在保证不当而导致的错误。 + +1.程序将全局变量的地址写入文件,然后后续相同程序的更大实例读取该地址并尝试解引用。这可能因为每个进程的地址空间随机化而失败,更不用说程序重新编译了。 + +2.模块可以将其中一个变量的地址记录在位于其他模块的指针中,然后尝试在模块被卸载之后对指针解引用`dereference(*)`。 + +3.函数可以将它的一个堆栈变量的地址记录到全局指针中,其他函数可能会在该函数返回之后尝试解引用。 + +------------- + +如果给定结构只能在持有一个给定的锁时被释放,那么持有锁就保证那个结构的存在。 + + +一种保证锁存在的简单方式是把锁放在一个全局变量里,但全局锁具有可扩展性受限的缺点。有种可以让可扩展性随着数据结构的大小增加而改进的方法,是在每个元素中放置锁的结构。不幸的是,把锁放在一个数据元素中以保护这个数据元素本身的做法会导致微妙的竞态条件,如下所示: +```c +int delete(int key) +{ + int b; + struct element *p; + + b = hashfunction(key); + p = hashtable[b]; + if (p == NULL || p->key != key) + return 0; + spin_lock(&p->lock); + hashtable[b] = NULL; + spin_unlock(&p->lock); + kfree(p); + return 1; +} +``` + +解决本例问题的办法是使用一个全局锁的哈希集合,使得每个哈希桶有自己的锁,如下所示: +```c +int delete(int key) +{ + int b; + struct element *p; + spinlock_t *sp; + + b = hashfunction(key); + sp = &locktable[b]; + spin_lock(sp); + p = hashtable[b]; + if (p == NULL || p->key != key) { + spin_unlock(sp); + return 0; + } + hashtable[b] = NULL; + spin_unlock(sp); + kfree(p); + return 1; +} +``` +> 我的小问题:这个例子看不太懂哟~ +## 7.5 Locking: Hero or Villain? +那些写应用程序的家伙很喜欢锁,那些编写并行库的同行不那么开心,那些并行化现有顺序库的人非常不爽。 + +### 7.5.1 Locking For Applications: Hero! +当编写整个应用程序(或整个内核)时,开发人员可以完全控制设计,包括同步设计。假设设计良好地使用分割原则,如第6章所述,锁可以是非常有效的同步机制,锁在生产环境级别的高质量并行软件中大量使用已经说明了一切。 + +然而,尽管通常其大部分同步设计是基于锁,这些软件也几乎总是还利用了其他一些同步机制,包括特殊计数算法(第5章)、数据所有权(第8章)、引用计数(9.1节)、顺序锁(9.2节)和RCU(9.3节)。此外,业界也使用死锁检测工具[[The kernel lock validator](https://lwn.net/Articles/185666/)]、获取/释放锁平衡工具[[Finding kernel problems automatically](https://lwn.net/Articles/87538/)]、高速缓存未命中分析[[valgrind](http://www.valgrind.org/)]和基于计数器的性能分析[[perf](https://perf.wiki.kernel.org/index.php/Tutorial),[Oprofile](https://oprofile.sourceforge.io/news/)]等等。 + +通过仔细设计、使用良好的同步机制和良好的工具,锁在应用程序和内核领域工作的相当不错。 + +### 7.5.2 Locking For Parallel Libraries: Just Another Tool +如例子`7.1.1.2`中所示,库函数调用引入的死锁问题值得关注。 +会引入死锁问题的几种情况: +- 函数调用应用程序代码 +- 与信号处理程序的交互,例如库函数接收的信号触发了应用程序的信号处理函数的调用。 +- 父进程和子进程之间的库函数调用 + +可以使用以下策略来避免死锁问题。 + + +> 1. Don’t use either callbacks or signals. + +不要使用回调或信号。 + +> 2. Don’t acquire locks from within callbacks or signal handlers. + +不要从回调或信号处理函数中获取锁。 + +> 3. Let the caller control synchronization. + +让调用者控制同步。 + +> 4. Parameterize the library API to delegate locking to caller. + +将库API参数化,以便让调用者处理锁。 + +> 5. Explicitly avoid callback deadlocks. + +显式地避免回调死锁。 + +> 6. Explicitly avoid signal-handler deadlocks. + +显式地避免信号处理程序死锁。 + +### 7.5.3 Locking For Parallelizing Sequential Libraries: Villain! +随着到处可见的低成本多核系统的出现,常见的任务往往是并行化现有的库,这些库的设计仅考虑了单线程使用的情况。从并行编程的角度看,这种对于并行性的全面忽视可能导致库函数API的严重缺陷。比如: +>1. Implicit prohibition of partitioning. + +隐式的禁止分割。 + +> 2. Callback functions requiring locking. + +需要锁的回调函数。 + +> 3. Object-oriented spaghetti code. + +面向对象的意大利面条式代码。