希望此文,能给想深入研究的人一点引导。
阅读此文,需要一些基础知识。
其次,要对
C语言有基本的理解,至少要能达到等级考试二级
C语言的水平。
还有,要有基本的汇编语言基础。不需要太全面。关于这点,可以参考《深入理解计算机系统》第三章,“程序的计算机级表示”。有这一章,寥寥十数页的汇编基础,已经足够了。
如果基础不好,建议补一下基础,在
IT行业混,基本的算法、数据结构、计算机原理还是要懂的。
逻辑读Cache Buffers Chain Latch(CBC Latch) Miss分析
Oracle有很多运行时信息,比如等待事件、各种资料(如
V$SYSSTAT、
V$UNDOSTAT等)视图,理解这些信息的真正含意是关键的,否则,就如同下面这则笑话。
B男问
A男,“你换了辆车,从哪儿搞的”。
A男:“昨天我在山上一个人喝酒,有一个女的开车经过,停在我旁边,脱光衣服,对我说,你可以拿走你想要的,我就把她的车开走了。”
B男:“你做的对,她的衣服你也穿不上。”
很多时候,
Oracle已经脱掉了“她”的衣服,你是否只开走“她”的车。
很可惜,很多时候
Oracle给我们的提示,并非如此易懂,看下面的图:

有网上大神解释残局意思是:
相(想)**就快一点
否则,等我马(妈)一出来就麻烦了
你没士(事),但我有士(事)
以后给我妈锁我在家里就很难再见面( “将 ”被困)
如果你不想我死(黑方死)就快闪
否则,等我马(妈)一出来就麻烦了
你没士(事),但我有士(事)
以后给我妈锁我在家里就很难再见面( “将 ”被困)
如果你不想我死(黑方死)就快闪
还有人说是这个意思:
女孩如果说想**快点,那男孩怎么会走?那个炮是黑方的,女方的,所以棋局的意思是黑方想**,但红方没棋子架炮,打也只能打
"相
"。翻译过来就是
:女的想**,男的没支点,只好空想。可怜的男孩纸惟有羞惭甩门而去了
这个残局的解释,网上多如牛毛,互相冲突的也不少,大家的说法莫衷一是。很多
Oracle问题,不也是一样吗。
继续八卦一下,蓝色的文字描述,“男孩
5秒看懂,
10分钟后摔门而出”。剩下的
9分
55秒在干什么。嗯,这个问题,最好有当时的
AWR报告,看一看他们当时的等待事件,我估计应该剩下的
9分
55秒,应该是在完成
I/O操作。
(注:
I/O,著名
IT述语,百度百科中的解释:
I/O是
input/output的缩写)。
笑过之后,我们开始正文,我们有个库的
AWR报告中,有如下的
Latch Miss:
|
cache buffers chains |
kcbgtcr: kslbegin excl |
0 |
1,489 |
1,237 |
|
cache buffers chains |
kcbchg: kslbegin: bufs not pinned |
0 |
618 |
657 |
|
cache buffers chains |
kcbget: pin buffer |
0 |
137 |
54 |
|
cache buffers chains |
kcbchg: kslbegin: call CR func |
0 |
73 |
409 |
|
cache buffers chains |
kcbrls: kslbegin |
0 |
65 |
119 |
|
……………… |
……………… |
|
|
|
排第一的是
kcbgtcr: kslbegin excl,
Sleep次数远超同类,它是干什么的。
长久以来,“
latch miss”就像
Oracle界的
UFO话题,代表着神秘、力量和难以理解。
Oracle明明准备了这个信息,但又不告诉我们这项信息的准确含义。如同一个
MM已经洗完澡、搞的混身香扑扑的,但又紧裹浴袍。
打开浴袍,你就可以“拿走你想要的”,注意,不要只开走她的车,我想
kcbgtcr: kslbegin excl不是让我们开走“她”的车。搞错了女孩子的意思,可是非常危险的。
首先,“秀才认字看半边”,
kcbgtcr,这可是大名鼎鼎的逻辑读函数。
kcbgtcr: kslbegin excl是否就是逻辑读导致的
Latch竞争呢?不一定噢。下面让我们开始我们的
Cache Buffers Chains Latch Miss之旅,揭开
Latch Miss的神秘面纱。
不过,这个面纱揭的会有点辛苦。
先从链表说起
还是先从
Cache Buffers Chains(以下简称
CBC)链表说起吧。
CBC链表长什么样,如下图:

前面的是
HASH表,有
N个
Bucket,每个
Bucket都对应一条
CBC链表。每个
Bucket中,有两个指针,一个指向链表头,一个指向链表尾。
要注意一点,
CBC 链表中串起来的,都是
BH(
Buffer Header或
Buffer Handle的简称),
BH是在
Buffer Cache的。而
HASH表内存,是在共享池中的。
如果
Bucket中还没有
CBC链,哪会是什么情况呢?
如下图:

Latch函数与Latch Miss
报歉,又要深入进去了。不想当将军的士兵,不是好士兵。
如果你是
China人,生在新中国,长在红旗下,又幸福的加入到了
IT行业中,成为一名光荣的攻城狮。我希望我们能更多的理解拿破仑的这句话。不想当将军的士兵,不是好士兵。哪么,不好的士兵结局是什么?
IT或许只是职业,而不是爱好。但很可惜,在中国我们没得选择。这是我一直准备出去的原因,我已经没得选择了,只希望我的下一代将来可以有更多的选择。谁有出去的门路,别忘了兄弟。
为了减轻难度,你可以想像在做一个侦探游戏。你就是主角大侦探:福尔摩斯。你有一个助手,当然了就是华生医生。让我们切换到作案现场,
Action:
华生问:“
Mr。福尔摩斯,罪犯留下的线索只有
kcbgtcr: kslbegin excl。我们如何深入调查。”
福尔摩斯:“你注意到没,华生,这是一个
CBC Latch。”。
华生:“大人圣明,料事如神”。
画外音,“我
Cao,这
TMD谁都知道。”
华生继续问道:“
Mr。福尔摩斯,我们如何继续调查呢,凶手又是谁呢?”
福尔摩斯在案发现场仔细看了一圈,华生很不解的看着福尔摩斯,他确信现场已经没什么了。罪犯明显是个很精明的人,除了
kcbgtcr: kslbegin excl,它没有留下任何线索。
福尔摩斯突然问:“华生,你有没有注意过,
kcbgtcr是什么。”
福尔摩斯这么一问,华生突然感到一道灵光闪过,“是啊,冒号前的
kcbgtcr,一定有它的意义。”但这道灵光又迅速消失。对于
kcbgtcr是什么,华生没有一点印象。
正在他搜肠刮肚的想着时,福尔摩斯举起手中的
iPad:“它可以告诉我们答案。”
华生凑近一看,差点吐血,只见福尔摩斯手中的
iPad,赫然显示着
Google的页面,搜索栏中,正是
kcbgtcr。
福尔摩斯悠悠的说道:“其实我先搜了百度,没找到可用的,然后才选择
Google。”
说着福尔摩斯转身面向镜头,说:“用
Google,我的选择。”
(植入广告可以打住了,
Google也没给我钱)
只见搜索结果中显示:“
kcbgtcr 是
Oracle数据库最重要的函数之一,其含义为:
Kernal Cache Buffer GeT Cosistents Read,也就是数据库的一致性读操作。”
华生惊呼:“啊,我想起来了,早些年流传江湖的九阴真经之
DSI中,有一篇中好像介绍过这个函数。”
福尔摩斯说:“没错,是
DSI 405。
Kcbgtcr就是
Oracle逻辑读的函数。”
福尔摩斯再次转身面向镜头,植入广告:“如欲对
DSI和
Oracle深入知识系统学习,可加入
QQ群:
127149411”。
华生说道:“也就是说,
kcbgtcr: kslbegin excl是和逻辑读相关的了。但是,
CBC Latch本来就和逻辑读相关,我们费了半天脑子,感脚还是没有前进半步啊。”说完华生一脸的沮丧。
福尔摩斯嘿嘿干笑了两声,“只怕唯必,你忘了,我们前不久研究的最新武器
----Dtrace。走,回实验室,我们用
Dtrace跟踪一下逻辑读先。”
场景切换:实验室。
福尔摩斯先生的测试环境是
Solaris虚拟机。由于
Oracle的工作原理,在众多
OS中大致类似,因此遇到疑难的案子,福尔摩斯先生总是在
Solaris中分析
Oracle原理,再根据原理去解决其他平台的问题。
华生看了一眼福尔摩斯的
Solaris,说:“福尔摩斯先生,其实
Oracle已经将
Solaris中的
Dtrace移植到了
Oracle Linux平台,我觉得
Linux更亲切,为什么我们不换到
Linux下研究呢?”
福尔摩斯答道:“
Oracle Linux下的
Dtrace我已经研究过了,它没有一个重要的功能:
PID探针。我们无法深入到
Oracle体内。”
华生噢一声,福尔摩斯继续说道:“我估计
PID探针比较复杂,
Oracle Linux的
DTrace刚刚推出,功能完善起来,尚需时日。”
说话间,虚拟机已经启动完毕,福尔摩斯熟练的启动数据库。
华生问道:“我们要怎样测试呢?”
福尔摩斯说:“很简单,跟踪逻辑读。”
说着,福尔摩斯已经将语句将语句执行了:
SQL> select dbms_rowid.ROWID_RELATIVE_FNO(rowid),dbms_rowid.rowid_block_number(rowid),rowid from a2 where rownum=1;
DBMS_ROWID.ROWID_RELATIVE_FNO(ROWID) DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID) ROWID
------------------------------------ ------------------------------------ ------------------
12 4356 AAADZuAAMAAABEEAAA
“
12号文件
4356号块,这就是我们的小白鼠。”福尔摩斯自言自语道,“让我看看你是用哪个
Latch保护。”一边自言自语,一边手没闲着,他已经输入了下一条命令:
SQL> select ba,file#,dbablk,HLADDR ,tch,lru_flag,obj from x$bh where file#=12 and dbablk=4356;
BA FILE# DBABLK HLADDR TCH LRU_FLAG OBJ
---------------- ---------- ---------- ---------------- ---------- ---------- ----------
0000000382A86000 12 4356 00000003911F4318 5 0 13934
“看,华生,
12号文件
4356号块用
00000003911F4318这个
Latch保护,它当然是个
CBC Latch。”福尔摩斯说道。
(注:
X$BH的
HLADDR列,记录块对应的
CBC Latch地址)
“我们的测试语句,就用刚才查到的
ROWID,
select * from a2 where rowid=’ AAADZuAAMAAABEEAAA’”,福尔摩斯继续自言自语道,“我们要将它先执行个三、四次,确保它是逻辑读、软软解析。我可不想
Dtrace跟踪出的结果有一大堆硬解析的东西。”
华生在一边说:“是啊,记得跟踪过一次硬解析,好像十分的复杂,比软软解析复杂十倍都不止。”
说话间,福尔摩斯已经将测试语句
select * from a2 where rowid=’ AAADZuAAMAAABEEAAA’在测试会话执行了
5、
6遍。
“可以开始跟踪了。”华生说。
福尔摩斯点点了头,执行了他们常用的
Dtrace脚本,脚本非常简单,也就下面几行:
#!/usr/s
bin/dtrace -s -n
dtrace:::BEGIN
{
i=1;
}
pid$1:::entry
{
printf("i=%d PID::entry:==%s:%s:%s:%s %x %x %x %x %x %x",i, probeprov, probemod, probefunc, probename,arg0,arg1,arg2,arg3,arg4,arg5);
i=i+1;
}
(注:脚本的解释,请参考我的七种武器系列,此处不再过多解释。)
福尔摩斯将脚本命名为all.d,并使用chmod命令,授予了执行权限。如下发布跟踪命令:
bash-3.2# ./all.d -x switchrate=1hz -b 16m 1609 > lread1.log
1609,是测试会话对应的进程号。
当脚本正确运行后,会显示一行:
dtrace: script './all.d' matched 152981 probes
福尔摩斯和华生常常感叹,Oracle的代码中竟然有15万多个函数,Oracle的代码规模,真是够庞大的。等到你在屏幕上看到“dtrace: script './all.d' matched 152981 probes”这一行信息显示出来后,才能说明all.d脚本已经在运行了。
脚本运行后,福尔摩斯在1609进程对应的会话中又一次执行了测试语句:select * from a2 where rowid='AAADZuAAMAAABEEAAA';。
这条语句,将会导致进程申请00000003911F4318地址处的CBC Latch,并在此Latch的保护下,开始逻辑读12号文件、4356号块。
一切都在按照福尔摩斯的计划进行,他喜欢这种把控一切的感觉。“只要静下心了,你也会喜欢这个游戏的。”他经常这样说。但华生其实一点都不喜欢Dtrace,他的看法是,只要架构做好,底层没必要搞哪么深。但福尔摩斯的说法,“向下挖的越深,是为了向上长的更高。”
结果已经有了:

用Latch地址3911F4318搜索,福尔摩斯很快在跟踪结果中找到Oracle申请CBC Latch的函数:sskgslcas。它的第一个参数值,就是Latch的地址。第二个参数是0,第三个参数是1,第四个参数是0。第四、五、六个参数,华生已经没兴趣再看下去了,瞪着屏幕发呆的福尔摩斯紧皱着眉头,又自言自语:“不对啊,为什么在sskgslcas中找不到Latch Miss信息。”
华生这一次没有发问,剩着福尔摩斯发呆的工夫,自己Google了一下:
关于Oracle调用Latch的函数,10G中有两个函数:kslgetl、kslgetsl,分别对应普通Latch和共享Latch。
11G中kslgetl还是普通Latch,共享Latch函数变成ksl_get_shared_latch。这些函数的第四个参数,是Latch Miss信息。
原来Oracle在申请Latch时,会顺便记录一个说明信息,这个说明信息就是Latch Miss。通过它,Oracle主要声明是为了什么才要申请Latch。主要的目的,是为了调试Latch相关的问题。
--------------------------------------------------------------------------------------------------------------------------------------
注:
Latch函数的第4个参数,是x$ksllw或x$kslwsc视图INDX列的值。这两个视图是V$Latch_miss的底层视图,你可以在V$FIXED_VIEW_DEFINITION中找到它们的对应关系。
sys@OFFERT>desc x$ksllw
Name Null? Type
--------------- -------- --------------------------------------------------------
ADDR RAW(8)
INDX NUMBER
INST_ID NUMBER
KSLLWNAM VARCHAR2(64):试图获得Latch的位置,V$LATCH_MISSES.LOCATION 列。
KSLLWLBL VARCHAR2(64): V$视图中没有此列,“Why” meaning for some of “where” may be guessed from ksllwlbl column of x$ksllw.
sys@OFFERT>desc x$kslwsc;
Name Null? Type
------------- -------- --------------------------------------------------------
ADDR RAW(8)
INDX NUMBER
INST_ID NUMBER
KSLNOWTF NUMBER v$latch_misses.NWFAIL_COUNT列,no-wait获得Latch失败的次数。
KSLSLEEP NUMBER v$latch_misses.SLEEP_COUNT,请求闩导致睡眠的次数
KSLWSCWSL NUMBER v$latch_misses.WTR_SLP_COUNT,等待着睡眠的次数
KSLWSCLTHG NUMBER v$latch_misses.LONGHOLD_COUNT,Number of times someone held a latch for the entire duration of someone else's sleep
KSLLASNAM VARCHAR2(50) : Latch的名字
x$ksllw中有Latch Miss的Location列,但它里面没有Latch的名字。x$kslwsc中有Latch的名字,但它没有Location列。它们两个都有INDX列,就是Latch函数的第四个参数。
--------------------------------------------------------------------------------------------------------------------------------------
了解完了背景知识,华生也不禁迷惑起来,Latch函数的第四个参数,代表Latch Miss的Location列的编号。但是,CBC Latch的申请函数明显不同以往:sskgslcas。而且,第四个参数为0,这肯定不是Latch Miss。怪不得福尔摩斯也迷惑起来呢。
线索到这里就中断了,如果是其他的Latch,只要Dtrace跟踪一下,找到调用Latch函数的第四个参数,就知道Latch Miss是什么了。但现在,CBC Latch的机制明显有了变化。
华生和福尔摩斯对望了一下,华生明显已经有放弃的想法,但福尔摩斯不是轻易认输之人。略停了片刻,福尔摩斯毅然决然一挥手,说:“上mdb。”
华生道:“mdb,威力太大了吧。这是核子级别的武器,当年日本广岛、长崎死伤太多。如果我们用mdb,得死多少脑细胞啊。我看,不如就此中止吧”
福尔摩斯说:“就此中止,原凶岂不逍遥法外。我要让他知道,天网恢恢,疏而不漏。脑细胞死了旧的,才能生出新的,如果长期不新陈代谢,反而容易衰老。”
华生叹了口气,“唉,哪只好如此。你准备如何使用mdb?”
福尔摩斯一字一顿的说:“反~~~汇~~~编。”
卡卡嚓,天空一道霹雳闪过,紧接着是哄隆隆的雷声。华生被福尔摩斯的这几个字吓了一大跳,情不自禁的重复道:“反~汇~编!”
“是的,”福尔摩斯说道,“唯一的线索说明,kcbgtcr函数申请Latch时,导致了kcbgtcr: kslbegin excl处争用,既然从Latch申请函数sskgslcas中,找不到线索,我们一不做二不休,将kcbgtcr函数反汇编,这样,一定能找到线索。”
“总要有人去做这样的事情的。不能提起DBA,就是Liews,就是Tanel Poder,就是国外某某大佬,相信很多国人也想深入研究,只是不得其门而入,我来找出这扇门在哪里,进与不进,在你自己了。”福尔摩斯悠悠的说的。
他边说边敲命令:
bash-3.2# mdb -p 1609
Loading modules: [ ld.so.1 libc.so.1 ]
>
1609号进程,还是刚才哪个进程。在mdb中,他使用kcbgtcr::dis反汇编kcbgtcr:
> kcbgtcr::dis
kcbgtcr: pushq %rbp
kcbgtcr+1: movq %rsp,%rbp
kcbgtcr+4: pushq %rbx
kcbgtcr+5: pushq %r12
kcbgtcr+7: pushq %r13
………………
反汇编的结果,一共12095行。汇编的结果有1万多行,想来C语言的代码,也要写个千吧行。真是一个复杂的大函数。
“这么复杂,该如何进行下去呢?”,华生叹道。
“Easy。”福尔摩斯轻松的说道,“你想想,CBC Latch虽然没有使用普通的Latch函数,而用了sskgslcas,但它也一定有Latch Miss相关信息。不然,案犯不会在罪案现场留下kcbgtcr: kslbegin excl的Latch Miss信息。我们之要……”。
福尔摩斯买了个关子,故意没再说下去,不过华生毕竟已经跟随福尔摩斯多年,久经沙场,马上也猜到了:“是不是先将Latch地址:00000003911F4318处的内存值设置为1,然后再执行我们的测试语句,估计制造一起CBC Latch等待,用mdb的单步执行,看看Oracle将会如何设置Latch Miss?”
福尔摩斯点点头:“是的。”
说话间,他已经开始动手,将Latch地址:00000003911F4318处的内存值设置为1,这一步很简单,使用oradebug即可:
SQL> oradebug setmypid
Statement processed.
SQL> oradebug peek 0x00000003911F4318 4
[3911F4318, 3911F431C) = 00000000
SQL> oradebug poke 0x00000003911F4318 4 1
BEFORE: [3911F4318, 3911F431C) = 00000000
AFTER: [3911F4318, 3911F431C) = 00000001
SQL>
Oradebug的poke命令,可以设置SGA中某处内存的值。就用它,将内存0x00000003911F4318处置为1。
注意,还要在mdb中设置断点:
bash-3.2# mdb -p 1609
Loading modules: [ ld.so.1 libc.so.1 ]
> kcbgtcr:b <-------------------------------在kcbgtcr函数处设置断点
> :c ß-----------------------------:c,继续执行1609进程正在执行的代码。
在1609进程对应的会话中,执行测试语句:select * from a2 where rowid='AAADZuAAMAAABEEAAA';。
Mdb会让1609停在我们设置的断点处:
> kcbgtcr:b
> :c
mdb: stop at kcbgtcr
mdb: target stopped at:
kcbgtcr: pushq %rbp
使用::step单步执行:
> ::step
mdb: target stopped at:
kcbgtcr+1: movq %rsp,%rbp
最终,福尔摩斯发现,进程在kcbgtcr函数偏移0x2788字节处,调用了sskgslcas函数:
> ::step
mdb: target stopped at:
kcbgtcr+0x2788: call +0x6894f08
“这是加CBC Latch的函数。”福尔摩斯迟疑着说。
“是啊,我记得加、释放Mutex,也是搞这个函数。”华生不知道福尔摩斯要干什么,在旁边接腔道。
福尔摩斯继续道:“是啊,也是Mutex的函数,我们从这个函数开始吧,顺带也将Mutex的秘密昭告天下。而且,我们的线索只是一个CBC Latch的Miss信息,我们也只能从sskgslcas开始入手。”
事已至此,华生也没有再劝说什么,福尔摩斯使用sskgslcas::dis,将sskgslcas也反汇编了一下,没想到sskgslcas函数的内容极为简单:
> sskgslcas::dis
sskgslcas: movq %rsi,%rax
sskgslcas+3: lock cmpxchgq %rdx,(%rdi)
sskgslcas+8: sete %al
sskgslcas+0xb: movzbq %al,%rax
sskgslcas+0xf: ret
| 这么简单的函数,相信应该是用C语言的内嵌汇编写的。现在,进程正停在sskgslcas的入口处,福尔摩斯看了一下寄存器的值:
> $r %rax = 0x0000000000000000 %r8 = 0x0000000000000000 %rbx = 0x0000000000000000 %r9 = 0x00000003927628d8 %rcx = 0x0000000000000000 %r10 = 0x00000003911a7e88 %rdx = 0x0000000000000001 %r11 = 0xfffffd7ffcd01f00 %rsi = 0x0000000000000000 %r12 = 0x00000003911f52e8 %rdi = 0x00000003911f4318 %r13 = 0x0000000392762790 %r14 = 0xfffffd7ffc924720 %r15 = 0x0000000000000001 这要要说明一下的是,从左边rdi寄存器开始,rsi、rdx等,依次是调用函数的第一个、第二个、第三个等等参数。 Sskgslcas中第一条指令: sskgslcas: movq %rsi,%rax 将函数的第二个参数传给rax寄存器,从上面展示的结果,第二个参数,也就是rsi的值为0。 Sskgslcas下一条指令: sskgslcas+3: lock cmpxchgq %rdx,(%rdi) 它在sskgslcas函数入口处向后3字节, “这说明第一条指令‘movq %rsi,%rax’占了三个字节。”福尔摩斯向华生解释道。 Lock是锁总线的,cmpxchgq是测试并交换,test&swap,将两个操作封装在一条指令中,由CPU保证这两个操作的原子性。在多芯、多核的系统中,加Lock是必须的。 福尔摩斯又向华生解释:“cmpxchgq会首先用(%rdi)处的内存值和rdx寄存品的值相比较,比较结果将会影响标志寄存器。如果比较结果不相等,会将rdx的值传入内存(%rdi)处。如果比较结果相等,则不再将rdx的值传入内存(%rdi)处” 华生一脸的疑惑,显然还没看懂,福尔摩斯问道:“你不是有二级C语言的基础吗,哪你应该能看懂下面这段C C语言伪码。” 说着,福尔摩斯写下这段伪码: If ( (rdi) != rdx ) { (rdi) = rdx ; } “嗯,”,看了一几行代码,华生有点懂了。“因为我们已经用oradebug poke,将Latch地址处的内存值设置为1。所以这里Cmpxchgq测试结果不成功,等于Latch没有加上。” “是的。”,福尔摩斯说道,“汇编和C语言不同,汇编要直接操作硬件,现代CPU架构中,任何比较语句,都会引起标志寄存器的改变。所以,无论条件语句中的(rdi) = rdx ;语句是否被执行,(rdi) != rdx这个比较语句,都会改变一些标志寄存器的值。” “因此,”,福尔摩斯顿了一顿,继续说道,“下一条语句就是从标志寄存器中取结果了” 下两条语句是: sskgslcas+8: sete %al sskgslcas+0xb: movzbq %al,%rax 华生赶忙Google了一下,sete,地ZF标志寄存器的值传送到al寄存器。这次福尔摩斯没有做过多解释,直接写了行C伪码: Al = ( (rdi) != rdx ) 华生一看就明白了,“sete %al,相当于将(rdi) != rdx比较的结果,送入al寄存器。” “差不多吧,”,福尔摩斯笑笑答道。 “此处,(rdi)和rdx相等,条件不成立,送进al的值,就是0了。”华生道。 “是的。”福尔摩斯回答说,“而下面一条指令,movzbq,就简单了,将8位的al寄存器值,扩展到64的rax中,然后,就是ret了,按照汇编的惯例,返回值一定要在rax寄存器中,整个来看,这段代码就如同下面这段C语言代码。” 福尔摩斯写下了下面这段代码: Int sskgslcas(rdi,rsi,rdx,rcx) { If ( al = (rdi) != rdx ) { (rdi) = rdx ; } Return al; } 这就是CBC Latch函数和Mutex的伪码了。 华生看了一会儿,在心中默默的想了一遍,就明白了,“sskgslcas的结果在rax寄存器,如果Latch申请成功,rax为1,如果申请不成功,rax为0,是这样的吧。”华生问道。 “是这样的。”福尔摩斯答道,“让我们单步执行,验证一下。” 说着,他在mdb中使用::step,让进程一直执行到ret处: > ::step mdb: target stopped at: sskgslcas+0xf: ret 查一下此进寄存器的值: > $r %rax = 0x0000000000000000 %r8 = 0x0000000000000000 %rbx = 0x0000000000000000 %r9 = 0x00000003927628d8 %rcx = 0x0000000000000000 %r10 = 0x00000003911a7e88 %rdx = 0x0000000000000001 %r11 = 0xfffffd7ffcd01f00 %rsi = 0x0000000000000000 %r12 = 0x00000003911f52e8 %rdi = 0x00000003911f4318 %r13 = 0x0000000392762790 %r14 = 0xfffffd7ffc924720 %r15 = 0x0000000000000001 Rax果然为0。申请Latch没有成功。 看到单步执行结果和他们分析的一样,福尔摩斯和华生相视一笑。 “接下来要做什么?”华生问道。 福尔摩斯笑着骂道,“动动脑子想想啊,sskgslcas函数已经分析完了,接下来做啥。” “从sskgslcas调用处,继续分析kcbgtcr。”华生肯定的回答。 “你开窃了,华生。”福尔摩斯开了句玩笑,手中没停,拿过刚才所汇编kcbgtcr的结果,从刚才的位置继续向下看: kcbgtcr+0x2788: call +0x6894f08 kcbgtcr+0x278d: testl %eax,%eax kcbgtcr+0x278f: jne +0x48 kcbgtcr+0x2791: movq $0x0,0x450(%r13) kcbgtcr+0x279c: testl %r15d,%r15d kcbgtcr+0x279f: movl $0x0,%eax kcbgtcr+0x27a4: movl $0x40,%ecx kcbgtcr+0x27a9: movl %eax,%esi kcbgtcr+0x27ab: cmovl.e %ecx,%esi kcbgtcr+0x27ae: orl $0x109,%esi kcbgtcr+0x27b4: movl 0x4(%r14),%eax kcbgtcr+0x27b8: movl %eax,%ecx kcbgtcr+0x27ba: movq 0xa718147 (%rip),%r8 kcbgtcr+0x27c1: movl (%r8),%r8d kcbgtcr+0x27c4: movq -0x698(%rbp),%rdi kcbgtcr+0x27cb: movl $0x1,%edx kcbgtcr+0x27d0: call -0x1202260 kcbgtcr+0x27d5: jmp +0x53 kcbgtcr+0x27d7: movq 0xaf571ca(%rip),%r8 d7f1bf0->r8 这次华生已经不再畏惧,主动说到,“调用sskgslcas返回,下一条语句是testl %eax,%eax,看来这是在比较Latch的申请是否成功了。” “是的”,福尔摩斯说到,“jne是不相等或非零跳转,这两行语句,相当于。”说着,他又写下了如下的伪码: kcbgtcr+0x278d: testl %eax,%eax kcbgtcr+0x278f: jne +0x48 相当于: if ( eax != 0 ) { goto } “Eax不等于零就跳转,”华生惊叫道,“这是判断Latch是否申请成功的,如果Latch申请成功就跳到kcbgtcr+0x27d7处。” 福尔摩斯冲着华生眨了眨眼,道:“瞧,我们离目标越来越近了,好像没也没什么难的,不是吗!”。 “是啊,”华生接着说:“被跳过去的部分,就是当Latch没有申请成功的话,Oracle会执行的代码。” 福尔摩斯道:“是的,我肯定案犯也来过这里,我们要仔细调查这段代码。” 这段代码是: kcbgtcr+0x278f: jne +0x48 kcbgtcr+0x2791: movq $0x0,0x450(%r13) kcbgtcr+0x279c: testl %r15d,%r15d kcbgtcr+0x279f: movl $0x0,%eax kcbgtcr+0x27a4: movl $0x40,%ecx kcbgtcr+0x27a9: movl %eax,%esi kcbgtcr+0x27ab: cmovl.e %ecx,%esi kcbgtcr+0x27ae: orl $0x109,%esi kcbgtcr+0x27b4: movl 0x4(%r14),%eax kcbgtcr+0x27b8: movl %eax,%ecx kcbgtcr+0x27ba: movq 0xa718147 (%rip),%r8 kcbgtcr+0x27c1: movl (%r8),%r8d kcbgtcr+0x27c4: movq -0x698(%rbp),%rdi kcbgtcr+0x27cb: movl $0x1,%edx kcbgtcr+0x27d0: call -0x1202260 kcbgtcr+0x27d5: jmp +0x53 kcbgtcr+0x27d7: movq 0xaf571ca(%rip),%r8 d7f1bf0->r8 “看,这段代码中调用了一个函数,kslgess。”华生叫到。 “是的,它一定和Latch Miss有关,单步执行到它哪里。”福尔摩斯说道。 接着,他用::step,一路执行下去,直到在调用kslgess处停下: > ::step mdb: target stopped at: kcbgtcr+0x27d0: call -0x1202260 用$r显示kslgess的参数: > ::step mdb: target stopped at: kcbgtcr+0x27d0: call -0x1202260 > $r %rax = 0x0000000003001104 %r8 = 0x000000000000067e %rbx = 0x0000000000000000 %r9 = 0x00000003927628d8 %rcx = 0x0000000003001104 %r10 = 0x00000003911a7e88 %rdx = 0x0000000000000001 %r11 = 0xfffffd7ffcd01f00 %rsi = 0x0000000000000109 %r12 = 0x00000003911f52e8 %rdi = 0x00000003911f4318 %r13 = 0x0000000392762790 %r14 = 0xfffffd7ffc924720 %r15 = 0x0000000000000001 “看到r8没”,福尔摩斯说,“67e,这个值有问题,我看,它就是Latch Miss。” 未等福尔摩斯吩咐,华生已经将验正语句准备好: set linesize 1000 col KSLLWNAM for a30 col KSLLWLBL for a20 col KSLLASNAM for a40 select a.addr,b.addr,a.KSLLWNAM, a.KSLLWLBL,b.KSLLASNAM from x$ksllw a,x$kslwsc b where a.indx=b.indx and a.indx=to_number('&aa','xxxxxxxx'); 执行结果是: Enter value for aa: 67e old 1: select a.addr,b.addr,a.KSLLWNAM,a.KSLLWLBL,b.KSLLASNAM from x$ksllw a,x$kslwsc b where a.indx=b.indx and a.indx=to_number('&aa','xxxxxxxx') new 1: select a.addr,b.addr,a.KSLLWNAM,a.KSLLWLBL,b.KSLLASNAM from x$ksllw a,x$kslwsc b where a.indx=b.indx and a.indx=to_number('67e','xxxxxxxx') ADDR ADDR KSLLWNAM KSLLWLBL KSLLASNAM ---------------- ---------------- ------------------------------ -------------------- ---------------------------------------- 000000000CFC1F08 00000003926F9AA0 kcbgtcr: fast path (cr pin) cache buffers chains “Oh yeah!”,华生叫道:“67e是kcbgtcr: fast path (cr pin)的代码。” 福尔摩斯道:“别哪么快下结论,验证一下。” 华生马上写了一下PL/SQL程序: declare v_id1 number; begin for i in 1..10000000 loop select id1 into v_id1 from a2 where rowid='AAADZuAAMAAABEEAAA'; end loop; end; / 在两个会话中执行,两个进程疯狂读同一块,一定会有大量CBC Latch竞争,查询V$Latch_misses视图,果然发现kcbgtcr: fast path (cr pin)的SLEEP_COUNT大量增加。 看来,调用kslgess时,r8寄存器的值果然就是latch miss的Location编号。 其他的Latch函数,如kslgetl,它的形式如下: Kslgetl(Latch地址,未知,未知,Where,……) 第四个参数Where,就是Latch Miss。 Sskgslcas不一样,这个函数看来是只用于非常繁忙的CBC Latch和Mutex。它在被调用时没有Latch Miss,如果申请Latch、Mutex失败,再通过kslgess函数登记Latch Miss信息。 忙了半天,终于小有进展。 福尔摩斯说道:“下面分析一下r8寄存器的值从哪里来的。” 关于r8,有下面两行代码: kcbgtcr+0x27ba: movq 0xa718147 (%rip),%r8 kcbgtcr+0x27c1: movl (%r8),%r8d 0xaf5d827(%rip),这也能算是一个“PC相关地址”,rip是PC指令寄存器,记录下一条要执行的指令代码的内存地址。0xa718147 (%rip)的值如何求呢? 福尔摩斯没等华生问,就写出了如下的方法 kcbgtcr+0x27ba+0xa718147 华生在mdb中使用dump命令,显示了一下: > kcbgtcr+0x27ba+0xa718147,8::dump -e ceaf9a1: 038001aa b0000000 038001aa b8000000 得到一个内存地址:038001aab0。 下一步“movl (%r8),%r8d”,就是将此内存地址处的值,传给r8。显示一下此内存地址的值: > 038001aab0,8::dump -e 38001aab0: 0000067e 00000000 0000067f 00000000 正好是67e。也就是kcbgtcr: fast path (cr pin)。 福尔摩斯说:“看华生,有了些进展。我们并不需要反汇编所有代码,只需要找到我们感兴趣的部分就行。” 华生说:“是的,但是这个kcbgtcr: fast path (cr pin),并不是罪案现场的kcbgtcr: kslbegin excl啊。” 福尔摩斯说:“嗯,是的。我们有了进展,但还没有最终找到结果。” 华生说:“不过,也算是有成果了,最普通的逻辑读,或者说一致读,Latch Miss是kcbgtcr: fast path (cr pin)。原凶并不是普通的逻辑读。” 福尔摩斯问道:“想一想,华生,接下来要怎么做!” 华生说:“可以将kcbgtcr所有kslgess找到,查看kcbgtcr: kslbegin excl会在哪儿。” 福尔摩斯说:“对,立即行动。” 华生答:“Yes,Sr。” 华生将kcbgtcr的反汇编代码放入一个文本文件,统计了一下: bash-3.2# cat kcbgtcr.asm|grep kslgess kcbgtcr+0x537: call -0x11fffc7 kcbgtcr+0x27d0: call -0x1202260 kcbgtcr+0x5bb3: call -0x1205643 kcbgtcr+0x61f5: call -0x1205c85 kcbgtcr+0x644e: call -0x1205ede kcbgtcr+0x9777: call -0x1209207 不多,只有6处。Kcbgtcr函数只在6个地方用sskgslcas申请CBC Latch。它们分别对应的Latch Miss是: 1、kcbgtcr+0x521: movq 0xaf5f9e0(%rip),%r8 ---662 kcbgtcr: fast path 2、kcbgtcr+0x27ba: movq 0xa718147 (%rip),%r8 ---- kcbgtcr: fast path (cr pin) 3、kcbgtcr+0x5ba9: movq 0xaf5a360(%rip),%r8 ---- 0x663 kcbgtcr: kslbegin excl 4、kcbgtcr+0x61eb: movq 0xaf59eae(%rip),%r8 --- 695 kcbgtcr_2 5、kcbgtcr+0x6444: movq 0xaf599bd(%rip),%r8 --- 642 kcbgtcr: kslbegin shared 6、kcbgtcr+0x976d: movq 0xaf56a0c(%rip),%r8 ---6b1 kcbgtcr: L2 其中,kcbgtcr+0x27ba处的,是刚刚发现的、普通逻辑读处的Latch Miss。案犯现场的目标:kcbgtcr: kslbegin excl,在偏移量kcbgtcr+0x5ba9处。 看到这个结果,福尔摩斯和华生都长出了一口气,kcbgtcr: kslbegin excl终于现形了。但新的问题又来了,kcbgtcr+0x5ba9,Oracle在什么情况下才会执行到这里呢?可以肯定的时,这不是最普通的物理读,因为最普通的物理读,Latch Miss是kcbgtcr+0x27ba处的kcbgtcr: fast path (cr pin)。 福尔摩斯和华生想了很多情况,但都不能找到什么情况下Oracle才会走到kcbgtcr: kslbegin excl这里。最后,福尔摩斯狠下心来,从kcbgtcr的头开始,单步执行,一步步跟踪。 “先把kcbgtcr函数的原理搞清楚”,福尔摩斯说,“不然,我们发现不了Oracle什么时候会执行kcbgtcr: kslbegin excl这里。” “也只好如此了,”华生举目沮丧的说到。 他们开始从kcbgtcr的第一个字节处分析反汇编代码,过程有点无聊,长话短说吧,从kcbgtcr+0x2c0处到kcbgtcr+0x385处,是Buffer Cache中的HASH算法: kcbgtcr+0x2c0: movq 0xa723e59(%rip),%r8 kcbgtcr+0x2c7: movl 0x80(%r8),%eax kcbgtcr+0x2ce: incl %eax kcbgtcr+0x2d0: andl $0xf,%eax kcbgtcr+0x2d3: movq 0xa723e46(%rip),%r8 kcbgtcr+0x2da: movl %eax,0x80(%r8) kcbgtcr+0x2e1: movl (%r14),%eax kcbgtcr+0x2e4: movq 0xa723e35(%rip),%r8 kcbgtcr+0x2eb: movq 0xa723e2e(%rip),%r9 kcbgtcr+0x2f2: movl 0x80(%r9),%ecx kcbgtcr+0x2f9: movl %ecx,%r9d kcbgtcr+0x2fc: movl %eax,(%r8,%r9,8) kcbgtcr+0x300: movl 0x4(%r14),%eax kcbgtcr+0x304: movq 0xa723e15(%rip),%r8 kcbgtcr+0x30b: movq 0xa723e0e(%rip),%r9 kcbgtcr+0x312: movl 0x80(%r9),%ecx kcbgtcr+0x319: movl %ecx,%r9d kcbgtcr+0x31c: movl %eax,0x4(%r8,%r9,8) kcbgtcr+0x321: movq 0xa714018(%rip),%r8 kcbgtcr+0x328: movl (%r8),%eax kcbgtcr+0x32b: movl %eax,-0x448(%rbp) kcbgtcr+0x331: movl (%r14),%eax kcbgtcr+0x334: shll $0x11,%eax kcbgtcr+0x337: addl 0x4(%r14),%eax kcbgtcr+0x33b: imull $0x9e370001,%eax,%eax kcbgtcr+0x341: movq 0xa72ed30(%rip),%r8 kcbgtcr+0x348: movl (%r8),%ecx kcbgtcr+0x34b: movl %eax,%edx kcbgtcr+0x34d: shrl %cl,%edx kcbgtcr+0x34f: movq 0xa72ed2a(%rip),%r8 kcbgtcr+0x356: movl (%r8),%eax kcbgtcr+0x359: andl %edx,%eax kcbgtcr+0x35b: movl %eax,%r8d kcbgtcr+0x35e: shlq $0x4,%r8 kcbgtcr+0x362: movq 0xa72ed1f(%rip),%r9 kcbgtcr+0x369: movl (%r9),%ecx kcbgtcr+0x36c: movl %edx,%eax kcbgtcr+0x36e: shrl %cl,%eax kcbgtcr+0x370: movl %eax,%r9d kcbgtcr+0x373: shlq $0x4,%r9 kcbgtcr+0x377: movq 0xa72ed12(%rip),%r10 kcbgtcr+0x37e: movq (%r10),%r10 kcbgtcr+0x381: movq (%r10,%r9),%r12 kcbgtcr+0x385: addq %r8,%r12 这段代码对应的C语言伪码是: int sp1=0x11; int sp2=0x7f; int sp3=0x7; int hash_header=0x394953140; hash(int p1,int p2) { int uk=p1; int rdba=p2; int v1,v2; int target_addr; uk=uk<<0x11+rdba; uk=(0x9e370001*uk)&0x00000000FFFFFFFF; uk=uk>>sp1; v1=uk&sp2; v1=v1<<0x4; v2=uk>>0xsp3; v2=v2<<0x4; target_addr=hash_header+v2+v1; } 通过解析反汇编代码,对于HASH运算消耗的CPU,更加清楚了。 在算法出HASH值后,执行流程会跳到kcbgtcr+0x25b1处,从这里开始,是申请CBC Latch的准备工作。然后在kcbgtcr+0x2788处,就是申请CBC Latch了,在它后面,就是开始搜索HASH链了,在这之后,有如下的代码: kcbgtcr+0x281d: movl 0x4(%r14),%eax kcbgtcr+0x2821: movl %eax,%r9d kcbgtcr+0x2824: movq %r9,0x18(%r8) kcbgtcr+0x2828: cmpq (%r12),%r12 kcbgtcr+0x282c: movq $0x0,%r8 kcbgtcr+0x2833: movq %r8,-0x5f0(%rbp) kcbgtcr+0x283a: movl $0x0,-0x5c8(%rbp) kcbgtcr+0x2844: movl $0x0,-0x5d0(%rbp) kcbgtcr+0x284e: movl $0x0,-0x664(%rbp) kcbgtcr+0x2858: je +0x2fe8 在kcbgtcr+0x281d处,从r14寄存器向下偏移4字节传入eax的值,是被读块的dba。R12寄存器的值,是HASH Bucket中CBC链的链表头。“cmpq (%r12),%r12”是判断CBC链是否为空。还记得前面所画的CBC链图吗,如果链表是空的,Bucket中记录Bucket本身的地址。也就是Bucket自已指向自己。cmpq (%r12),%r12正是比较r12指向的内容和r12是否相等。如果相等,Bucket后就没有链表。 如果链表为空,证明块一定不在Buffer Cache,kcbgtcr+0x2858处的“je +0x2fe8 这充分说明kcbgtcr+0x5840后的代码,是处理物理读的。 如果CBC链不为空,下面的代码就是遍历CBC链,这行代码: kcbgtcr+0x289c: movq (%r8),%r8 这句的意义是将指针指向的值赋给指针,类似于这样的代码:prt_next=*ptr_next 。这句正是用来遍历CBC链表的。 紧接着这三行,将注释直接写在代码后: kcbgtcr+0x2918: movl 0x4(%r14),%eax // 将dba传给eax寄存器,作为目标dba kcbgtcr+0x291c: cmpl %eax,0xc4(%r13) // 寄存器r13记录BH地址,向下偏移0xc4字节, 就是BH中的dba,用eax中的目标dba和此dba比较。 kcbgtcr+0x2923: jne +0x1857 跳转过去之后,有如下的代码: kcbgtcr+0x4195: movq %r8,-0x5a0(%rbp) kcbgtcr+0x419c: movq -0x5a0(%rbp),%r8 kcbgtcr+0x41a3: testq %r8,%r8 kcbgtcr+0x41a6: jne -0x18a6 继续查找。 kcbgtcr+0x41ac: jmp +0x9 kcbgtcr+0x41ae: movq %r13,-0x608(%rbp) kcbgtcr+0x41b5: cmpq $0x0,-0x608(%rbp) kcbgtcr+0x41bd: je +0x1683 跳转到kcbgtcr+0x5840处。 kcbgtcr+0x41c3: movzwl 0xbc(%r14),%eax 也就是说,CBC链中没找到目标时(也就是物理读时),将跳到kcbgtcr+0x5840处。 看看kcbgtcr+0x5840代码吧: kcbgtcr+0x583d: popq %rbx kcbgtcr+0x583e: leave kcbgtcr+0x583f: ret kcbgtcr+0x5840: movl -0x5b8(%rbp),%eax kcbgtcr+0x5846: testl %eax,%eax 在kcbgtcr+0x583f处,有指令ret,kcbgtcr到这里有可能结束,返回调用者。程序什么时候会跳到kcbgtcr+0x5840处呢,物理读。 纯逻辑读,kcbgtcr函数的执行,会在kcbgtcr+0x583f处终止。物理读则会跳至kcbgtcr+0x5840后开始执行。 经过艰难的调查取证,福尔摩斯和华生终于大概搞清楚了kcbgtcr的执行流程。 华生看了一眼作案现场留下的线索:kcbgtcr: kslbegin excl,轻轻的喃喃自语:“终于抓到你了。” 有没有忘掉kcbgtcr: kslbegin excl在代码的哪里: kcbgtcr+0x5ba9: movq 0xaf5a360(%rip),%r8 ---- 0x663 kcbgtcr: kslbegin excl kcbgtcr+0x5ba9,在kcbgtcr+0x5840之后不远的位置,这块代码,只有物理读才有可能走到这里。 所以,原凶就是:“物理读”。 这真是一个奇怪的结果,逻辑读函数中的Latch竞争,原凶竟然是物理读。 调优的方向,应该是针对物理读最多的一些SQL,这样才有助于减少kcbgtcr: kslbegin excl处的Latch争用。 而这调优逻辑读高的SQL,对于减少kcbgtcr: kslbegin excl处的Latch争用,是没有帮助的。 进一步的,可以将kcbgtcr中的Latch Miss分个类: 一、纯逻辑读相关的 1、kcbgtcr+0x521: movq 0xaf5f9e0(%rip),%r8 ---662 kcbgtcr: fast path 2、kcbgtcr+0x27ba: movq 0xa718147 (%rip),%r8 ---- kcbgtcr: fast path (cr pin) 二、物理读相关的: 3、kcbgtcr+0x5ba9: movq 0xaf5a360(%rip),%r8 ---- 0x663 kcbgtcr: kslbegin excl 4、kcbgtcr+0x61eb: movq 0xaf59eae(%rip),%r8 --- 695 kcbgtcr_2 5、kcbgtcr+0x6444: movq 0xaf599bd(%rip),%r8 --- 642 kcbgtcr: kslbegin shared 6、kcbgtcr+0x976d: movq 0xaf56a0c(%rip),%r8 ---6b1 kcbgtcr: L2 |

更多内容请关注微信公众号:数据与人
