在Linux下寫一個驅動時候遇到的讀操作性能問題,讓我想一窺系統調用的處理流程,以查出問題的root cause。很多書把它和中斷處理放在一起講,但是又沒有哪本書說清楚了,看來只有代碼才能說明一切。以Linux系統下MIPS體系結構為例。
從開始說起。
1. 相關代碼
1 trap_init(void) /* 系統初始化:start_kernel中 */ 2 set_handler(0x180, &except_vec3_generic, 0x80);/* except_vec3_generic 根據cause寄存器跳轉到其若干類異常/中斷處理函數中*/ 3 set_except_vector(0, rollback ? rollback_handle_int : handle_int); 4 set_except_vector(1, handle_tlbm); 5 ... ... 6 set_except_vector(8, handle_sys);
當系統發現異常時,CPU將自動進入內核模式並禁止中斷,同時將PC指針指向默認的地址(根據異常的不同將分別進入偏移地址為0x180 or 0x200的地方,這些在函數trap_init都有體現,上述代碼沒有一一貼出)。
異常號為8時進入系統調用的處理入口。
1 /* 進入系統調用異常處理 */ 2 handle_sys 3 SAVE_SOME 4 STI # 進入內核模式並enable中斷 5 la t1, sys_call_table 6 ... ...
注意這里系統調用入口一開始就會enable中斷。
而異常號為0時進入中斷處理的入口。
1 /* 進入中斷異常處理 */ 2 handle_int 3 SAVE_ALL 4 CLI # 進入內核模式並disable中斷 5 plat_irq_dispatch # 每種類型的CPU都有自己的實現 6 do_IRQ 7 generic_handle_irq /*處理用戶注冊的ISR,關中斷進行 */ 8 irq_exit() 9 do_softirq /*進入下半部處理(軟中斷) */ 10 local_irq_enable /*仍然在中斷上下文中,但是開中斷*/ 11 ... ... /*處理用戶注冊的下半部, 12 Note:有可能在softirq內核線程中進行處理,所以下半部的處理並不總是在中斷上下文中,但是下半部的思想就是允許中斷嵌套*/
上面的代碼描述了中斷處理的框架,簡要列出了處理中值得注意的地方。這里尤其注意中斷處理入口一開始直接disable中斷。
2. 結論
所以,系統調用和普通中斷處理的異同在於:兩者都是從同一個異常處理入口開始,但是系統調用會一開始讓CPU進入內核模式且使能中斷,然后從系統調用表中取得相應的注冊函數調用之;而中斷處理則讓CPU進入內核模式且disable中斷,繼而調用do_IRQ函數。所以系統調用的真實處理(系統調用表中的注冊函數執行)中可以阻塞,而中斷處理的上半部不可以。所以在寫驅動代碼如字符設備驅動,實現讀操作時是可以讓其sleep的(比如沒有數據時候,用戶設置讀模式是阻塞型的)。另一方面,如果該驅動讀操作過於耗時也是不可取的,它在內核態中執行,這個時候只有中斷的優先級比它高,其它的高優先級線程將不能得到及時調度執行。
另外,思考一下為什么在中斷的下半部如tasklet中不能做阻塞的操作,而系統調用卻可以?
首先,系統調用過程中阻塞意味着阻塞時(內核模式中)當前上下文放在當前線程棧中,同時系統調用所在線程狀態改變(比如調用wait_event),下次改線程再次被調度后就可以從阻塞點繼續run,這整個過程和一個普通線程阻塞並再次調度的過程是一樣的,唯一的區別在於系統調用的阻塞點是在內核模式下。而下半部則不一樣,作為中斷處理的一個例程邏輯上不屬於某個線程,所以它阻塞時第一不能改變當前線程的狀態(而wait_semaphore之類的函數則會),其次它在阻塞的情況下再次run的點是在被中斷線程再次被調度后,這也是不合理的,因為它不是該線程的一部分,憑什么讓它的運行時間受限於該線程的調度時機?