710 likes | 876 Views
Driver. 核心模組 vs 應用程式. 應用程式啟動後從頭到尾都只執行同一件任務 . 模組被載入核心之後必須先向核心註冊它自己 . •init_module() 函式 ( 模組的入口點 ) 任務是將模組的功能準備好 以便事後可被 invocation( 調用 ). •cleanup_module() 在模組離開之前必須要被呼叫 . 模組只能與核心連結所以模組只能呼叫核心所提供的程式 ex: printfk(). 由於上一點 , 模組的原始碼不能引入一般的標頭檔 .
E N D
核心模組 vs 應用程式 • 應用程式啟動後從頭到尾都只執行同一件任務. • 模組被載入核心之後必須先向核心註冊它自己. •init_module()函式(模組的入口點)任務是將模組的功能準備好 以便事後可被invocation(調用). •cleanup_module()在模組離開之前必須要被呼叫. • 模組只能與核心連結所以模組只能呼叫核心所提供的程式 ex: printfk(). • 由於上一點,模組的原始碼不能引入一般的標頭檔. •有關核心相關事物放在 /usr/src/linux下的 include/linux 與 include/asm/ 目錄下的標頭檔.
使用者空間與核心空間 • 模組存活在kernel space(核心空間)而應用程式存在user space(使用者空間). • 作業系統必須負責讓程式得以獨立運作並保護系統資源避免非授權的存取. • 由CPU來保護系統軟體所以CPU本身提供了不同層級的作業模式(operating modality). • Unix系統提供兩個層級而現在CPU也至少有兩種層級,故Unix系統只使用最高與最低層級,Unix核心運作在最高層級(supervisor mode)應用程式運作在最低層級(user mode). • execution mode:包括kernel-space 與user-space分別有各自的memory mapping(記憶體對應關係)的關係與各自的address space(定址空間).
insmod • 載入 •insmod對模組的作用:將模組內的任何unresolved symbol(懸置 符號)連結到目前核心(函式庫)的符號表. •核心如何支援insmod? 依賴定義在kernel/module.c的system call函式: 1) sys_create_module():配置一塊可以容納模組的核心記憶空間. 2) sys_get_kernel_syms():傳回核心符號表,解決模組的懸置符號. 3) sys_init_module():將insmod改好的relocated object code移到 預先配置的核心空間. • 系統呼叫大略表: kernel/ 目錄執行 egrep “sys_.*\)$” *.c
版本依存性 • 模組與連結對象核心息息相關每當升級何新版本時模組就必須再新版本核心下重新編譯一次. • 編譯器會在ELF(executable Linking and Format).的.modifo(chap 11)區定義_module_kernel_version符號 • insmod會依照此符號與當時的核心作比較. • 定義在<linux/module.h>. • 可使用insmod –f來略過版本檢查. • 針對特定版本的核心來編譯模組必須引入該版本核心的標頭檔再Makefile定義一個KERNELDIR環境變數讓他指另一個不同的位置.
核心符號表 • 模組化驅動程式所需的[核心全域項目(函式與變數)]的位置都紀錄在符號表裡 可從/proc/ksyms取的此表. • 若模組能被順利載入核心,模組所釋放的符號也會成為核心符號的一部分. • 模組所釋出的符號可以被新模組使用新模組可以疊在其他模組之上. ex: msdos檔案系統得仰賴fat模組所釋出的符號
模組的生與死 • init_module()會註冊模組所提供的任何facility(功能性). • 模組可以註冊許多不同類型的facility,對於每一個facility都有一個特定的核心函式來完成其註冊程序. • 傳給核心註冊函式的引數:facility註冊名稱,指標(指向此facility的資料結構). • facility種類:序列阜,雜項裝置,/proc檔案,作業領域(executable domain),管制線路(line discipline).
Init_module的錯誤處置 • 註冊失敗:系統沒有足夠空間或某資源已被其他驅動程式佔用… • 模組自己要負責回復(undo)到註冊失敗之前的狀態,如果init_module()在中途失敗,模組必須自己主動註銷(unregister)那些已經註冊成功的facility. • 不能藉由重新載入模組來重新註冊facility,也不太可能註銷他們,因為需要當初註冊所用的相同指標. • 使用goto解決.
卸載模組 • 使用rmmod可卸載(unload)沒用的模組. • 原理: rmmod觸發delete_module() system call,如果模組的用量為零,則delete_module()會呼叫模組本身的cleanup_module();否則回傳作錯誤代碼. • cleanup_module()必須負責註銷該模組的每一項facility.
實際的初始化與清理函式 • 命名模組函式,須引入<linux/init.h> •module_init(my_init); •module_exit(my_cleanup); • my_init取代init_module(),my_cleanup取代cleanup_module(); • 所以可使每個初始函式與清理函式都有自己專屬的名稱,以方便除錯.
資源的運用 • 大部分驅動程式的工作大概就在讀寫I/O port或I/O memory(統稱 I/O region) • 核心都應該保證驅動程式能獨占存取其I/O port以免受到其他驅動程式的干擾 • /proc/ioports與/proc/iomem檔案可取得以註冊的系統資源
User-space驅動程式(1/2) • 優點 •可使用完整的c函式庫. •可使用傳統的debugger. •若user-space driver當掉,直接kill就可以. •user-space記憶體可被“置換(swappable)”,而kernel-space不可能,因此較大的程式不會排擠其他程式所使用的RAM,除非此裝置正在使用. •user-space driver也能容許目標裝置同時被多個行程所存取.
User-space驅動程式(2/2) • 缺點: •在user-space內無法使用”中斷”. •不能直接存取I/O memory. •必須先呼叫ioperm()或iopl()才能存取I/O port. •回應時間較緩慢,因為應用程式要傳輸資料給硬體而硬體也要回應應用程式(context switch) •若驅動程式已被“置換(swap)”到磁碟上,那回應時間就更長了 •大部分重要裝置不能在user-space上予以有效控制,ex:網路介面…
I/O memory(1/3) • int check_region(unsigned long start,unsigned long len); •用來檢查某段範圍的I/O位址是否被佔用. • struct resource *request_region(unsigned long start,unsigned long len , char *nam); •要求註冊該位址區.若核心同意,此函式會回傳一個non-NULL指標.
I/O memory(2/3) • void release_region(unsigned long start.unsigned long len); •將取得的I/O 位址歸還給系統.
I/O memory(3/3) • 取得,釋放特定一段的I/O memory region •int check_mem_region(usigned long start,unsigned long len); •int request_mem_region(unsigned long start,unsigned long len,char *name); •int release_mem_region(unsigned long start,unsigned long len);
決定組態參數(1/4) • 驅動程式所需的某些參數會因系統而異,ex:I/O 位址,記憶區範圍…有時候必須傳遞參數給驅動程式,才能幫助他找到目標裝置或啟動,關閉特定功能. • 對於某些裝置,如裝置品牌,型號…等也有可能影響驅動程式的行為. • 將正確的參數傳遞給驅動程式(configuring),是驅動程式在初始化期間必須完成的工作. • 參數值可在載入期間由insmod或modprobe傳給模組
決定組態參數(2/4) • modprobe可從一各組態檔(/etc/modules.conf)讀入參數值. • 使用者可在命令列指定參數值ex: #insmod skull skull_ival=666 skull_sval=“beast” • 在insmod能夠改變模組參數之前,模組本身必須以MODULE_PARM()巨集(定義在linux/module.h)宣告可被修改的參數. • MODULE_PARM()需要兩個引數,一是變數名稱,二是變數型別.
決定組態參數(3/4) int skull_ival=0; char *skull_sval; MODULE_PARM(skull_ival, “I”); MODULE_PARM(skull_sval, “s”); • 參數型別有五種:b(1-byte),h(2-byte短整數),i(整數),l(長整數),s(字串).
決定組態參數(4/4) • MODULE_PARM_DESC()巨集,可讓程式設計師用來描述模組參數的意義. static int skull_port_bse=0x300; MODULE_PARM(skull_port_base,”i”); MODULE_DESC(skull_port_base,”skull I/O base(default 0x300)); • 可使用objdump,modinfo指令來檢視參數指令.
訊息除錯法(1/4) • printk()函式 • 通用的除錯技巧,對應用程式而言使用printf();對於核心程式而言則使用printk(). • printk()能讓你指定訊息的loglevel(等級),共分為八纇.定義在<linux/kernel.h>裡. •KERN_EMERG:緊急訊息,出現在系統崩潰前. •KERN_ALERT:危險通知,發生在需要立即採 取行動的事件. •KERN_CRIT:嚴重狀況,涉及硬體或軟體故障
訊息除錯法(2/4) •KERN_ERR:錯誤狀況回報,通常回報硬體上的困難. •KERN_WARNING:緊急訊息,程度上不影響系統. •NERN_NOTICE:通知,意料中會發生且值得注意的 狀況. •KERN_INFO:資訊性訊息,driver在啟動階段所印出 的硬體資訊. •KERN_DEBUG:供除錯用途的訊息.
訊息除錯法(3/4) • 上述代號展開後分別成為:<0>,<1>,<2>…<7>之類的字串,括弧內數字越低表示等級越高. •ex:printk(KERN_DEBUG “HI”); • 不同等級的訊息會被輸出到不同的地點,有可能是目前的操控台(console)或者是某種文字終端機(Xterm視窗,Telnet或SSH連線). • 任何等值低於console_loglevel的變數訊息都會被顯示在操控台上(console). • 不過若系統上同時執行klogd與syslogd不管console_loglevel的值為何,所有核心訊息都會被加入/var/log/messages.
訊息除錯法(4/4) • 如果沒跑klogd,則訊息不會流入user-space,除非主動讀取/proc/kmsg. • 修改console_loglevel的值 •使用sys_syslog() 利用klogd的 –c選項. •本書範例:misc-progs/setlevel.c, • 2.1.31版本開始,可以直接透/proc/sys/kernel/ printk檔案來修改或檢視console_levle的值. • # echo 8 > /proc/sys/kernel/printk
核心訊息的輸出流程(1/2) • printk()會將訊息寫入一個環型queue,長度為LOG_BUF_LEN(定義在kernel/printk.c),且喚醒 •正在等待訊息的行程(使用syslog()). •正在讀取/proc/kmesg的行程. • 在環型queue被填滿時,printk()將會繞回原點,將新訊息覆蓋在最就訊息上,其優點: •固定記憶體,既使沒跑日誌紀錄引擎 (klogd,syslogd)也不至於消耗所有的記憶體. •核心內部到處都可以直接呼叫printk().
核心訊息的輸出流程(2/2) • klogd所取得的核心訊息會被轉交給syslogd,由它依據/etc/syslog.conf來決定如何處裡收到的訊息. • 若系統沒跑klogd,核心訊息將會一值留在環型佇列,直到有人讀取它或者是被新訊息蓋掉. • 若不希望driver所發出得訊息擾亂的系統的日誌檔,可以用-f選項來從新啟動klogd並將其訊息寫入特定檔案 . kernel /proc/kmsg [klogd] /dev/log [syslogd] syslog.conf
查詢除錯法(1/6)(使用/proc檔案系統) • /proc是靠軟體模擬出來的特殊檔案系統,並不實際存在於硬碟上,而是核心提供給user-space的資訊窗口. • 在proc/下的每一個檔案,接聯繫到核心內的專屬函式,這些函式在使用者讀取檔案時,及時產生檔案的內容. •ex: 以/proc/modules為例,當你讀取它時它會顯示目前載入哪些模組,但此檔案系統的長度都為0.
查詢除錯法(2/6)(使用/proc檔案系統) • Linux許多系統工具,如ps,top,uptime等都是從 /proc取得它們所需要的資訊. • 建立/proc檔案:driver必須製作一備查程式,讓它在檔案被存取時,及時供應資料,核心也會配置一記憶頁給它 • 備查程式介面: int (*read_proc)(char*page,char**start,off_toffset,intcount,int*eof ,void*data); read() /proc 備查函式 記憶頁 User-space
查詢除錯法(3/6)(使用/proc檔案系統) • page指標:指向核心預先配置的記憶頁 • *start與offset:若檔案超過一記憶頁大小,可利用分批傳輸的方式,先將*start指向page再利用offset指向下一各位元組. • skcull程式中有對read_proc的實作:
查詢除錯法(4/6)(使用/proc檔案系統) • 定義好read_proc作業方式後,必須為它在/proc下設置一個入口點,使用create_proc_read_entry(). • 若希望使用者能透過/proc/scullmem取得該函式提供的資料,則driver需宣告如下: /proc入口點名稱 static void scull_create_proc() { create_proc_read_entry(“scullmem”, 0 /* 預設模式(0x444) */, NULL /* 上層目錄(*proc_dir_entry)*/, scull_read_procmem, NULL /* 提供給read_proc使用的資料 */); } 檔案權限 入口點上層目錄 read_proc作業方法的指標 傳給read_proc的資料的指標
查詢除錯法(5/6)(使用/proc檔案系統) • 第三參數的說明: •在/proc檔案系統下的每一個子目錄,都有各自 專屬的proc_dir_entry結構來描述. •ex:描述/proc/driver子目錄的結構為 proc_root_driver,描述/proc/bus子目錄的結 構為proc_bus. •若此引數設定為NULL代表入口點設置在/proc 目錄下.
查詢除錯法(6/6)(使用/proc檔案系統) • 在模組被載卸之前必須先移除相關的/proc入口點,使用remove_proc_entry() //移除 /proc/scullmem; remove_pcor_entry(“scullmem”,NULL); //移除 /proc/driver/scullmem; remove_proc_entry(“scullmem”,proc_root_driver); //移除 /proc/scull/scullmem; remove_proc_entry(“scullmem”,scull_procdir);
觀測除錯法 • strace,檢驗位於kernel-space的程式碼. • 能顯示出由user-space程式所發出的所有system call. • #strace ls /dev > /dev/scull0
排除重大系統錯誤(1/2) • 除了監視,除錯技術,驅動程式可能還是意料之外的bug存在,嚴重可能造成system fault. • fault(失誤)不等於panic(死當),失誤通常會摧毀目前的行程,系統本身能正常. • 若fault發生在process context之外,或是破壞系統的關鍵部分,即有可能早成panic. • oops訊息:往往發生在不當的操作指標所引起,如dereference(提領)或者是誤用指標的值.
排除重大系統錯誤(2/2) • page fault: 在protect mode(保護模式)下,所使用的是virtual address(虛擬記憶體),藉由page table換算出physical address(實體位置),若程式提領一個無效指標,分頁機制則沒有辦法算出實體位置,因而發生page fault. • 若在user-space發生提領無效,後果頂多是無法”page in “該位址. • 若發生在kernel則迫使核心發出oops訊息.
Ksymoops用法 • ksymoops用法: • systme.map檔(-v) /usr/src/linux/system.map • 模組清單(-l): /proc/modules • 核心符號表(-k): /proc/ksyms • 映像檔(-v): • 模組object檔存放位置(-o):
gdb使用 • gdb使用: • 指令:gdb /usr/src/linux/vmlinux /proc/kcore • (gdb) x/i <addr> • Ex: (gdb) x/20i 0xc8002060 • 參考網址:http://es-sun2.fernuni-hagen.de/cgi-bin/info2html?(gdb)Top
其他核心除錯器 • kdb • kgdb
核心的計時間隔 • 中斷—CPU暫停目前工作,然後執行ISR來處理中斷.(CH9) • 計時器中斷,固定間隔觸發的中斷事件,核心依據HZ(定義在<linux/param.h>)的值來設定間隔長度,硬體平台不同,值也不同. • 每次發生計時器中斷,jiffies變數的值就會被遞增一次,宣告在<linux/sched.h>,型別為unsigned long volatile,核心確保溢位之後還能正確運作,不必擔心,只須注意.
處理器特有的暫存器 • 量測指令本身執行時間 unsigned long ini, end; rdtscl(ini); rdtscl(end); printk(“time lapse: %li\n”,end-ini); • 與平台無關的函式用來替代rdstc() #include <linux/timex.h> cycles_t get_cycles(void); //無時脈計數,則回傳永0,32bit • 如何內插組語指令(iniline assembly) for MIPS #define rdtscl (dest) \ __asm__ __volatile__(“mfc0 %0,$9; nop” : “=r” (dest)) • 內插組語的語法相當有威力,但是有點複雜,特別是在那些會限定暫存器用途的平台上(x86系列).完整語法請參考gcc的說明文件.
取得目前時間(1/2) • jiffies從開機到至今的時間,與驅動程式生命期無關,也不可能跨越開關機時間. • 驅動程式可利用jiffies的現值來估算兩事件之間的間隔時間,如mouse • 驅動程式不需要知道牆鐘時刻(wall-clock time),若真的需要靠自己處理當時的時刻,do_gettimeofday()或許可派上用場. • 此函式並非直接告知今天是星期幾,而是將一般的秒與微秒填入一個struct timeval,原型如下 #include <linux/time.h> void do_gettimeofday(struct timeval *tv);
取得目前時間(2/2) • 從xtime變數同樣也可取得目前時刻,但這是不被鼓勵的行為,因為無法連動取得timevalue,結構內的tv_sec與tv_usec欄位值,除非暫停掉中斷. • 不太講求精準度,2.2版核心提供一個快又安全的函式來取得目前時刻: void get_fast_time(struct timeval *tv); • 範例 jit(Just In Time)模組,他不會產生裝置節點,而是直接將它取得的時刻資訊透過/proc/currentime傳到user-space. cat /proc/currentime /proc/currentime /proc/currentime
延遲執行 • 延遲—通常是為了讓硬體有足夠充裕的時間完成某些工作 • 需要考慮的重點之一,是延遲時間是否超過一個時脈單位 • 較長的延遲,可以利用系統時鐘來計時,較短的延遲,則通常以軟體迴圈來應付
長期延遲(1/4) • 最簡單也是最蠢的做法,稱為忙著等待(busy waiting): unsigned long j = jiffies + jit_delay * HZ; while (jiffies < j) /* 發呆… */ ; • 因為jiffies是volatile變數,使得C編譯器會落實每次的讀取動作(不使用快取技術). • 在延遲期間,處理器是被鎖死的,因為這是核心裡的回圈,排程器不會岔斷進入核心行程. • 若中斷失效時進入迴圈,jiffies不會更新,迴圈無法終止,只能使用reset按鈕.
長期延遲(2/4) • busy-wait範例,讀取/proc/jitbusy,每當它的read作業方法被呼叫一次,其內部的忙碌迴圈就會延遲一秒.如果使用dd if=/proc/jitbusy bs=1命令,就可以看到每秒讀出一個字元的效果. • 這種做法會嚴重拖累系統效能,因為其他行程每隔一秒才有機會執行一次,比較合理的做法是: while (jiffies < j) schedule(); • 但還不夠理想,倘若它是整個系統上唯一的可跑行程,它真的會動作(呼叫schedule(),然後立刻被排程器選中,然後再呼叫schedule() … 所以說,機器負載程度將至少等於1,而idle行程將沒機會上線. (省電 降溫 生命年) • 若在一個非常忙碌的系統上,呼叫排程器的做法,反而有可能造成驅動程式等待了超過原本預期的時間. time cat /proc/jitsched
長期延遲(3/4) • 排程迴圈提供一個觀測驅動程式工作程序的速成工具.(printk()之後一點點延遲,讓klogd有機會盡忠職守,以免不知如何死當) • 最佳的延遲方式,是要求核心代勞,核心提供兩種執行短程延遲的機制,看你的驅動程式是否要等待其他事件而定. sleep_on_timeout (wait_queue_head_t *q, unsigned long timeout); interruptible_sleep_on_timeout (wait_queue_head_t *q, unsigned long timeout); • 兩種版本都會讓行程待在指定的待命佇列裡休眠,但一定會在指定期限內返回.timeout值是要等待的jiffies個數,而非jiffies絕對值.
長期延遲(4/4) • 範例 /proc/jitqueue wait_queue_head_t wait; init_waitqueue_head (&wait); interruptible_sleep_on_timeout (&wait, jit_delay*HZ); • 但沒有人會對這個待命佇列呼叫wake_up(),所以必定是因為超過timeout期限而甦醒,所以這種延遲計完美又合法. • 若不須等待其他事件,還有更直接了當的方法: set_current_state (TASK_INTERRUPTIBLE); schedule_timeout (jit_delay*HZ); • 但實際延遲時間,有可能略為超過你原本預期的時間. time cat /proc/jitqueue time cat /proc/jitself
短期延遲 • 在計算非常短暫的延遲,jiffies無法達成,所以核心提供udelay()和mdelay()函式,原型如下: #include <linux/delay.h> void udelay (unsigned long usecs); //inline void mdelay (unsigned long msecs); • udelay()以當地系統的BogoMips(開機所計算出的系統常數)值來決定迴圈的圈數,其值大約是CPU時脈數的兩倍左右. • mdelay()是含有udelay()的迴圈所構成 • 兩者都是busy-waiting函式,因此除非沒有其它辦法,否則應該儘量避免使用mdelay()
核心內建的工作佇列 • 要延遲特定工作的開始執行時間,最簡便的辦法是利用核心所維護的佇列.其中有三個可供驅動程式運用(宣告在<linux/tqueue.h>),分別是: 排程器佇列(scheduler queue) 在行程環境內運作,所以限制較寬鬆.2.4是以專用的 kernel thread來執行的,稱為keventd,並且必須透過 schedule_task()來存取. 計時器佇列(tq_timer) 由計時器時脈訊號觸發的佇列.必須遵守中斷模式規則 即期佇列(tq_immediate) 可能再系統呼叫返回之前或是排程器介入時,看何者先 到.會在中斷期被消化掉.