并发执行单元引起的错误 : 竞态案例
a 初始化 为 0
A 内核进程 对 变量 a 加1
B 内核进程 对 变量 a 加1
a 现在 为 2
下面的顺序不会有问题
A load 内存中的a 进 寄存器
A 寄存器 自加 1
A store 寄存器中的值 进 内存
B load 内存中的a 进 寄存器
B 寄存器 自加 1
B store 寄存器中的值 进 内存
此时a的值为2
下面的顺序会有问题
A load 内存中的a 进 寄存器1
A 寄存器1 自加 1
B load 内存中的a 进 寄存器2
B 寄存器2 自加 1
A store 寄存器1 中的值 进 内存
B store 寄存器2 中的值 进 内存
此时a的值为1
内核态并发原因分类
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - 竞态
由于 中断 调度 的存在, 会存在 伪并发
由于 SMP 的存在, 会存在 真并发
解决 中断 矛盾问题引入了软中断, 也就引入了 软中断 产生的竞态
在 __irq_svc 返回时调度, 也就引入了内核抢占的 竞态
总之, 竞态原因有以下五种
中断
调度
SMP
软中断
内核抢占
对于内核代码( 驱动, 内核线程, 中断, 其他内核模块) 来说( 因为地址空间相同) , 存在的并发原因有五种
中断
调度
SMP
软中断
内核抢占
中断处理程序可以打断软中断,tasklet和进程上下文的执行
软中断和tasklet之间不会并发,但可以打断进程上下文的执行
在支持抢占的内核中,进程上下文之间会产生并发
在不支持抢占的内核中,进程上下文之间不会产生并发
同一类型的中断处理程序不会并发, 但是不同类型的中断有可能被送到不同的cpu上, 因此不同类型的中断处理程序可能存在并发执行
同一类型的软中断会在不同的cpu上并发执行
同一类型的tasklet是串行执行的, 不会再多个cpu上并发
不同cpu上的进程上下文会并发
静态局部变量
全局变量
共享的数据结构
缓存
链表
红黑树
用户态并发原因分类
对于应用代码来说, 存在的并发原因有一种( 对 进程地址空间中的内存相同部分 进行并发访问) ,
这时候我们不对 并发原因( 并发原因只有调度) 分类, 而是对( 共享资源) 分类, 共享资源分为多种
多线程代码中的所有变量
多进程代码中的共享内存中的变量
解决竞态的方案
解决竞态的方案 叫做 同步
所有的 同步 都是基于原子操作, 是芯片提供的指令, 不是软件做的. 软件做的是在原子操作的封装
同步根据 竞态原因 的不同, 而 分为多种
我们可以用不同的 同步 方法 围住 共享资源 保证 在 某个情景下 不会产生竞态
虽然在代码上, 我们的同步方法围住了共享资源, 但是 却 不能保证 共享资源 一定会被 同步 方法 围住. 因为存在着cpu重排序指令这个问题
这个问题主要分为以下几种:
1. 编译器编译时的优化
2. 处理器执行时的多发射和乱序优化
3. 读取和存储指令的优化
4. 缓存同步顺序(导致可见性问题)
而cpu 重排序指令的 解决方案 为 内存屏障原语, 在外表现为多种形式
1. volatile 关键字
2. barrier ( )
3. mb/ rmb/ wmb/ smp_mb/ smp_rmb/ smp_wmb
volatile 关键字使用的是Lock指令,volatile 的作用取决于Lock指令。
Lock是软件指令
Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。类似于Lock指令。
volatile 的变量在进行写操作时,会在前面加上lock质量前缀。
Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。
而Lock前缀是这样实现的
它先对总线/ 缓存加锁,然后执行后面的指令,最后释放锁后会把高速缓存中的脏数据全部刷新回主内存。
在Lock锁住总线的时候,其他CPU的读写请求都会被阻塞,直到锁释放。Lock后的写操作会让其他CPU相关的cache失效,从而从新从内存加载最新的数据,这个是通过缓存一致性协议做的。
lock前缀指令相当于一个内存屏障(也称内存栅栏)(既不是Lock中使用了内存屏障,也不是内存屏障使用了Lock指令),内存屏障主要提供3 个功能:
确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
强制将对缓存的修改操作立即写入主存,利用缓存一致性机制,并且缓存一致性机制会阻止同时修改由两个以上CPU缓存的内存区域数据;
如果是写操作,它会导致其他CPU中对应的缓存行无效。
内存屏障是CPU指令
所有同步机制的基础 - 同步原语(Synchronization primitives)
swap and swap byte instructions 在ARMv6中不推荐使用, 建议所有软件都迁移到使用新的同步原语
ARMv6提供了一种新的机制来支持更全面的非阻塞共享内存同步原语,这种原语可以扩展到多处理器系统设计中
Load- Exclusive : LDREX
Store- Exclusive : STREX
不同的同步机制 都是 以 同步原语( Synchronization primitives) 和 屏障 为基础的
针对不同的 竞态原因, 有不同的同步方法
per- cpu 变量
atomic bit 64 位 变量
禁中断/ 中断屏蔽
禁抢占
禁软中断
自旋锁
读写锁
顺序锁
信号量( count初始化为1 ) / 读写信号量
互斥锁
RCU
大内核锁BLK
其他易混淆的概念
解决竞态的方案( 同步) 是 为了 不让 并发( 真并发和伪并发) , 保证数据的一致性
而 事件同步 是为了 两个事件必须有先后顺序( 叫做两个同步事件)
不相干的事件 不需要 有先后顺序
而 不管 两个事件( A B) 要不要先后顺序, 都需要 做 解决竞态的方案( 同步)
设计 让 两个事件同步 的 机制 有
1. 等待一段时间
2. 等待事件完成或条件满足
事件同步, 某事件等待另一件事件结束
事件异步, 某事件不等待另一事件结束
A 等待 B 结束
1. A 等待一段时间( sleep并调出) 等到 硬件复位( B)
2. A 调用 poll ( wait并调出) 等待事件完成或条件满足( 数据到来) ( B)
A 不等待 B 结束
1. A 调用 提交任务给 tasklet/ 工作队列 , A 继续执行 . B在一个时机处理 任务
2. A 调用 设置回调 给 timer/ hrtimer , A 继续执行 . 定时器到了, B调用回调
同步 : 确定会会断在哪条指令上
异步 : 不确定会会断在哪条指令上
进程A的一个IO请求分为两个过程
1. 等待数据准备就绪
2. 将数据从内核态拷贝到用户空间
阻塞IO与非阻塞IO的定义
第一个过程阻塞/ 非阻塞( A) 对应 阻塞IO/ 非阻塞IO ( B)
同步IO与异步IO的定义
第二个过程阻塞/ 非阻塞( A) 对应 同步IO/ 异步IO ( 非同步)
read 非阻塞
第一个过程 阻塞
第二个过程 阻塞
第一个过程使用的 api read 不阻塞( 此时说的是read的等待数据准备好)
第二个过程使用的 api read 阻塞( 此时说的是read的拷贝数据)
read 阻塞
第一个过程 阻塞
第二个过程 阻塞
第一个过程使用的 api read 阻塞( 此时说的是read的等待数据准备好)
第二个过程使用的 api read 阻塞( 此时说的是read的拷贝数据)
select
第一个过程 阻塞
第二个过程 阻塞
第一个过程使用的 api select 阻塞( 此时说的是select的等待数据准备好)
第二个过程使用的 api read 阻塞( 此时说的是read的拷贝数据, 不是read的等待数据准备好)