Solaris學習筆記(6)

Solaris學習筆記(6) - 07年9月修訂版

作者: Badcoffee
Email:
[email protected]
Blog: http://blog.csdn.net/yayong
2007年9月

本文介紹使用kmdb和mdb調試Solaris內核的基本方法,kmdb和mdb是Solaris默認安裝的內核模塊調試器,可以用於調試和定位內核模塊及驅動程序發生的錯誤。本文僅用於學習交流目的,錯誤再所難免,如果有勘誤或疑問請與作者聯繫。

本文首次發佈於07年3月,此後因網友發現錯誤而修訂於07年9月。在此感謝細心網友指出我的錯誤。

關鍵詞:mdb/kmdb/panic/hang/crashdump/dump/kernel/debug/Solaris/OpenSolaris


系統panic和hang是內核開發人員常遇到的兩個問題。在Solaris學習筆記(5)中,我們對一個panic做出了簡單分析,可以看到,通過系統panic線程的調用棧信息,通過檢查函數的入口參數,我們可以比較快速的定位引起panic內核模塊,並做深入分析。但是系統hang的問題有時會更復雜一些,通常,我們會嘗試在系統hang發生時,強制系統產生一個crash dump,通過檢查當時內核的狀態來定位問題。


系統hang的分類

1. 死鎖(deadlock)問題

死鎖問題,通常會導致操作系統無法正常調度系統內核線程;因此,通過對調度隊列(dispatch queue)及相關內核線程的狀態和這些線程持有鎖的檢查,通常可以定位問題的發生,描繪出系統是如何發生死鎖的。

2. 系統資源耗盡

系統資源耗盡也是引起系統hang的原因之一,因此,對系統的CPU,物理內存,slab子系統的檢查往往是定位此類問題的關鍵。

3. 硬件問題

硬件引起的問題往往令人難以琢磨,不幸的是,在開發中,尤其是系統軟件的開發,我們經常會遇到此類問題。當我們排除問題不屬於前面描述的兩類時,往往要考慮硬件問題。


關於Sparc系統

在Sparc系統上,當系統hang發生時,即便我們不再啓動時加載kmdb,我們也可以通過進入OBP,通過sync命令來產生一個crashdump, 然後再用mdb對這個crash dump進行事後分析:

Type 'go' to resume
{
1} ok sync

panic[cpu1]
/thread=2a10037fcc0: sync initiated

sched: software trap
0x7f
pid
=0, pc=0xf0046ac0, sp=0xede4f3c1, tstate=0x8800001400, context=0x0
g1
-g7: 134167c, 1, 5, 18f5c90, 0, eddc1190, 1343d4c

00000000fedd3cd0 unix:sync_handler
+138 (180c000, 0, 1, 109e000, 1, 1818800)
%l0-3: 0000000001863d80 0000000001863c00 000000000000017f 000000000185a000
%l4-7: 0000000000000000 0000000001853400 0000000000000003 0000000001814400
00000000fedd3da0 unix:vx_handler
+80 (fad99478, 1822138, 0, 2928, 1822240, f0057d3d)
%l0-3: 0000000001822240 0000000000000000 0000000000000001 0000000000000001
%l4-7: 0000000001815000 00000000f0000000 0000000001000000 000000000101dde4
00000000fedd3e50 unix:callback_handler
+20 (fad99478, fff6a280, 0, 0, 0, 0)
%l0-3: 0000000000000016 00000000fedd3701 00000000f0000000 00000000fff78000
%l4-7: 00000000f0046a68 0000000000000000 0000000000000000 00000000fff657a8

syncing file systems... done
dumping to
/dev/dsk/c0t0d0s1, offset 214827008, content: kernel
100% done: 51986 pages dumped, compression ratio 4.20, dump succeeded

關於x86系統

在x86下,由於沒有OBP的支持,因此只有引導時加載kmdb,纔可以在系統hang時通過激活kmdb來產生crashdump;在Solaris x86上設置和激活kmdb的方法,在Solaris學習筆記(5)已經給出過,在此就不再贅述。


案例分析


問題描述:一個同事正在嘗試改進Solaris的Intel千兆網卡驅動(e1000g),在新編譯出版本上運行測試時,系統hang頻繁發生,並且可以通過運行相同的測試重現該問題。

對於可以穩定重現的此類問題,即便在不瞭解root cause的情況下,通過查看新修改的代碼,不斷的修改-重試,總能定位到出問題的代碼。

但是,無疑這需要耗費很多時間,並且整個過程是痛苦和索然無味的,而且也許你解決了問題,但是不知道問題的本質。

現在讓我們用更理性更符合邏輯的方式來分析和解決這個問題。


1. Enable kmdb - 過程略,方法詳見Solaris學習筆記(5)

2. 加載新驅動,運行測試來重現這個系統hang - 過程略

3. 系統hang再次發生,在console上激活kmdb,產生一個crashdump - 過程略,方法詳見Solaris學習筆記(5)

4. 系統重啓後,用mdb檢查crashdump文件,過程如下:

在dumpadm(1M)指示的路徑下,用mdb打開序號最新的文件:

> mdb 3
Loading modules: [ unix krtld genunix specfs dtrace cpu.AuthenticAMD.
15 uppc pcplusmp scsi_vhci ufs ip hook neti sctp arp

usba nca lofs zfs random nfs sppp crypto ptm ]

檢查系統緩衝區,看是否能得到與網卡驅動或者系統hang相關的信息:

> ::msgbuf
MESSAGE
pcplusmp: pci1000,
30 (mpt) instance 0 vector 0x1b ioapic 0x3 intin 0x3 is bound to cpu 0
..........................................................
..........................................................
..........................................................

panic[cpu1]
/thread=fffffe8000401c80:
BAD TRAP: type
=e (#pf Page fault) rp=fffffe800043dd70 addr=0 occurred in module "" due to a NULL pointer dereference


sched:
#pf Page fault
Bad kernel fault at addr
=0x0
pid
=0, pc=0x0, sp=0xfffffe800043de68, eflags=0x10046
cr0: 8005003b
<pg,wp,ne,et,ts,mp,pe> cr4: 6f0<xmme,fxsr,pge,mce,pae,pse>
cr2:
0 cr3: f8f4000 cr8: c
rdi:
286 rsi: 2000 rdx: 3f8
rcx:
11 r8: fffffffffbcc9eb0 r9: ffffffff82e46000
rax:
0 rbx: 0 rbp: fffffe800043de70
r10: fffffffffbc4c3c0 r11: 290818a385a r12:
0
r13: ffffffff82751480 r14: ffffffff82e5a080 r15:
0
fsb: ffffffff80000000 gsb: ffffffff82e46000 ds:
43
es:
43 fs: 0 gs: 1c3
trp: e err:
10 rip: 0
cs:
28 rfl: 10046 rsp: fffffe800043de68
ss:
30

fffffe800043dc50 unix:die
+c8 ()
fffffe800043dd60 unix:trap
+12ec ()
fffffe800043dd70 unix:cmntrap
+140 ()
fffffe800043de70
0 ()
fffffe800043de80 genunix:kdi_dvec_enter
+10 ()
fffffe800043deb0 unix:debug_enter
+37 ()
fffffe800043dee0 unix:abort_sequence_enter
+35 ()
fffffe800043df40 asy:async_rxint
+24d ()
fffffe800043df90 asy:asyintr
+c7 ()
fffffe800043dff0 unix:av_dispatch_autovect
+7b ()
fffffe8000401b30 unix:cmnint
+155 ()
fffffe8000401c40 unix:cpu_halt
+c5 ()
fffffe8000401c60 unix:idle
+116 ()
fffffe8000401c70 unix:thread_start
+8 ()

syncing file systems...
done
dumping to
/dev/dsk/c1t0d0s1, offset 429719552, content: kernel

本例中::msgbuf的輸出中並沒有找到什麼有價值的信息,如e1000g驅動的錯誤消息,或者內核的錯誤消息;由於系統panic是我們通過 kmdb強制產生的,因此調用棧的信息並不像分析panic時那樣是非常重要的,而且,在本例中毫無用處,可以從調用棧看到,我們的console時重定向到串口設備上的,因此纔會出現asy驅動的名字。


接着我們檢查調度隊列,來看看在CPU上和dispatch queue上的線程狀態:

> ::cpuinfo -v
ID ADDR FLG NRUN BSPL PRI RNRN KRNRN SWITCH THREAD PROC
0 fffffffffbc27730 1f 1 6 169 no no t-3 fffffe80000bfc80 sched
| | |
RUNNING
<--+ | +--> PIL THREAD
READY
| 10 fffffe80000bfc80
QUIESCED
| 6 fffffe80000b9c80
EXISTS
|
ENABLE
+--> PRI THREAD PROC
99 fffffe80000d1c80 sched

ID ADDR FLG NRUN BSPL PRI RNRN KRNRN SWITCH THREAD PROC
1 fffffffffbc2f260 1b 1 0 -1 no no t-17 fffffe8000401c80 (idle)
| |
RUNNING
<--+ +--> PRI THREAD PROC
READY
60 fffffe80044d9c80 sched
EXISTS
ENABLE

系統中有兩個CPU,我們先檢查CPU 0相關的線程,共3個,狀態如下:

fffffe80000bfc80 - 在CPU上正在運行,panic時,運行了3個tick,它的PIL是10,應該是時鐘中斷線程;
fffffe80000b9c80 - 該線程PIL是6,是中斷線程,狀態不明;
fffffe80000d1c80 - 是內核線程,調度優先級爲99,在dispatch queue上,等待被CPU 0調度;

首先,我們檢查正在CPU 0上運行的線程的狀態:


> fffffe80000bfc80::thread -i
ADDR STATE FLG PFLG SFLG PRI EPRI PIL INTR
fffffe80000bfc80 onproc
9 0 3 169 0 10 fffffe8000005c80

狀態是onproc,果然是正在運行,用::findstack可以查看這個線程的調用棧:

> fffffe80000bfc80::findstack -v
stack pointer
for thread fffffe80000bfc80: fffffe80000bf8e0
fffffe80000bf930 apic_intr_enter
+0xc7(fffffffffbc27730, f)
fffffe80000bf940 _interrupt
+0x13b()
fffffe80000bfa60 pc_rtcget
+0xe3(fffffe80000bfa80)
fffffe80000bfac0 pc_tod_get
+0x13()
fffffe80000bfae0 tod_get
+0x11()
fffffe80000bfb50 clock
+0x737()
fffffe80000bfc00 cyclic_softint
+0xc9(fffffffffbc27730, 1)
fffffe80000bfc10 cbe_softclock
+0x1a()
fffffe80000bfc60 av_dispatch_softvect
+0x5f(a)
fffffe80000bfc70 dosoftint
+0x32()

可以看出,它的確是時鐘中斷線程,clock函數是實際上solaris時鐘中斷線程需要執行的一個函數。

接着查看fffffe80000b9c80,這個線程的PIL是6,因爲網卡的中斷線程PIL就是6,所以很有可能它就是我們的網卡中斷線程:

> fffffe80000b9c80::thread -i
ADDR STATE FLG PFLG SFLG PRI EPRI PIL INTR
fffffe80000b9c80 sleep
9 0 3 165 0 6 n/a
> fffffe80000b9c80::findstack -v
stack pointer
for thread fffffe80000b9c80: fffffe80000b9a70
[ fffffe80000b9a70 resume_from_intr
+0xbb() ]
fffffe80000b9ab0 swtch
+0x9f()
fffffe80000b9b50 turnstile_block
+0x76b(ffffffff93338a00, 1, ffffffff82f76288, fffffffffbc05908, 0, 0)
fffffe80000b9bb0 rw_enter_sleep
+0x1de(ffffffff82f76288, 1)
fffffe80000b9c00 e1000g_intr
+0x94(ffffffff82f76000)
fffffe80000b9c60 av_dispatch_autovect
+0x7b(1b)
fffffe80000b9c70 intr_thread
+0x50()

果然,e1000g_intr告訴我們,這是e1000g網卡驅動的中斷處理例程,即ISR。

在接下來檢查第3個線程前,我們在網卡驅動函數的調用棧中,發現了一個有趣的信息,那就是這個網卡中斷在嘗試獲得一個rwlock(讀寫鎖)未果,最後睡眠了:

e1000g_intr -> rw_enter_sleep -> turnstile_block -> swtch

rw_enter_sleep則告訴我們它嘗試獲得rwlock失敗;
turnstile_block告訴我們它被置入turnstile隊列,即一種特殊的sleep queue;
swtch函數,告訴我們它已經完成上下文切換;

上面就是典型的嘗試獲得rwlock未果而睡眠的調用棧;

查看OpenSolaris的源代碼,可以知道,turnstile_block的第三個參數就是rwlock的地址:

http://src.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/common/os/turnstile.c#turnstile_block


int
turnstile_block(turnstile_t
*ts, int qnum, void *sobj, sobj_ops_t *sobj_ops,
kmutex_t
*mp, lwp_timer_t *lwptp)
{
....................
}

那麼,我們就可以用turnstile_block的第三個參數的地址來檢查rwlock的狀態:

> ffffffff82f76288::rwlock
ADDR OWNER
/COUNT FLAGS WAITERS
ffffffff82f76288 READERS
=1 B011 ffffffff838470c0 (W)
| | fffffe800027bc80 (R)
WRITE_WANTED
-------+| fffffe80000ddc80 (R)
HAS_WAITERS
--------+ fffffe80000b9c80 (R)

可以看到,有4個內核線程阻塞在這個rwlock上,其中1個寫者和3個讀者,所以WRITE_WANTED和HAS_WAITERS位都置1了,並且最重要的是,該rwlock的具體類型是讀鎖,因爲在OWNER/COUNT域的值是擁有鎖的讀者數,即READERS=1,這表明這是一個讀鎖;如果是寫鎖,那麼OWNER/COUNT就應該是擁有寫鎖的內核線程地址;

我們可以查看阻塞在該rwlock上的4個線程的調用棧,它們全都和e1000g驅動有關:

> ffffffff82f76288::walk blocked |::findstack -v
stack pointer
for thread fffffe80000b9c80: fffffe80000b9a70
[ fffffe80000b9a70 resume_from_intr
+0xbb() ]
fffffe80000b9ab0 swtch
+0x9f()
fffffe80000b9b50 turnstile_block
+0x76b(ffffffff93338a00, 1, ffffffff82f76288, fffffffffbc05908, 0, 0)
fffffe80000b9bb0 rw_enter_sleep
+0x1de(ffffffff82f76288, 1)
fffffe80000b9c00 e1000g_intr
+0x94(ffffffff82f76000)
fffffe80000b9c60 av_dispatch_autovect
+0x7b(1b)
fffffe80000b9c70 intr_thread
+0x50()
stack pointer
for thread fffffe80000ddc80: fffffe80000dd9d0
[ fffffe80000dd9d0 _resume_from_idle
+0xf8() ]
fffffe80000dda10 swtch
+0x167()
fffffe80000ddab0 turnstile_block
+0x76b(ffffffff93338a00, 1, ffffffff82f76288, fffffffffbc05908, 0, 0)
fffffe80000ddb10 rw_enter_sleep
+0x1de(ffffffff82f76288, 1)
fffffe80000ddb40 e1000g_tx_cleanup
+0x56(ffffffff82f76000)
fffffe80000ddb80 e1000g_LocalTimer
+0x19(ffffffff82f76000)
fffffe80000ddbd0 callout_execute
+0xb1(ffffffff80a3e000)
fffffe80000ddc60 taskq_thread
+0x1a7(ffffffff826be7a0)
fffffe80000ddc70 thread_start
+8()
stack pointer
for thread fffffe800027bc80: fffffe800027b1a0
[ fffffe800027b1a0 _resume_from_idle
+0xf8() ]
fffffe800027b1e0 swtch
+0x167()
fffffe800027b280 turnstile_block
+0x76b(ffffffff93338a00, 1, ffffffff82f76288, fffffffffbc05908, 0, 0)
fffffe800027b2e0 rw_enter_sleep
+0x1de(ffffffff82f76288, 1)
fffffe800027b320 e1000g_m_tx
+0x3f(ffffffff82f76000, ffffffff9054d540)
fffffe800027b340 dls_tx
+0x1d(ffffffff82f2ebe8, ffffffff9054d540)
fffffe800027b370 dld_tx_single
+0x2a(ffffffff83a75888, ffffffff9054d540)
fffffe800027b400 proto_unitdata_req
+0x1a0(ffffffff83a75888, ffffffff9ad3c970, ffffffff9ad3f780)
fffffe800027b420 dld_proto
+0x84(ffffffff83a75888, ffffffff9ad3f780)
fffffe800027b460 dld_wput
+0xe2(ffffffff894ab138, ffffffff9ad3f780)
fffffe800027b4d0 putnext
+0x22b(ffffffff8ed2de10, ffffffff9ad3f780)
fffffe800027b5a0 ar_xmit
+0x2d3(ffffffff84eef8e8, 1, 800, 4, ffffffff83717840, ffffffff9a09046c, ffffffff837177f0,

ffffffff9a090468
, ffffffff837177f0)
fffffe800027b620 ar_query_xmit
+0xf2(ffffffff8ed2c798, ffffffff8ed25bd8)
fffffe800027b690 ar_entry_query
+0x361(ffffffff894abd10, ffffffff9a099240)
fffffe800027b6d0 ar_cmd_dispatch
+0x12e(ffffffff894abd10, ffffffff9a099240)
fffffe800027b790 ar_rput
+0x62c(ffffffff894abd10, ffffffff9a099240)
fffffe800027b800 putnext
+0x22b(ffffffff894ac7e8, ffffffff9a099240)
fffffe800027b940 ip_newroute
+0xf4e(ffffffff919c2110, ffffffffab8dbb80, f603000b, 0, ffffffff9187e000, 0)
fffffe800027ba00 ip_output
+0xc7b(ffffffff9187e000, ffffffffab8dbb80, ffffffff919c2110, 2)
fffffe800027bad0 tcp_send_data
+0x174(ffffffff9187e200, ffffffff919c2110, ffffffffab8dbb80)
fffffe800027bb20 tcp_timer
+0x942(ffffffff9187e000)
fffffe800027bb60 tcp_timer_handler
+0x37(ffffffff9187e000, ffffffff919085f8, ffffffff82e8bf00)
fffffe800027bbf0 squeue_drain
+0x1e0(ffffffff82e8bf00, 2, af62fce930)
fffffe800027bc60 squeue_worker
+0x10e(ffffffff82e8bf00)
fffffe800027bc70 thread_start
+8()
stack pointer
for thread ffffffff838470c0: fffffe8000a38890
[ fffffe8000a38890 _resume_from_idle
+0xf8() ]
fffffe8000a388d0 swtch
+0x167()
fffffe8000a38970 turnstile_block
+0x76b(0, 0, ffffffff82f76288, fffffffffbc05908, 0, 0)
fffffe8000a389d0 rw_enter_sleep
+0x16b(ffffffff82f76288, 0)
fffffe8000a38a40 e1000g_m_stat
+0x44(ffffffff82f76000, 3e8, fffffe8000a38a68)
fffffe8000a38a80 mac_stat_get
+0x73(ffffffff83318a88, 3e8)
fffffe8000a38ad0 i_dls_stat_update
+0x67(ffffffff894b4640, 0)
fffffe8000a38ca0 read_kstat_data
+0x142(fffffe8000a38e9c, 80c8b80, 100001)
fffffe8000a38ce0 kstat_ioctl
+0x4a(5a00000000, 4b02, 80c8b80, 100001, ffffffff9067d858, fffffe8000a38e9c)
fffffe8000a38d20 cdev_ioctl
+0x48(5a00000000, 4b02, 80c8b80, 100001, ffffffff9067d858, fffffe8000a38e9c)
fffffe8000a38d60 spec_ioctl
+0x86(ffffffff82f48880, 4b02, 80c8b80, 100001, ffffffff9067d858, fffffe8000a38e9c)
fffffe8000a38dc0 fop_ioctl
+0x37(ffffffff82f48880, 4b02, 80c8b80, 100001, ffffffff9067d858, fffffe8000a38e9c)
fffffe8000a38ec0 ioctl
+0x16b(4, 4b02, 80c8b80)
fffffe8000a38f10 sys_syscall32
+0x101()

那麼,現在的問題是,這樣的情況是否正常呢?是否它們和系統hang有關呢?

我們知道,Linux的中斷處理函數中是隻能用自旋鎖的,中斷處理函數阻塞將會導致災難。
和Linux不同,Solaris的中斷服務是由中斷線程來完成的,中斷線程中可以阻塞並睡眠;因此,到目前爲止,似乎沒有什麼異常。

但是,考慮死鎖的情況,如果擁有該rwlock的線程因爲某種原因而無法釋放該鎖,那麼這4個線程就永遠無法得到執行,這樣肯定就不是正常情況了。

因此,我們需要找到這個rwlock的擁有者,檢查它的狀態是否正確。

尋找鎖的擁有者

我們把系統內所有內核線程的棧全部得到,並保存到一個臨時文件中:

>::log -e /tmp/a.log
>::threadlist -v
>::log -d

然後,用vi打開這個臨時文件a.log,查找包含e1000g的所有線程。

在a.log裏一共有5個e1000g線程,其中4個是阻塞在那個rwlock上的線程,剩下的唯一1個的調用棧如下:

stack pointer for thread fffffe80044e5c80: fffffe80044e5880
[ fffffe80044e5880 _resume_from_idle
+0xf8() ]
fffffe80044e58c0 swtch
+0x167()
fffffe80044e5930 cv_timedwait
+0xcf(ffffffff82f76390, ffffffff82f76388, 1036d)
fffffe80044e59c0 cv_timedwait_sig
+0x2cc(ffffffff82f76390, ffffffff82f76388, 1036d)
fffffe80044e5a70 e1000g_send
+0x136(ffffffff82f76370, ffffffffac2fce40)
fffffe80044e5ab0 e1000g_m_tx
+0x6f(ffffffff82f76000, ffffffffa21f8180)
fffffe80044e5ad0 dls_tx
+0x1d(ffffffff82f2ec80, ffffffffa21f8180)
fffffe80044e5b20 dld_wsrv
+0xcc(ffffffff894acb70)
fffffe80044e5b50 runservice
+0x42(ffffffff894acb70)
fffffe80044e5b80 queue_service
+0x42(ffffffff894acb70)
fffffe80044e5bc0 stream_service
+0x73(ffffffff83905740)
fffffe80044e5c60 taskq_d_thread
+0xbb(ffffffff833af820)
fffffe80044e5c70 thread_start
+8()


那麼這個線程是否是那個rwlock的唯一讀者呢?如果手頭有代碼的話,那就容易驗證了;只需要看一下e1000g_m_tx的源代碼和上面的調用棧既可以知道了;可惜手頭沒有源代碼,只能看反彙編的代碼了:

> e1000g_m_tx::dis
e1000g_m_tx: pushq
%rbp
e1000g_m_tx
+1: movq %rsp,%rbp
e1000g_m_tx
+4: subq $0x10,%rsp
e1000g_m_tx
+8: pushq %r12
e1000g_m_tx
+0xa: pushq %r13
e1000g_m_tx
+0xc: pushq %r14
e1000g_m_tx
+0xe: pushq %r15
e1000g_m_tx
+0x10: movq %rdi,-0x8(%rbp)
e1000g_m_tx
+0x14: movq %rsi,-0x10(%rbp)
e1000g_m_tx
+0x18: movq %rsi,%r13
e1000g_m_tx
+0x1b: movq %rdi,%r14
e1000g_m_tx
+0x1e: movq %r14,%r15
e1000g_m_tx
+0x21: addq $0x370,%r15
e1000g_m_tx
+0x28: movq %r14,%rdi
e1000g_m_tx
+0x2b: addq $0x288,%rdi
e1000g_m_tx
+0x32: movq %rdi,%r12
e1000g_m_tx
+0x35: movl $0x1,%esi
e1000g_m_tx
+0x3a: call +0xb2ce471 <rw_enter>
e1000g_m_tx
+0x3f: cmpl $0x0,0x238(%r14)
e1000g_m_tx
+0x47: jne +0xb <e1000g_m_tx+0x54>
e1000g_m_tx
+0x49: movq %r13,%rdi
e1000g_m_tx
+0x4c: call +0xb4b7e1f <freemsgchain>
e1000g_m_tx
+0x51: xorq %r13,%r13
e1000g_m_tx
+0x54: testq %r13,%r13
e1000g_m_tx
+0x57: je +0x28 <e1000g_m_tx+0x81>
e1000g_m_tx
+0x59: movq 0x0(%r13),%r14
e1000g_m_tx
+0x5d: xorq %r8,%r8
e1000g_m_tx
+0x60: movq %r8,0x0(%r13)
e1000g_m_tx
+0x64: movq %r15,%rdi
e1000g_m_tx
+0x67: movq %r13,%rsi
e1000g_m_tx
+0x6a: call +0x31 <e1000g_send>
e1000g_m_tx
+0x6f: testl %eax,%eax
e1000g_m_tx
+0x71: je +0xa <e1000g_m_tx+0x7d>
e1000g_m_tx
+0x73: movq %r14,%r13
e1000g_m_tx
+0x76: testq %r14,%r14
e1000g_m_tx
+0x79: jne -0x22 <e1000g_m_tx+0x59>
e1000g_m_tx
+0x7b: jmp +0x4 <e1000g_m_tx+0x81>
e1000g_m_tx
+0x7d: movq %r14,0x0(%r13)
e1000g_m_tx
+0x81: movq %r12,%rdi
e1000g_m_tx
+0x84: call +0xb2ce4a7 <rw_exit>
e1000g_m_tx
+0x89: movq %r13,%rax
e1000g_m_tx
+0x8c: popq %r15
e1000g_m_tx
+0x8e: popq %r14
e1000g_m_tx
+0x90: popq %r13
e1000g_m_tx
+0x92: popq %r12
e1000g_m_tx
+0x94: leave
e1000g_m_tx
+0x95: ret

顯然,e1000g_m_tx在調用e1000g_send時,已經執行過rw_enter了,而該線程卻阻塞在cv_timedwait上,狀態是sleep:

> fffffe80044e5c80::thread -i
ADDR STATE FLG PFLG SFLG PRI EPRI PIL INTR
fffffe80044e5c80 sleep
8 0 3 60 0 0 n/a

那麼這個線程有可能被喚醒執行嗎?如果可以的話,死鎖就不應該發生。這就需要進一步檢查這個線程的狀態。

關於cv_timedwait

Kernel Functions for Drivers condvar(9F)

NAME
condvar, cv_init, cv_destroy, cv_wait, cv_signal,
cv_broadcast, cv_wait_sig, cv_timedwait, cv_timedwait_sig
-
condition variable routines

SYNOPSIS
#include
<sys/ksynch.h>
..................................................
..................................................
..................................................

clock_t cv_timedwait(kcondvar_t
*cvp, kmutex_t *mp, clock_t timeout);

clock_t cv_timedwait_sig(kcondvar_t
*cvp, kmutex_t *mp, clock_t timeout);


timeout A time,
in absolute ticks since boot, when
cv_timedwait() or cv_timedwait_sig() should
return.

可以看到,cv_timedwait的第3個參數就是就是時間參數,從前面的調用棧裏,就可以找到,是1036d;
到了這個時間,cv_timedwait就應該返回,線程也就被喚醒;那麼,時間到了嗎?我們查看一下:

> fffffe80044e5c80::thread -d
ADDR DISPTIME BOUND PR
fffffe80044e5c80 1036c
-1 0
> 1036d-1036c=D
1
> *lbolt=X
18a4f
> 18a4f-1036c=D
34531

可以看到,線程fffffe80044e5c80的DISPTIME是1036c,一個tick後就應該被喚醒,可是系統並沒有被喚醒,在我們強制系統crashdump時,滴答值,即lbolt,已經累加到18a4f,也就是過了規定時間後的34531個tick,線程仍舊在sleep。

這就意味着,fffffe80044e5c80永遠也不會被喚醒,那麼其它4個阻塞在rwlock的線程也永遠不會被喚醒;我們記得其中之一就是中斷線程,那麼e1000g驅動就永遠不會相應網卡中斷了。顯然,這已經是bug了;

可是,爲什麼cv_timedwait沒有按照手冊上規定的行爲工作呢?

感興趣的話,就看內核源代碼。

cv_timedwait調用realtime_timeout在內核的callout table註冊一項,在指定的時間上註冊執行setrun函數,該函數的參數就是調用cv_timedwait的線程,即當前內核線程的指針kthread_t:

http://src.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/common/os/condvar.c#199

/*
* Same as cv_wait except the thread will unblock at 'tim'
* (an absolute time) if it hasn't already unblocked.
*
* Returns the amount of time left from the original 'tim' value
* when it was unblocked.
*/
clock_t
cv_timedwait(kcondvar_t
*cvp, kmutex_t *mp, clock_t tim)
{
kthread_t
*t = curthread;
timeout_id_t id;
clock_t timeleft;
int signalled;

if (panicstr)
return (-1);

timeleft
= tim - lbolt;
if (timeleft <= 0)
return (-1);
id
= realtime_timeout((void (*)(void *))setrun, t, timeleft);
thread_lock(t);
/* lock the thread */
cv_block((condvar_impl_t
*)cvp);
thread_unlock_nopreempt(t);
mutex_exit(mp);
if ((tim - lbolt) <= 0) /* allow for wrap */
setrun(t);
swtch();

註冊在callout table中的setrun函數會到期執行,就調用setrun_locked把當時的線程喚醒:

http://src.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/common/disp/thread.c#1161


void
setrun(kthread_t
*t)
{
thread_lock(t);
setrun_locked(t);
thread_unlock(t);
}

對本例來說,就是把調用過cv_timedwait,處於sleep狀態的線程喚醒:

http://src.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/common/disp/thread.c#1099

/*
* Set the thread running; arrange for it to be swapped in if necessary.
*/
void
setrun_locked(kthread_t
*t)
{
ASSERT(THREAD_LOCK_HELD(t));
if (t->t_state == TS_SLEEP) {
/*
* Take off sleep queue.
*/
SOBJ_UNSLEEP(t
->t_sobj_ops, t);

因此函數cv_timedwait的實現機制是依賴於Solaris內核callout隊列機制,如果cv_timedwait沒有按照手冊上規定的行爲工作,則很有可能是因爲callout機制出了問題。

關於callout隊列

那麼,系統callout機制是否工作正常呢?

首先看調用cv_timedwait不能超時返回的線程:

stack pointer for thread fffffe80044e5c80: fffffe80044e5880
[ fffffe80044e5880 _resume_from_idle
+0xf8() ]
fffffe80044e58c0 swtch
+0x167()
fffffe80044e5930 cv_timedwait
+0xcf(ffffffff82f76390, ffffffff82f76388, 1036d)
fffffe80044e59c0 cv_timedwait_sig
+0x2cc(ffffffff82f76390, ffffffff82f76388, 1036d)
fffffe80044e5a70 e1000g_send
+0x136(ffffffff82f76370, ffffffffac2fce40)
fffffe80044e5ab0 e1000g_m_tx
+0x6f(ffffffff82f76000, ffffffffa21f8180)
fffffe80044e5ad0 dls_tx
+0x1d(ffffffff82f2ec80, ffffffffa21f8180)
fffffe80044e5b20 dld_wsrv
+0xcc(ffffffff894acb70)
fffffe80044e5b50 runservice
+0x42(ffffffff894acb70)
fffffe80044e5b80 queue_service
+0x42(ffffffff894acb70)
fffffe80044e5bc0 stream_service
+0x73(ffffffff83905740)
fffffe80044e5c60 taskq_d_thread
+0xbb(ffffffff833af820)
fffffe80044e5c70 thread_start
+8()

檢查系統全局的callout隊列,正如我們之前發現的,這個線程對應的callout表項已經嚴重過期了:

> ::callout ! grep fffffe80044e5c80
setrun fffffe80044e5c80 3ffffffffffe1a80 1036d (T
-33730)

其中T-33730表示已經過期了33730個tick。

用mdb打印出所有callout表項,我們發現,系統中有過期的表項還有很多,用wc算一下,有2573個。

> ::callout
FUNCTION ARGUMENT ID TIME
sigalarm2proc ffffffff9569aae0 7fffffffffffc010
144a1 (T
-17038)
sigalarm2proc ffffffff91bb7510 7fffffffffffe010
14484 (T-17067)
sigalarm2proc ffffffff9569c380 7fffffffffffc020
144a1 (T
-17038)
sigalarm2proc ffffffff95428d48 7fffffffffffc030
144a1 (T
-17038)
sigalarm2proc ffffffff91bb8db0 7fffffffffffe030
14483 (T-17068)
sigalarm2proc ffffffff9542b238 7fffffffffffc040
144a1 (T
-17038)
.....................[snipped].................................... ................................

> ::callout ! grep "T-" | wc -l
2573

看來callout機制似乎失靈了,這也是導致cv_timedwait的不工作的直接原因。

下面我們就看一下callout到底出了什麼問題。這還得從callout的代碼開始看起。

首先,系統在每個tick執行時鐘中斷處理時進入clock例程,這個例程會調用callout_schedule來處理全局的callout隊列:

http://src.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/common/os/clock.c#692

static void
clock(
void)
{
..........................
..........................
..........................

/*
* Schedule timeout() requests if any are due at this time.
*/
callout_schedule();

在callout_schedule裏,會循環遍歷所有調出表,把調出表的入口傳遞給callout_schedule_1:

/*
* Schedule callouts for all callout tables. Called by clock() on each tick.
*/

void
callout_schedule(
void)
{
int f, t;

if (cpr_stop_callout)
return;

for (t = 0; t < CALLOUT_NTYPES; t++)
for (f = 0; f < callout_fanout; f++)
callout_schedule_1(callout_table[CALLOUT_TABLE(t, f)]);
}

而在callout_schedule_1會遍歷給定調出表中的調出項,選擇用兩種不同的方式執行callout_execute。

http://src.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/common/os/callout.c#295

/*
* Schedule any callouts that are due on or before this tick.
*/
static void
callout_schedule_1(callout_table_t
*ct)
{
callout_t
*cp;
clock_t curtime, runtime;

mutex_enter(
&ct->ct_lock);
ct
->ct_curtime = curtime = lbolt;
while (((runtime = ct->ct_runtime) - curtime) <= 0) {
for (cp = ct->ct_lbhash[CALLOUT_LBHASH(runtime)];
cp
!= NULL; cp = cp->c_lbnext) {
if (cp->c_runtime != runtime ||
(cp
->c_xid & CALLOUT_EXECUTING))
continue;
mutex_exit(
&ct->ct_lock);
if (ct->ct_taskq == NULL)
softcall((
void (*)(void *))callout_execute, ct);
else
(
void) taskq_dispatch(ct->ct_taskq,
(task_func_t
*)callout_execute, ct,
KM_NOSLEEP);
return;
}
ct
->ct_runtime++;
}
mutex_exit(
&ct->ct_lock);
}

總結下來callout隊列的執行通常是經過如下code path:

1. softcall

clock -> callout_schedule -> callout_schedule_1 ->通過softcall產生一個PIL爲1的軟中斷執行callout_execute

2. taskq

clock -> callout_schedule -> callout_schedule_1 ->通過dipatch一個獨立的taskq線程來執行callout_execute

下面看看callout_execute的實現:

http://src.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/common/os/callout.c#241

/*
* Do the actual work of executing callouts. This routine is called either
* by a taskq_thread (normal case), or by softcall (realtime case).
*/
static void
callout_execute(callout_table_t
*ct)
{
callout_t
*cp;
callout_id_t xid;
clock_t runtime;
timestruc_t now;
int64_t hresms;

mutex_enter(
&ct->ct_lock);
....[snipped]..............
cp
->c_executor = curthread;
cp
->c_xid = xid |= CALLOUT_EXECUTING;
mutex_exit(
&ct->ct_lock);
DTRACE_PROBE1(callout__start, callout_t
*, cp);
(
*cp->c_func)(cp->c_arg);
DTRACE_PROBE1(callout__end, callout_t
*, cp);
mutex_enter(
&ct->ct_lock);
....[snipped]..............
mutex_exit(
&ct->ct_lock);
}

我們看到,在callout_execute執行中會用到mutex_enter(&ct->ct_lock)來保證互斥訪問 callout表項的內容,但在執行真正的定時器函數時,它會調用mutex_exit(&ct->ct_lock)釋放掉鎖。因此,可以有多個taskq線程或者軟中斷併發執行調出函數,而不會相互影響。

此前,我沒仔細看callout_execute的代碼,犯了一個嚴重錯誤,幸好有朋友指出,在這裏再次表示謝意。希望之前的錯誤沒有誤導大家。

回過頭來看線程fffffe80000ddc80的調用棧,顯然它是由e1000g驅動調用timeout(9F)註冊的e1000g_LocalTimer函數,而且callout_execute是由單獨的taskq線程執行的:

stack pointer for thread fffffe80000ddc80: fffffe80000dd9d0
[ fffffe80000dd9d0 _resume_from_idle
+0xf8() ]
fffffe80000dda10 swtch
+0x167()
fffffe80000ddab0 turnstile_block
+0x76b(ffffffff93338a00, 1, ffffffff82f76288, fffffffffbc05908, 0, 0)
fffffe80000ddb10 rw_enter_sleep
+0x1de(ffffffff82f76288, 1)
fffffe80000ddb40 e1000g_tx_cleanup
+0x56(ffffffff82f76000)
fffffe80000ddb80 e1000g_LocalTimer
+0x19(ffffffff82f76000)
fffffe80000ddbd0 callout_execute
+0xb1(ffffffff80a3e000)
fffffe80000ddc60 taskq_thread
+0x1a7(ffffffff826be7a0)
fffffe80000ddc70 thread_start
+8()

這個線程執行過程中,又因爲等待一個讀寫鎖而睡眠,我們前面分析出這個鎖的所有者是fffffe80044e5c80,它因爲調用cv_timedwait而睡眠等待它被callout機制來喚醒:

stack pointer for thread fffffe80044e5c80: fffffe80044e5880
[ fffffe80044e5880 _resume_from_idle
+0xf8() ]
fffffe80044e58c0 swtch
+0x167()
fffffe80044e5930 cv_timedwait
+0xcf(ffffffff82f76390, ffffffff82f76388, 1036d)
fffffe80044e59c0 cv_timedwait_sig
+0x2cc(ffffffff82f76390, ffffffff82f76388, 1036d)
fffffe80044e5a70 e1000g_send
+0x136(ffffffff82f76370, ffffffffac2fce40)
fffffe80044e5ab0 e1000g_m_tx
+0x6f(ffffffff82f76000, ffffffffa21f8180)
fffffe80044e5ad0 dls_tx
+0x1d(ffffffff82f2ec80, ffffffffa21f8180)
fffffe80044e5b20 dld_wsrv
+0xcc(ffffffff894acb70)
fffffe80044e5b50 runservice
+0x42(ffffffff894acb70)
fffffe80044e5b80 queue_service
+0x42(ffffffff894acb70)
fffffe80044e5bc0 stream_service
+0x73(ffffffff83905740)
fffffe80044e5c60 taskq_d_thread
+0xbb(ffffffff833af820)
fffffe80044e5c70 thread_start
+8()

而cv_timedwait是通過realtime_timeout註冊callout表項的,這意味着,callout_execute要通過softcall機制來執行,進而調用到setrun函數喚醒該線程。

而我們已經知道,fffffe80044e5c80調用cv_timedwait後從未返回,而且已經嚴重過期:

> ::callout ! grep fffffe80044e5c80
setrun fffffe80044e5c80 3ffffffffffe1a80 1036d (T
-33730)

那麼,爲什麼會這樣呢?很自然,我們需要了解softcall是如何實現的。

關於softcall

http://cvs.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/common/os/softint.c#103

/*
* Call function func with argument arg
* at some later time at software interrupt priority
*/
void
softcall(
void (*func)(void *), void *arg)
{
softcall_t
*sc;

/*
* protect against cross-calls
*/
mutex_enter(
&softcall_lock);
/* coalesce identical softcalls */
for (sc = softhead; sc != 0; sc = sc->sc_next) {
if (sc->sc_func == func && sc->sc_arg == arg) {
mutex_exit(
&softcall_lock);
return;
}
}

if ((sc = softfree) == 0)
panic(
"too many softcalls");
softfree
= sc->sc_next;
sc
->sc_func = func;
sc
->sc_arg = arg;
sc
->sc_next = 0;

if (softhead) {
softtail
->sc_next = sc;
softtail
= sc;
mutex_exit(
&softcall_lock);
}
else {
softhead
= softtail = sc;
if (softcall_state == SOFT_DRAIN)
/*
* softint is already running; no need to
* raise a siron. Due to lock protection of
* softhead / softcall state, we know
* that softint() will see the new addition to
* the softhead queue.
*/
mutex_exit(
&softcall_lock);
else {
softcall_state
= SOFT_PEND;
mutex_exit(
&softcall_lock);
siron();
}
}
}

可以看到,softcall會把需要執行的函數放入一個內核全局的隊列並交由系統處理,softhead指針可以訪問這個隊列:

http://cvs.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/common/os/softint.c#73

static softcall_t softcalls[NSOFTCALLS], *softhead, *softtail, *softfree;

從源代碼裏,我們看到,全局變量softcall_state用來標識當前softcall隊列的狀態,首次排隊時,隊列狀態會被置爲待處理態,SOFT_PEND。然後調用一個siron()來在CPU上產生一個軟中斷。

其中,softcall_state的狀態定義如下:

http://cvs.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/common/os/softint.c#60

/*
* Defined states for softcall processing.
*/
#define SOFT_IDLE 0x01 /* no processing is needed */
#define SOFT_PEND 0x02 /* softcall list needs processing */
#define SOFT_DRAIN 0x04 /* the list is being processed */

下面我們就用mdb遍歷這個隊列,發現隊列中有2個待執行的callout_execute調用:

>*softhead::list softcall_t sc_next|::print softcall_t
{
sc_func
= callout_execute
sc_arg
= 0xffffffff80219000
sc_next
= softcalls+0x1290
}
{
sc_func
= callout_execute
sc_arg
= 0xffffffff80216000
sc_next
= 0
}

>softcall_state/J
softcall_state:
softcall_state:
2



mdb讀出當時隊列狀態是0x2,就是SOFT_PEND,待處理狀態。

那麼我們阻塞在cv_timedwait上的線程fffffe80044e5c80,是否屬於這兩個待處理的callout_execute之一呢?

sc_arg是callout_execute的參數,類型是callout_table_t,我們用mdb查看一下:

> 0xffffffff80219000::print callout_table_t
{
ct_lock
= {
_opaque
= [ 0 ]
}
ct_freelist
= 0xffffffffad201c38
ct_curtime
= 0x1872f
ct_runtime
= 0x1036d
ct_taskq
= 0
ct_short_id
= 0x3ffffffffffe1a80
ct_long_id
= 0x7ffffffffffd91a0
ct_idhash
= [ 0, 0xffffffffa40a9560, 0xffffffffa40a9600, 0xffffffffa40a9740, 0xffffffffa40a9880, 0xffffffffa40a9970,
0xffffffffa9383ce0, 0xffffffffa40a9b50, 0xffffffffa40a9c40, 0xffffffffa40a9d30, 0xffffffffa40a9e70, 0xffffffffa409f068,
0xffffffffa409f108, 0xffffffffa409f248, 0xffffffffa409f338, 0xffffffffa409f478, 0xffffffffa409f568, 0xffffffffa409f6a8,
0xffffffffa409f798, 0xffffffffa409f888, 0xffffffffa9383c90, 0xffffffffa409fa68, 0xffffffffa1a2e878, 0xffffffffa409fb08,
0xffffffffacfbb4c8, 0xffffffffa409fd38, 0xffffffffa1a2eeb8, 0xffffffffa40ba008, 0xffffffffa40ba198, 0xffffffffa40ba2d8,
0xffffffffa40ba3c8, 0xffffffffa40ba4b8, ... ]
ct_lbhash
= [ 0, 0, 0, 0, 0xffffffffad188ca0, 0, 0, 0, 0, 0, 0, 0, 0, 0xffffffff8ff5a658, 0, 0, 0xffffffff8276e878, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xffffffffad1d0528, ... ]
}

非常的幸運,ct_short_id鏈表裏的第一個項就是我們cv_timedwait註冊的那個setrun的表項,我們不用再遍歷整個鏈表了:)

> ::callout ! grep fffffe80044e5c80
setrun fffffe80044e5c80 3ffffffffffe1a80 1036d (T
-33730)

看來,cv_timedwait沒有按照預想原因返回的根源我們找到了,是softcall隊列嚴重推遲引起的,而且,看起來softcall隊列永遠不可能繼續被處理了。


那麼爲什麼會這樣呢?很自然我們想到要查看CPU的狀態,因爲softcall隊列是軟中斷處理的,軟中斷線程的PIL一般是1,比一般線程的優先級都要高,除非CPU上有其它中斷,否則應該會立即得到處理。

> ::cpuinfo -v
ID ADDR FLG NRUN BSPL PRI RNRN KRNRN SWITCH THREAD PROC
0 fffffffffbc27730 1f 1 6 169 no no t-3 fffffe80000bfc80 sched
| | |
RUNNING
<--+ | +--> PIL THREAD
READY
| 10 fffffe80000bfc80
QUIESCED
| 6 fffffe80000b9c80
EXISTS
|
ENABLE
+--> PRI THREAD PROC
99 fffffe80000d1c80 sched

ID ADDR FLG NRUN BSPL PRI RNRN KRNRN SWITCH THREAD PROC
1 fffffffffbc2f260 1b 1 0 -1 no no t-17 fffffe8000401c80 (idle)
| |
RUNNING
<--+ +--> PRI THREAD PROC
READY
60 fffffe80044d9c80 sched
EXISTS
ENABLE

其實,前面我們已經看到系統中有兩個CPU, CPU1是空閒的,而CPU0的BSPL是6,意味着6以下的中斷都不能打斷當前執行的線程,而這個BSPL正是網卡中斷線程設置的,用來屏蔽低優先級中斷。

因爲我們的softcall隊列保持了相當長的PEND狀態,那就意味着似乎這些待處理的softcall被分配到CPU0上來執行。

是不是這樣呢?我們可以用mdb檢查CPU結構cpu_t中成員machcpu的狀態,即可知到softcall被分配到了哪一個CPU上:

CPU0:

> fffffffffbc27d10::print struct machcpu mcpu_softinfo
{
mcpu_softinfo.st_pending
= 0x416
}

CPU
1:

> fffffffffbc2f840::print struct machcpu mcpu_softinfo
{
mcpu_softinfo.st_pending
= 0x404
}

可以看到,CPU0上有PIL 1,2,4,10的中斷待處理。CPU1有4和10待處理。其中PIL爲1的中斷就是處理softcall隊列的軟中斷。
如果用mdb查看,該軟中斷處理函數是softlevel1,PIL是1:

> ::softint
ADDR PEND PIL ARG1 ARG2 ISR(s)
ffffffff8277a5c0
0 1 ffffffff8048da80 0 errorq_intr
fffffffffbc05ae8
0 1 0 0 softlevel1
ffffffff8277a4c0
0 2 ffffffff8048dd00 0 errorq_intr
fffffffffbc00070
0 2 0 0 cbe_low_level
ffffffff83a706c0
0 4 ffffffff90004d18 0 ghd_doneq_process
ffffffff8ed31880
0 4 ffffffff90004d18 0 ghd_timeout_softintr
ffffffff83946e00
0 4 ffffffff8f046c40 0 power_soft_intr
ffffffff83a70c00
0 4 ffffffff83b1b000 0 bge_chip_factotum
ffffffff83a70cc0
0 4 ffffffff83b1b000 0 bge_reschedule
ffffffff8277a2c0
0 4 0 0 asysoftintr
ffffffff82f4d100
0 9 ffffffff82f76370 0 e1000g_tx_softint_worker
ffffffff82f4df00
0 9 ffffffff82f86370 0 e1000g_tx_softint_worker
ffffffff833b3e80
0 9 ffffffff801af7e8 0 hcdi_soft_intr
ffffffff8277a000
0 9 ffffffff801afb68 0 hcdi_soft_intr
fffffffffbc00030
0 10 0 0 cbe_softclock

但實際上我們前面也知道,CPU0上的網卡中斷線程fffffe80000b9c80已經睡眠在了讀寫鎖上,而讀寫鎖的主人fffffe80044e5c80,此刻卻在callout隊列中等待被喚醒來繼續執行。
而callout的執行又依賴於CPU0能夠處理softcall隊列中callout_execute,但CPU0上的軟中斷已經被網卡中斷線程fffffe80000b9c80通過BSPL屏蔽掉了。
此時,死鎖已經發生,線程fffffe80000b9c80和fffffe80044e5c80永遠的互相等待下去了。

當然,上述死鎖條件的成立還得基於以下假設:

softcall一旦被分派,就不能重新調度到其它CPU上。本例中,即使CPU1空閒,也不能通過cross-call,或者叫處理器間中斷來重新分配softcall隊列的處理。

顯然,這個假設似乎不合道理,如果真是這樣的話,那可是內核的bug,我們的分析再次陷入困境。

因此,只好發郵件給OpenSolaris.org社區的code郵件列表了,終於,這個假設得到了確認,因爲內核中這兩個bug,我們的假設是成立的:

http://www.opensolaris.org/jive/thread.jspa?threadID=38081&tstart=30

http://www.opensolaris.org/jive/thread.jspa?threadID=38118&tstart=30

6292092 callout should not be blocked by interrupts from executing realtime timeouts
6540436 kpreempt() needs a more reliable way to generate level1 intr

如果上面兩個內核的bug被修復,死鎖還會發生嗎?

答案不難得出:如果是SMP系統,大概不會發生了。但UP系統,單CPU,即便沒有了上面兩個bug,系統一樣會死鎖。

所以,這個死鎖到底還是e1000g驅動的問題。好在這只是e1000g試驗版本的一個錯誤,顯然,我們在中斷處理函數中引入讀寫鎖是有問題的。


小結


1. 系統hang的root cause是e1000g的rwlock的不當使用,導致死鎖的發生;

2. 關於callout

e1000g_LocalTimer顯然是e1000g註冊的定時器函數,但在這個函數中卻試圖使用一個rwlock,而導致了在 callout_execute調用中的睡眠。而我們知道,callout_execute睡眠會引起系統全局的callout table被鎖住,callout機制無法使用;因此要避免在e1000g_LocalTimer中調用引起阻塞的代碼或者函數;

3. Solaris Internal第二版的bug:

解決這個問題的事後,偶然發現Solaris Internal第二版的英文原版836頁第17章圖17.6存在一個bug。

這個圖是關於rwlock的,其中關於讀鎖的圖中,關於讀者記數器的起始位,應該是3-31.63,而書上說是4。

這個可以從源代碼中得到證實:
http://src.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/common/os/rwlock.c#40

/*
* Big Theory Statement for readers/writer locking primitives.
*
* An rwlock provides exclusive access to a single thread ("writer") or
* concurrent access to multiple threads ("readers"). See rwlock(9F)
* for a full description of the interfaces and programming model.
* The rest of this comment describes the implementation.
*
* An rwlock is a single word with the following structure:
*
* ---------------------------------------------------------------------
* | OWNER (writer) or HOLD COUNT (readers) | WRLOCK | WRWANT | WAIT |
* ---------------------------------------------------------------------
* 63 / 31 .. 3 2 1 0

這我也從crashdump中得到了驗證,檢查讀寫鎖實際的值,你會發現,是0xb, 其中第3位恰好是1,即只有1個reader。

> ffffffff82f76288::rwlock
ADDR OWNER/COUNT FLAGS WAITERS
ffffffff82f76288 READERS=1 B011 ffffffff838470c0 (W)
|| fffffe800027bc80 (R)
WRITE_WANTED -------+| fffffe80000ddc80 (R)
HAS_WAITERS --------+ fffffe80000b9c80 (R)
>
> ffffffff82f76288/J
0xffffffff82f76288: b
> ffffffff82f76288/R
0xffffffff82f76288: 1011

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章