Latch闩锁在Oracle中属于 KSL Kernel Services Latching, 而从顶层视图来说 KSL又属于VOS Virtual Operating System。
Latches 是一种 低级别(low-level)的 锁机制, 初学IT的同学请注意 低级不代表简单, C语言对比java语言要 低级一些但C并不比java简单。
在一些文章著作中也将latch称为spin lock 自旋锁。 latch用来保护 共享内存(SGA)中的数据 以及关键的代码区域。 一般我们说有2种latch:
1)Test and Set 简称TAS :
TAS是计算机科学中的专指, test-and-set instruction 指令 用以在一个 原子操作(atomic 例如非中断操作)中写入到一个内存位置 ,并返回其旧的值。 常见的是 值1被写入到该内存位置。 如果多个进程访问同一内存位置, 若有一个进程先开始了test-and-set操作,则其他进程直到第一个进程结束TAS才可以开始另一个TAS。
关于TAS指令更多信息 可以参考wiki ,包括TAS的伪代码例子: http://t.cn/zQgATRr
askmaclean.com
在Oracle中Test-And-Set类型的latch使用原生的Test-And-Set指令。 在绝大多数平台上, 零值zero代表latch是 空闲或者可用的 , 而一个非零值代表 latch 正忙或者被持有。 但是仅在HP PA-RISC上 正相反。 TAS latch只有2种状态 : 空闲 或者 忙。
2) Compare-And-Swap 简称 CAS
Compare-And-Swap 也是计算机专有名词, Compare-And-Swap(CAS)是一个用在多线程环境中实现同步的 原子指令( atomic )。 该指令将在一个给定值(given value)和 指定内存位置的内容 之间比对,仅在一致的情况下 修改该内存位置的内容为一个 给定的 新值(不是前面那个值)。 这些行为都包含在一个 单独的原子操作中。 原子性保证了该新的值是基于最新的信息计算获得的; 如果该 内存位置的内容在同时被其他线程修改过,则本次写入失败。 该操作的结果必须说明其到底是否执行了 取代动作。 它要么返回一个 布尔类型的反馈, 要么返回从 指定内存地址读取到的值(而不是要写入的值)。
关于CAS的更多信息可以参考 http://t.cn/hcEqh
Oracle中的 Compare-And-Swap Latch也使用原生态的Compare-And-Swap指令。 和TAS Latch类似, 空值代表latch是free的,而一个非空值代表latch正忙。 但是一个CAS latch 可以有多种状态 : 空闲的、 以共享模式被持有 、 以排他模式被持有。 CAS latch可以在同一时间被 多个进程或线程以共享模式持有, 但还是仅有一个进程能以排他模式持有CAS latch。 典型的情况下, 共享持有CAS latch的进程以只读模式访问相关数据, 而一个排他的持有者 目的显然是要写入/修改 对应CAS latch保护的数据。
举例来说, CAS latch的共享持有者是为了扫描一个链表 linked list , 而相反排他的持有者是为了修改这个列表。 共享持有者的总数上线是0x0fffffff即10进制的 268435455。
注意几乎所有平台均支持CAS latch, 仅仅只有HP的PA-RISC平台不支持(惠普真奇葩)。 在PA-RISC上CAS latch实际是采用TAS latch。 所以虽然在HP PA-RISC上代码仍会尝试以共享模式获得一个latch,但是抱歉最终会以 排他模式获得这个latch。
一般 一个latch会包含以下 信息:
- Latch type 类型 , latch type定义了 是TAS 还是CAS latch, latch class和 latch number
- Latch的 level 级别
- 持有该latch的代码位置where ,例如 使用kslgetl函数获得某个latch,则持有文职为kslgetl
- 持有该latch的原因
- nowait模式下获得该latch的次数 V$LATCH.IMMEDIATE_GETS
- wait模式下第一个尝试失败的次数 V$LATCH .MISSES
- nowait模式下尝试失败的次数 V$LATCH.IMMEDIATE_MISSES
- 获取latch失败造成sleep的总时间 X$KSLLTR.KSLLTWSL, V$LATCH.SLEEPS
- 首次spin成功获得latch的次数 X$KSLLTR.KSLLTHST0, V$LATCH.SPIN_GETS
- latch wait list等等
子闩 child latch
当一个单一的latch要保护过多的资源时 会造成许多争用, 在此种场景中 child latch变得很有用。 为了使用child latch, 需要分割原latch保护的资源为多个分区, 最常见的例子是 放入到多个hash buckets里, 并将不同子集的资源分配给一个child latch。 比起每一个hash bucket都去实现一个单独的latch来说, 编程上 使用child latch要方便的多, 虽然这不是我们用户所需要考虑的。 为一个latch 定义多个child latch,则这个latch称为parent latch父闩。 child latch 可以继承 parent latch的一些属性, 这些属性包括 级别和清理程序。 换句话说, child latch就像是parent 父闩的拷贝一样。
经典情况下, 在SGA 初始化过程中child latch将被分配和初始化(startup nomount)。但在目前版本中(10/11g)中也允许在实例启动后 创建和删除latch。
child latch又可以分成2种:
- 允许一个进程/线程在同一时刻持有2个兄弟child latch
- 不允许一个进程/线程在同一时刻持有2个兄弟child latch
因为child latch从parent latch那里继承了属性,所以注意 child latch的 latch level和 parent 父闩是一样的。 因为 一个进程/线程 不能在同一时间 持有2个latch level一样的闩,所以正常情况下 一个进程/线程 也不能同一时间 持有2个兄弟child latch。
回到我们说的hash bucket的例子里来, 假设一个进程/线程有 将一个resource从一个hash bucket 移动到另一个hash bucket的需求,在此场景中就需要 同时持有2个兄弟child latch。 但是如果允许这种同时持有2个兄弟child latch的行为发生的话, 那么很容易造成死锁deadlock的麻烦。 oracle 不允许 进程/线程任意地同时获得2个兄弟child latch,由于此种操作很容易引起死锁。 由此引入了一些规则 : 兄弟child latch必须是相关的child number ,且进程/线程只能以特性的顺序来同时get 2个兄弟child latch,即child number 从大到小低贱的顺序。
此外需要注意的是仅有TAS latch可以同时get多个兄弟child latch,目前还不支持 CAS的latch。
Latch 清理恢复
Oracle中定义了一个latch, 就需要这个latch对应的清理函数cleanup function,这个函数在以下2个场景中生效:
- 当某个latch被持有,但是持有进程遇到了某一个错误 ==》主动
- 当持有latch的进程die掉,需要PMON进程前去恢复这个latch的状态 ==》被动
经典情况下, 执行清理函数的进程要么把正在执行过程中的操作回滚掉 ,要么前滚掉。 为了为 前滚(rolling forward)或者回滚(rolling back)提供必要的信息, oracle在latch结构中加入了recovery的结构,其中包含了这个正在执行过程中的操作的日志信息, 这些日志信息必须包含足以前滚或者回滚的数据。 如我们以前讲过的, 理论上oracle进程可能在运行任何指令的时候意外终止,所以清理恢复是常事。
清理恢复最恶心的bug是PMON 执行cleanup function时因为 代码bug ,把PMON自己给弄dead了, 由于PMON的关键的后台进程,所以这会引起实例终止。
Latch和 10.2.0.2后引入的KGX Mutex对比
和 latch一样, kgx mutex也是用来控制串行化访问SGA中数据的 ,但仍有一些重要区别:
- KGX mutex要比 CAS latch更轻量级, mutex 结构大约为16个字节, 而一个latch结构大约是100个字节。 因此 mutex嵌入到大量其他对象结构中是可行的, 因为他的struct 足够小
- 之所以mutex可以提供 更小的结构 很廉价的成本,其原因是 使用mutex有一个简单的前提假设: 对于mutex的争用是很小的。 因此没有为mutex那样提供一个优化过的wait list , mutex做更多的 SPIN & WAIT 并消耗更多的CPU。 此外mutex也没有提供任何死锁检测和预防机制,这些都完全取决于Kgx mutex的用户自身的行为。
- Latch在 内部视图(例如X$KSLLT)中提供 全面的诊断信息。 KGX mutex在(x$mutex_sleep、x$mutex_sleep_history等内部视图)中提供部分信息, 同时也允许其用户在回调程序中用特定信息填充这些视图。
- 除了共享和排他模式之外, KGX mutex还提供一种examine 模式, 允许其在不以共享或排他模式持有mutex的情况下client检查一个mutex的状态以及其用户数据。 这种模式是latch所没有的
Latch 和Enqueue lock队列锁对比,以下是latch和enqueue的几个重大区别:
- 在典型情况下,latch被认为将仅仅被持有很短的一段时间(ms级别),而enqueue 将被持有 比之长得多的多的时间(秒=》分钟=》小时)。 例如TX 队列事务锁在整个事务的生命周期中被持有 。 latch被设计出来就是为了在 函数运行到某几十个乃至上百个指令过程中被持有,这是很短暂的过程
- latch是为了避免同一时间 有一个以上的进程运行相似的代码片段, 而enqueue是为了避免同一时间多于一个的进程访问相同的资源
- latch的使用较为简单, 而enqueue的使用则由于命名空间namespace和死锁检测 的问题而较为麻烦
- latch只有2个模式 共享和排他, 而enqueue 则支持6个模式
- RAC中 latch 总是本地存放在当前实例的SGA中, 而enqueue可以是Local的 也可能是Global的
- 9i以前latch不是FIFO的,是抢占式的; 从9i开始 大多数latch也是FIFO了; enqueue始终是FIFO的
有同学仍不理解 latch和enqueue的区别的话, 可以这样想一下, latch 保护的SGA中的数据 对用户来说几乎都是不可见的, 例如 cache buffer的hash bucket 对不研究内部原理的用户来说 等于不存在这个玩样,这些东西都是比较简单的数据结构struct ,如果你是开发oracle的人 你会用几百个字节的enqueue 来保护 几个字节的一个变量吗?
而队列锁 TX是针对事务的 , TM是针对 表的,US是针对 undo segment的,这些东西在实例里已经属于比较高级的对象了,也是用户常可见的对象, 维护这些对象 需要考虑 死锁检测、 并发多模式访问、RAC全局锁 等等问题,所以需要用更复杂的enqueue lock。
死锁dead lock
为了使得latch使用足够轻量级 ,死锁预防机制十分简单。 由此Oracle开发人员 在构建一个latch时会定义 一个数字级别 level (从 0 到 16 ), 并且Oracle要求它们必须以level增序顺序获取。 若一个进程/线程在持有一个 latch的情况下,要求一个相同或者更低level的latch的话,KSL层会生成一个内部错误, 这种问题称为 “latch hierarchy violation”。
SQL> select distinct level# from v$latch order by 1; LEVEL# ---------- 0 1 2 3 4 5 6 7 8 9 10 11 14 15 16
仅有以nowait模式get latch时可以以级别(level) 非兼容的级别获得一个latch,但是这种情况非常少。
Latch Level 级别
Oracle在定义latch level的时候 取决于以下2个原则:
- 那些latch是在被持有的情况下, 进程/线程还会去get其他的latch?
- 当已经有latch被进程/线程持有的情况下, 那些latch还会被 get?
如上文dead lock的描述, latch level的一大作用是 帮助减少latch dead lock。
Latch Class
latch的类class定义了如下的内容:
- spin count
- yield count (number of times we yield cpu before sleeping)
- wait time sample rate (0 implies it is not enabled)
- sleep (in microseconds and repeated [see below])
对于post/wait 类而言 SLEEP_BUCKET和SLEEP_TIME 是被忽略的。
以下是几个latch class:
Class 0 Post/Wait Class ,绝大多数latch都是该类型
Class 1 Waiter List Latch。 该Latch保护对应latch的Waiter List,这种latch被假定总是只被持有非常短的时间(指令级别), 有充分的理由花费更多的spin count 消耗更多的CPU , 并尽可能减少sleep时间
Class 2 那些由于多种原因,不能使用post/wait机制的latch 。 例如process allocation latch 这个闩 是在一个新进程创建时所需要获取的,但是新进程还没加载post/wait的上下文,显然无法用post/wait , 所以这种 latch不能用post /wait机制
Class 3 非常短持有的latch, 特性与class 1类似。
CLASS_KSLLT字段代表了latch的类型
SQL> select CLASS_KSLLT,count(*) from x$kslltr group by CLASS_KSLLT; CLASS_KSLLT COUNT(*) ----------- ---------- 2 1 0 702 SQL> select KSLLTNAM,CLASS_KSLLT from x$kslltr where CLASS_KSLLT=2; KSLLTNAM CLASS_KSLLT ---------------------------------------------------------------- ----------- process allocation 2
从9.0.2 开始 每个latch class的SPIN COUNT、YIELD COUNT 、WAITTIME_SAMPLING 、 SLEEP_TIME[1] …. SLEEP_TIME[i] 均在参数_latch_class_X中定义 。
SQL> col name for a20 SQL> col avalue for a20 SQL> col sdesc for a20 SELECT x.ksppinm NAME,y.ksppstvl avalue,x.KSPPDESC sdesc FROM SYS.x$ksppi x, SYS.x$ksppcv y WHERE x.inst_id = USERENV ('Instance') AND y.inst_id = USERENV ('Instance') AND x.indx = y.indx AND x.ksppinm like '%latch%class%'; NAME AVALUE SDESC -------------------- -------------------- -------------------- _latch_class_0 latch class 0 _latch_class_1 latch class 1 _latch_class_2 latch class 2 _latch_class_3 latch class 3 _latch_class_4 latch class 4 _latch_class_5 latch class 5 _latch_class_6 latch class 6 _latch_class_7 latch class 7 _latch_classes latch classes override SQL> select INDX,SPIN,YIELD,WAITTIME,SLEEP0 from X$KSLLCLASS; INDX SPIN YIELD WAITTIME SLEEP0 ---------- ---------- ---------- ---------- ---------- 0 20000 0 1 8000 1 20000 0 1 1000 2 20000 0 1 8000 3 20000 0 1 1000 4 20000 0 1 8000 5 20000 0 1 8000 6 20000 0 1 8000 7 20000 0 1 8000 8 rows selected.
举例来说 _latch_class_1=”5000 2 0 1000 2000 4000 8000″ 则
- SPIN_COUNT=5000
- YIELD_COUNT=2
- wait time sampling: 0 (不收集,一般都是1即收集)
- 增序的sleep time 1000 => 2000 => 4000 => 8000, 单位是microseconds,超过4次则保持在8000
每一个wait class 适应自己对应_latch_class_X中的SPIN_COUNT、YIELD_COUNT等参数 。 而实例参数_SPIN_COUNT只做为向后兼容,若对应的latch Class没有自己的SPIN_COUNT属性才会生效。
由此实际生效的SPIN_COUNT由由以下几个参数 按优先级从高到低生效:
- 首先是 设置过的_latch_class_X 中的SPIN_COUNT
- 设置够的_SPIN_COUNT
- 内部函数
注意 除非是oracle support建议你去修改这些latch参数,否则在任何系统中都不该去尝试修改它们,如果你确实遇到了latch free的问题,那么你应当首先做 SQL 调优 和并发调整。
SPIN
还记得 电影《inception》盗梦空间里中旋转的陀螺吗, 旋转的陀螺 在英文里就是spinning top。 spin 自旋是 latch话题中一个频率很高的词,但是一直以来我们对自旋的理解都不够彻底 ,下面我们彻底解释 9i以后的自旋SPIN 和 Busy Latch原理。
SPIN 是指 当进程首先尝试获取latch失败后( 一般是别人持有了该latch), 有2种选择 要么是退让CPU(yield CPU) 休眠一段时间后再重新尝试获取latch , 要么是 本进程抱着希望在CPU 上空转,因为如果我不用CPU了 让给别人用了 就会造成context switch上下文切换 (vmstat 里看到的CS),而我在CPU上空转的话就可以等等看这个latch是否会在这段时间里被人家释放, 我的一次空转称为SPIN 一次, 而SPIN_COUNT定义了我在这次总的SPIN 操作里总共SPIN 空转多少次,例如SPIN_COUNT=2000(注意 见上文中对SPIN_COUNT的描述)就是说 我有机会空转 2000次, 空转一次后 我跑去查看一下latch是否被别人释放了,如果没有我继续下一次空转, 如果是释放了 那么我就获得这个latch了,也就是SPIN_GETS成功了。如果SPIN 2000次了还是没有等到释放latch,则SPIN_GETS没有成功, 之后该SLEEP就SLEEP( 9i前后 从9i开始有区别,具体见下文)。
如果上述SPIN GET的成功获得了latch,那么因为我没有退让CPU 也就没有上下文切换, 所以显然我获得latch的速度要比直接sleep并重试来的快。
另假设我提高了某个latch对应的 spin_count ,例如修改latch_class_1中的SPIN_COUNT为更高的值,则在上述情况下可能SPIN循环的次数更多,也就意味着有更高的概率在 SPIN阶段获得 latch, 而代价是SPIN消耗更高的 CPU时间片。 相反 若降低SPIN_COUNT,则意味着SPIN阶段获得latch的概率降低, SPIN消耗相对少的CPU。
在中古的硬件中 可能有仅有1个CPU的系统,虽然现在很少见了, 但是显然在仅有一个CPU的情况下SPIN是无意义的,因为如果你把唯一的一个CPU用来SPIN了,显然 真正持有对应latch的那个进程获取不到CPU,获取不到CPU的结果是它无法释放这个latch。在这种环境里代码自动把spin_count调整为1。
SPIN 与Latch Busy
9i之前的 spin与latch busy 运作伪代码可以点击这里(main for 8i)。
从9.0.2开始oracle 开始大量启用 post/wait和latch class机制 , 我们来描述一下 伪代码
SLEEPs //睡眠次数计数 yields //yield 计数 copyright askmaclean.com on_wait_list = FALSE; while (如果未获得latch) { 在对应的latch上SPIN ,循环次数为SPIN_COUNT,SPIN_COUNT 来源一般为 _latch_class_X if (获得latch) break; if(yields < YIELD_COUNT) // YIELD_COUNT来源为latch_class_X { yields++; yield CPU ; } else { yield =0; if (如果latch是post/wait机制的) { on_wait_list=TRUE ; get wait list latch ; //获得wait list latch add current process to wait list; //将被进程加入到wait list的尾部 free wait list latch; //释放wait list latch wait to be posted; //等待被post } else { wait for SLEEP_TIMES[sleeps] microseconds; //等待SLEEP_TIME[sleeps]对应的时间 来源为latch_class_X if ( sleeps < SLEEP_BUCKETS) //SLEEP_BUCKETS 一般为4 sleeps++; } } } //如果某一刻获得了一个post/wait latch,且本进程在wait list上,则需要从wait list 上把自己移走: if( 如果我在wait list上) { get wait list latch ; //获得wait list latch remove current process from wait list; //从wait list 上把自己移走 free wait list latch; //释放wait list latch }
从9i开始绝大多数 latch都是 post/wait class的, 出去少量非post/wait class的latch和PMON进程外, 进程都会进入非时控的sleep (sleep[1]..[sleep[i] i到4以后不再增加) 不会自己醒来, 仅在该latch的持有者 释放该latch 且 等待的进程在wait list的头部的情况下被post唤醒 (awake)。 Oracle 选择这种非时空的sleep的原因 为了避免在miss后引起反复的 上下文交换context switch 以便改善性能。
但是这种实现也存放一种风险, 即需要应对那些 持有latch进程意外终止 和 存在丢失 post的bug的情况。