## 4.5.?調試系統故障
即便你已使用了所有的監視和調試技術, 有時故障還留在驅動里, 當驅動執行時系統出錯. 當發生這個時, 能夠收集盡可能多的信息來解決問題是重要的.
注意"故障"不意味著"崩潰". Linux 代碼是足夠健壯地優雅地響應大部分錯誤:一個故障常常導致當前進程的破壞而系統繼續工作. 系統可能崩潰, 如果一個故障發生在一個進程的上下文之外, 或者如果系統的一些至關重要的部分毀壞了. 但是當是一個驅動錯誤導致的問題, 它常常只會導致不幸使用驅動的進程的突然死掉. 當進程被銷毀時唯一無法恢復的破壞是分配給進程上下文的一些內存丟失了; 例如, 驅動通過 kmalloc 分配的動態列表可能丟失. 但是, 因為內核為任何一個打開的設備在進程死亡時調用關閉操作, 你的驅動可以釋放由 open 方法分配的東西.
盡管一個 oops 常常都不會關閉整個系統, 你很有可能發現在發生一次后需要重啟系統. 一個滿是錯誤的驅動能使硬件處于不能使用的狀態, 使內核資源處于不一致的狀態, 或者, 最壞的情況, 在隨機的地方破壞內核內存. 常常你可簡單地卸載你的破驅動并且在一次 oops 后重試. 然而, 如果你看到任何東西建議說系統作為一個整體不太好了, 你最好立刻重啟.
我們已經說過, 當內核代碼出錯, 一個提示性的消息打印在控制臺上. 下一節解釋如何解釋并利用這樣的消息. 盡管它們對新手看來相當模糊, 處理器轉儲是很有趣的信息, 常常足夠來查明一個程序錯誤而不需要附加的測試.
### 4.5.1.?oops 消息
大部分 bug 以解引用 NULL 指針或者使用其他不正確指針值來表現自己的. 此類 bug 通常的輸出是一個 oops 消息.
處理器使用的任何地址幾乎都是一個虛擬地址, 通過一個復雜的頁表結構映射為物理地址(例外是內存管理子系統自己使用的物理地址). 當解引用一個無效的指針, 分頁機制無法映射指針到一個物理地址, 處理器發出一個頁錯誤給操作系統. 如果地址無效, 內核無法"頁入"缺失的地址; 它(常常)產生一個 oops 如果在處理器處于管理模式時發生這個情況.
一個 oops 顯示了出錯時的處理器狀態, 包括CPU 寄存器內容和其他看來不可理解的信息. 消息由錯誤處理的 printk 語句產生( arch/*/kernel/traps.c )并且如同前面 "printk" 一節中描述的被分派.
我們看一個這樣的消息. 這是來自在運行 2.6 內核的 PC 上一個 NULL 指針導致的結果. 這里最相關的信息是指令指針(EIP), 錯誤指令的地址.
~~~
Unable to handle kernel NULL pointer dereference at virtual address 00000000
printing eip:
d083a064
Oops: 0002 [#1]
SMP
CPU: 0
EIP: 0060:[<d083a064>] Not tainted
EFLAGS: 00010246 (2.6.6)
EIP is at faulty_write+0x4/0x10 [faulty]
eax: 00000000 ebx: 00000000 ecx: 00000000 edx: 00000000
esi: cf8b2460 edi: cf8b2480 ebp: 00000005 esp: c31c5f74
ds: 007b es: 007b ss: 0068
Process bash (pid: 2086, threadinfo=c31c4000 task=cfa0a6c0)
Stack: c0150558 cf8b2460 080e9408 00000005 cf8b2480 00000000 cf8b2460 cf8b2460 fffffff7 080e9408 c31c4000 c0150682 cf8b2460 080e9408 00000005 cf8b2480 00000000 00000001 00000005 c0103f8f 00000001 080e9408 00000005 00000005
Call Trace:
[<c0150558>] vfs_write+0xb8/0x130
[<c0150682>] sys_write+0x42/0x70
[<c0103f8f>] syscall_call+0x7/0xb
Code: 89 15 00 00 00 00 c3 90 8d 74 26 00 83 ec 0c b8 00 a6 83 d0
~~~
寫入一個由壞模塊擁有的設備而產生的消息, 一個故意用來演示失效的模塊. faulty.c 的 write 方法的實現是瑣細的:
~~~
ssize_t faulty_write (struct file *filp, const char __user *buf, size_t count, loff_t *pos)
{
/* make a simple fault by dereferencing a NULL pointer */
*(int *)0 = 0;
return 0;
}
~~~
如你能見, 我們這里做的是解引用一個 NULL 指針. 因為 0 一直是一個無效的指針值, 一個錯誤發生, 由內核轉變為前面展示的 oops 消息. 調用進程接著被殺掉.
錯誤模塊有不同的錯誤情況在它的讀實現中:
~~~
ssize_t faulty_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
int ret;
char stack_buf[4];
/* Let's try a buffer overflow */
memset(stack_buf, 0xff, 20);
if (count > 4)
count = 4; /* copy 4 bytes to the user */
ret = copy_to_user(buf, stack_buf, count);
if (!ret)
return count;
return ret;
}
~~~
這個方法拷貝一個字串到一個本地變量; 不幸的是, 字串長于目的數組. 當函數返回時導致的緩存區溢出引起一次 oops . 因為返回指令使指令指針到不知何處, 這類的錯誤很難跟蹤, 并且你得到如下的:
~~~
EIP: 0010:[<00000000>]
Unable to handle kernel paging request at virtual address ffffffff
printing eip:
ffffffff
Oops: 0000 [#5]
SMP
CPU: 0
EIP: 0060:[<ffffffff>] Not tainted
EFLAGS: 00010296 (2.6.6)
EIP is at 0xffffffff
eax: 0000000c ebx: ffffffff ecx: 00000000 edx: bfffda7c
esi: cf434f00 edi: ffffffff ebp: 00002000 esp: c27fff78
ds: 007b es: 007b ss: 0068
Process head (pid: 2331, threadinfo=c27fe000 task=c3226150)
Stack: ffffffff bfffda70 00002000 cf434f20 00000001 00000286 cf434f00 fffffff7 bfffda70 c27fe000 c0150612 cf434f00 bfffda70 00002000 cf434f20 00000000 00000003 00002000 c0103f8f 00000003 bfffda70 00002000 00002000 bfffda70
Call Trace: [<c0150612>] sys_read+0x42/0x70 [<c0103f8f>] syscall_call+0x7/0xb
Code: Bad EIP value.
~~~
這個情況, 我們只看到部分的調用堆棧( vfs_read 和 faulty_read 丟失 ), 內核抱怨一個"壞 EIP 值". 這個抱怨和在開頭列出的犯錯的地址 ( ffffffff ) 都暗示內核堆棧已被破壞.
通常, 當你面對一個 oops, 第一件事是查看發生問題的位置, 常常與調用堆棧分開列出. 在上面展示的第一個 oops, 相關的行是:
~~~
EIP is at faulty_write+0x4/0x10 [faulty]
~~~
這里我們看到, 我們曾在函數 faulty_write, 它位于 faulty 模塊( 在方括號中列出的 ). 16 進制數指示指令指針是函數內 4 字節, 函數看來是 10 ( 16 進制 )字節長. 常常這就足夠來知道問題是什么.
如果你需要更多信息, 調用堆棧展示給你如何得知在哪里壞事的. 堆棧自己是 16 機制形式打印的; 做一點工作, 你經常可以從堆棧的列表中決定本地變量的值和函數參數. 有經驗的內核開發者可以從這里的某些模式識別中獲益; 例如, 如果你看來自 faulty_read oops 的堆棧列表:
~~~
Stack: ffffffff bfffda70 00002000 cf434f20 00000001 00000286 cf434f00 fffffff7
bfffda70 c27fe000 c0150612 cf434f00 bfffda70 00002000 cf434f20 00000000
00000003 00002000 c0103f8f 00000003 bfffda70 00002000 00002000 bfffda70
~~~
堆棧頂部的 ffffffff 是我們壞事的字串的一部分. 在 x86 體系, 缺省地, 用戶空間堆棧開始于 0xc0000000; 因此, 循環值 0xbfffda70 可能是一個用戶堆棧地址; 實際上, 它是傳遞給 read 系統調用的緩存地址, 每次下傳過系統調用鏈時都被復制. 在 x86 (又一次, 缺省地), 內核空間開始于 0xc0000000, 因此這個之上的值幾乎肯定是內核空間的地址, 等等.
最后, 當看一個 oops 列表, 一直監視本章開始討論的"slab 毒害"值. 例如,如果你得到一個內核 oops, 里面的犯錯地址時 0xa5a5a5a5a5, 你幾乎肯定 - 某個地方在初始化動態內存.
請注意, 只在你的內核是打開 CONFIG_KALLSYMS 選項而編譯時可以看到符號的調用堆棧. 否則, 你見到一個裸的, 16 機制列表, 除非你以別的方式對其解碼, 它是遠遠無用的.
### 4.5.2.?系統掛起
盡管內核代碼的大部分 bug 以 oops 消息結束, 有時候它們可能完全掛起系統. 如果系統掛起, 沒有消息打印. 例如, 如果代碼進入一個無限循環, 內核停止調度,[[15](#)] 并且系統不會響應任何動作, 包括魔術 Ctrl-Alt-Del 組合鍵. 你有 2 個選擇來處理系統掛起-- 或者事先阻止它們, 或者能夠事后調試它們.
你可阻止無限循環通過插入 schedule 引用在戰略點上. schedule 調用( 如你可能猜到的 )調度器, 因此, 允許別的進程從當前進程偷取 CPU 數據. 如果一個進程由于你的驅動的bug而在內核空間循環, schedule 調用使你能夠殺掉進程在跟蹤發生了什么之后.
你應當知道, 當然, 如何對 schedule 的調用可能創造一個附加的重入調用源到你的驅動, 因為它允許別的進程運行. 這個重入正常地不應當是問題, 假定你在你的驅動中已經使用了合適的加鎖. 然而, 要確認在你的驅動持有一個自旋鎖的任何時間不能調用 schedule.
如果你的驅動真正掛起了系統, 并且你不知道在哪里插入 schedule 調用, 最好的方式是加入一些打印消息并且寫到控制臺(如果需要, 改變 console_loglevel 值).
有時候系統可能看來被掛起, 但是沒有. 例如, 這可能發生在鍵盤以某個奇怪的方式保持鎖住的時候. 這些假掛起可通過查看你為此目的運行的程序的輸出來檢測. 一個你的顯示器上的時鐘或者系統負載表是一個好的狀態監控器; 只要他繼續更新, 調度器就在工作.
對許多的上鎖一個必不可少的工具是"魔術 sysrq 鍵", 在大部分體系上都可用. 魔鍵 sysrq 是 PC 鍵盤上 alt 和 sysrq 鍵組合來發出的, 或者在別的平臺上使用其他特殊鍵(詳見 documentation/sysrq.txt), 在串口控制臺上也可用. 一個第三鍵, 與這 2 個一起按下, 進行許多有用的動作中的一個:
r 關閉鍵盤原始模式; 用在一個崩潰的應用程序( 例如 X 服務器 )可能將你的鍵盤搞成一個奇怪的狀態.
k 調用"安全注意鍵"( SAK ) 功能. SAK 殺掉在當前控制臺的所有運行的進程, 給你一個干凈的終端.
s 進行一個全部磁盤的緊急同步.
u umount. 試圖重新加載所有磁盤在只讀模式. 這個操作, 常常在 s 之后馬上調用, 可以節省大量的文件系統檢查時間, 在系統處于嚴重麻煩時.
b boot. 立刻重啟系統. 確認先同步和重新加載磁盤.
p 打印處理器消息.
t 打印當前任務列表.
m 打印內存信息.
有別的魔術 sysrq 功能存在; 完整內容看內核源碼的文檔目錄中的 sysrq.txt. 注意魔術 sysrq 必須在內核配置中顯式使能, 大部分的發布沒有使能它, 因為明顯的安全理由. 對于用來開發驅動的系統, 然而, 使能魔術 sysrq 值得為它自己建立一個新內核的麻煩. 魔術 sysrq 可能在運行時關閉, 使用如下的一個命令:
echo 0 > /proc/sys/kernel/sysrq
如果非特權用戶能夠接觸你的系統鍵盤, 你應當考慮關閉它, 來阻止有意或無意的損壞. 一些以前的內核版本缺省關閉 sysrq, 因此你需要在運行時使能它, 通過向同樣的 /proc/sys 文件寫入 1.
sysrq 操作是非常有用, 因此它們已經對不能接觸到控制臺的系統管理員可用. 文件 /proc/sysrq-trigger 是一個只寫的入口點, 這里你可以觸發一個特殊的 sysrq 動作, 通過寫入關聯的命令字符; 接著你可收集內核日志的任何輸出數據. 這個 sysrq 的入口點是一直工作的, 即便 sysrq 在控制臺上被關閉.
如果你經歷一個"活掛", 就是你的驅動粘在一個循環中, 但是系統作為一個整體功能正常, 有幾個技術值得了解. 經常地, sysrq p 功能直接指向出錯的函數. 如果這個不行, 你還可以使用內核剖析功能. 建立一個打開剖析的內核, 并且用命令行中 profile=2 來啟動它. 使用 readprofile 工具復位剖析計數器, 接著使你的驅動進入它的循環. 一會兒后, 使用 readprofile 來看內核在哪里消耗它的時間. 另一個更高級的選擇是 oprofile, 你可以也考慮下. 文件 documentation/basic_profiling.txt 告訴你啟動剖析器所有需要知道的東西.
在追逐系統掛起時一個值得使用的防范措施是以只讀方式加載你的磁盤(或者卸載它們). 如果磁盤是只讀或者卸載的, 就沒有風險損壞文件系統或者使它處于不一致的狀態. 另外的可能性是使用一個通過 NFS, 網絡文件系統, 來加載它的全部文件系統的計算機, 內核的"NFS-Root"功能必須打開, 在啟動時必須傳遞特殊的參數. 在這個情況下, 即便不依靠 sysrq 你也會避免文件系統破壞, 因為文件系統的一致有 NFS 服務器來管理, 你的設備驅動不會關閉它.
[[15](#)] 實際上, 多處理器系統仍然在其他處理器上調度, 甚至一個單處理器的機器可能重新調度, 如果內核搶占被使能. 然而, 對于大部分的通常的情況( 單處理器不使能搶占), 系統一起停止調度.
- Linux設備驅動第三版
- 第 1 章 設備驅動簡介
- 1.1. 驅動程序的角色
- 1.2. 劃分內核
- 1.3. 設備和模塊的分類
- 1.4. 安全問題
- 1.5. 版本編號
- 1.6. 版權條款
- 1.7. 加入內核開發社團
- 1.8. 本書的內容
- 第 2 章 建立和運行模塊
- 2.1. 設置你的測試系統
- 2.2. Hello World 模塊
- 2.3. 內核模塊相比于應用程序
- 2.4. 編譯和加載
- 2.5. 內核符號表
- 2.6. 預備知識
- 2.7. 初始化和關停
- 2.8. 模塊參數
- 2.9. 在用戶空間做
- 2.10. 快速參考
- 第 3 章 字符驅動
- 3.1. scull 的設計
- 3.2. 主次編號
- 3.3. 一些重要數據結構
- 3.4. 字符設備注冊
- 3.5. open 和 release
- 3.6. scull 的內存使用
- 3.7. 讀和寫
- 3.8. 使用新設備
- 3.9. 快速參考
- 第 4 章 調試技術
- 4.1. 內核中的調試支持
- 4.2. 用打印調試
- 4.3. 用查詢來調試
- 4.4. 使用觀察來調試
- 4.5. 調試系統故障
- 4.6. 調試器和相關工具
- 第 5 章 并發和競爭情況
- 5.1. scull 中的缺陷
- 5.2. 并發和它的管理
- 5.3. 旗標和互斥體
- 5.4. Completions 機制
- 5.5. 自旋鎖
- 5.6. 鎖陷阱
- 5.7. 加鎖的各種選擇
- 5.8. 快速參考
- 第 6 章 高級字符驅動操作
- 6.1. ioctl 接口
- 6.2. 阻塞 I/O
- 6.3. poll 和 select
- 6.4. 異步通知
- 6.5. 移位一個設備
- 6.6. 在一個設備文件上的存取控制
- 6.7. 快速參考
- 第 7 章 時間, 延時, 和延后工作
- 7.1. 測量時間流失
- 7.2. 獲知當前時間
- 7.3. 延后執行
- 7.4. 內核定時器
- 7.5. Tasklets 機制
- 7.6. 工作隊列
- 7.7. 快速參考
- 第 8 章 分配內存
- 8.1. kmalloc 的真實故事
- 8.2. 后備緩存
- 8.3. get_free_page 和其友
- 8.4. 每-CPU 的變量
- 8.5. 獲得大量緩沖
- 8.6. 快速參考
- 第 9 章 與硬件通訊
- 9.1. I/O 端口和 I/O 內存
- 9.2. 使用 I/O 端口
- 9.3. 一個 I/O 端口例子
- 9.4. 使用 I/O 內存
- 9.5. 快速參考
- 第 10 章 中斷處理
- 10.1. 準備并口
- 10.2. 安裝一個中斷處理
- 10.3. 前和后半部
- 10.4. 中斷共享
- 10.5. 中斷驅動 I/O
- 10.6. 快速參考
- 第 11 章 內核中的數據類型
- 11.1. 標準 C 類型的使用
- 11.2. 安排一個明確大小給數據項
- 11.3. 接口特定的類型
- 11.4. 其他移植性問題
- 11.5. 鏈表
- 11.6. 快速參考
- 第 12 章 PCI 驅動
- 12.1. PCI 接口
- 12.2. 回顧: ISA
- 12.3. PC/104 和 PC/104+
- 12.4. 其他的 PC 總線
- 12.5. SBus
- 12.6. NuBus 總線
- 12.7. 外部總線
- 12.8. 快速參考
- 第 13 章 USB 驅動
- 13.1. USB 設備基礎知識
- 13.2. USB 和 sysfs
- 13.3. USB 的 Urbs
- 13.4. 編寫一個 USB 驅動
- 13.5. 無 urb 的 USB 傳送
- 13.6. 快速參考
- 第 14 章 Linux 設備模型
- 14.1. Kobjects, Ksets 和 Subsystems
- 14.2. 低級 sysfs 操作
- 14.3. 熱插拔事件產生
- 14.4. 總線, 設備, 和驅動
- 14.5. 類
- 14.6. 集成起來
- 14.7. 熱插拔
- 14.8. 處理固件
- 14.9. 快速參考
- 第 15 章 內存映射和 DMA
- 15.1. Linux 中的內存管理
- 15.2. mmap 設備操作
- 15.3. 進行直接 I/O
- 15.4. 直接內存存取
- 15.5. 快速參考
- 第 16 章 塊驅動
- 16.1. 注冊
- 16.2. 塊設備操作
- 16.3. 請求處理
- 16.4. 一些其他的細節
- 16.5. 快速參考
- 第 17 章 網絡驅動
- 17.1. snull 是如何設計的
- 17.2. 連接到內核
- 17.3. net_device 結構的詳情
- 17.4. 打開與關閉
- 17.5. 報文傳送
- 17.6. 報文接收
- 17.7. 中斷處理
- 17.8. 接收中斷緩解
- 17.9. 連接狀態的改變
- 17.10. Socket 緩存
- 17.11. MAC 地址解析
- 17.12. 定制 ioctl 命令
- 17.13. 統計信息
- 17.14. 多播
- 17.15. 幾個其他細節
- 17.16. 快速參考
- 第 18 章 TTY 驅動
- 18.1. 一個小 TTY 驅動
- 18.2. tty_driver 函數指針
- 18.3. TTY 線路設置
- 18.4. ioctls 函數
- 18.5. TTY 設備的 proc 和 sysfs 處理
- 18.6. tty_driver 結構的細節
- 18.7. tty_operaions 結構的細節
- 18.8. tty_struct 結構的細節
- 18.9. 快速參考