71
Driver

Driver

  • Upload
    vadin

  • View
    37

  • Download
    0

Embed Size (px)

DESCRIPTION

Driver. 核心模組 vs 應用程式. 應用程式啟動後從頭到尾都只執行同一件任務 . 模組被載入核心之後必須先向核心註冊它自己 . •init_module() 函式 ( 模組的入口點 ) 任務是將模組的功能準備好 以便事後可被 invocation( 調用 ). •cleanup_module() 在模組離開之前必須要被呼叫 . 模組只能與核心連結所以模組只能呼叫核心所提供的程式 ex: printfk(). 由於上一點 , 模組的原始碼不能引入一般的標頭檔 . - PowerPoint PPT Presentation

Citation preview

Page 1: Driver

Driver

Page 2: Driver

核心模組 vs 應用程式

應用程式啟動後從頭到尾都只執行同一件任務 . 模組被載入核心之後必須先向核心註冊它自己 .

•init_module() 函式 ( 模組的入口點 ) 任務是將模組的功能準備好 以便事後可被 invocation( 調用 ).

•cleanup_module() 在模組離開之前必須要被呼叫 . 模組只能與核心連結所以模組只能呼叫核心所提供的程式 ex: prin

tfk(). 由於上一點 , 模組的原始碼不能引入一般的標頭檔 .

•有關核心相關事物放在 /usr/src/linux 下的 include/linux 與

include/asm/ 目錄下的標頭檔 .

Page 3: Driver
Page 4: Driver

使用者空間與核心空間

模組存活在 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( 定址空間 ).

Page 5: Driver

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

Page 6: Driver

版本依存性

模組與連結對象核心息息相關每當升級何新版本時模組就必須再新版本核心下重新編譯一次 .

編譯器會在 ELF(executable Linking and Format). 的 .modifo(chap 11) 區定義 _module_kernel_version 符號

insmod 會依照此符號與當時的核心作比較 . 定義在 <linux/module.h>. 可使用 insmod –f 來略過版本檢查 . 針對特定版本的核心來編譯模組必須引入該版本核心的標頭檔

再 Makefile 定義一個 KERNELDIR 環境變數讓他指另一個不同的位置 .

Page 7: Driver

核心符號表

模組化驅動程式所需的 [ 核心全域項目 ( 函式與變數 )] 的位置都紀錄在符號表裡 可從 /proc/ksyms 取的此表 .

若模組能被順利載入核心 , 模組所釋放的符號也會成為核心符號的一部分 .

模組所釋出的符號可以被新模組使用新模組可以疊在其他模組之上 .

ex: msdos 檔案系統得仰賴 fat 模組所釋出的符號

Page 8: Driver

模組的生與死

init_module() 會註冊模組所提供的任何 facility( 功能性 ). 模組可以註冊許多不同類型的 facility, 對於每一個 facility

都有一個特定的核心函式來完成其註冊程序 . 傳給核心註冊函式的引數 :facility 註冊名稱 , 指標 ( 指向此

facility 的資料結構 ). facility 種類 :序列阜 ,雜項裝置 ,/proc 檔案 , 作業領域 (exe

cutable domain),管制線路 (line discipline).

Page 9: Driver

Init_module 的錯誤處置

註冊失敗 : 系統沒有足夠空間或某資源已被其他驅動程式佔用…

模組自己要負責回復 (undo) 到註冊失敗之前的狀態 , 如果init_module() 在中途失敗 , 模組必須自己主動註銷 (unregister)那些已經註冊成功的 facility.

不能藉由重新載入模組來重新註冊 facility, 也不太可能註銷他們 ,因為需要當初註冊所用的相同指標 .

使用 goto 解決 .

Page 10: Driver

卸載模組

使用 rmmod 可卸載 (unload)沒用的模組 . 原理 : rmmod觸發 delete_module() system call, 如果模組的

用量為零 ,則 delete_module() 會呼叫模組本身的 cleanup_module();否則回傳作錯誤代碼 .

cleanup_module() 必須負責註銷該模組的每一項 facility.

Page 11: Driver

實際的初始化與清理函式

命名模組函式 , 須引入 <linux/init.h>

•module_init(my_init);

•module_exit(my_cleanup); my_init 取代 init_module(),my_cleanup 取代 cleanup_modul

e(); 所以可使每個初始函式與清理函式都有自己專屬的名稱 ,

以方便除錯 .

Page 12: Driver

資源的運用

大部分驅動程式的工作大概就在讀寫 I/O port或 I/O memory( 統稱 I/O region)

核心都應該保證驅動程式能獨占存取其 I/O port 以免受到其他驅動程式的干擾

/proc/ioports 與 /proc/iomem 檔案可取得以註冊的系統資源

Page 13: Driver

User-space 驅動程式 (1/2)

優點 •可使用完整的 c 函式庫 .

•可使用傳統的 debugger.

•若 user-space driver 當掉 ,直接 kill 就可以 .

•user-space 記憶體可被“置換 (swappable)”, 而 kernel-space不可能 ,因此較大的程式不會排擠其他程式所使用的 RAM,除非此裝置正在使用 .

•user-space driver 也能容許目標裝置同時被多個行程所存取 .

Page 14: Driver

User-space 驅動程式 (2/2)

缺點 : •在 user-space 內無法使用”中斷” . •不能直接存取 I/O memory. •必須先呼叫 ioperm()或 iopl()才能存取 I/O port. •回應時間較緩慢 ,因為應用程式要傳輸資料給硬體而硬

體也要回應應用程式 (context switch) •若驅動程式已被“置換 (swap)” 到磁碟上 ,那回應時間

就更長了 •大部分重要裝置不能在 user-space 上予以有效控制 ,ex:網路介面…

Page 15: Driver

I/O memory(1/3)

int check_region(unsigned long start,unsigned long len);

•用來檢查某段範圍的 I/O 位址是否被佔用 . struct resource *request_region(unsigned long start,unsigned l

ong len , char *nam);

•要求註冊該位址區 . 若核心同意 , 此函式會回傳一個non-NULL 指標 .

Page 16: Driver

I/O memory(2/3)

void release_region(unsigned long start.unsigned long len);

•將取得的 I/O 位址歸還給系統 .

Page 17: Driver

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);

Page 18: Driver

決定組態參數 (1/4)

驅動程式所需的某些參數會因系統而異 ,ex:I/O 位址 ,記憶區範圍…有時候必須傳遞參數給驅動程式 ,才能幫助他找到目標裝置或啟動 , 關閉特定功能 .

對於某些裝置 , 如裝置品牌 ,型號…等也有可能影響驅動程式的行為 .

將正確的參數傳遞給驅動程式 (configuring), 是驅動程式在初始化期間必須完成的工作 .

參數值可在載入期間由 insmod或 modprobe 傳給模組

Page 19: Driver

決定組態參數 (2/4)

modprobe 可從一各組態檔 (/etc/modules.conf)讀入參數值 .

使用者可在命令列指定參數值 ex:

#insmod skull skull_ival=666 skull_sval=“beast” 在 insmod 能夠改變模組參數之前 , 模組本身必須以 M

ODULE_PARM()巨集 ( 定義在 linux/module.h)宣告可被修改的參數 .

MODULE_PARM() 需要兩個引數 , 一是變數名稱 ,二是變數型別 .

Page 20: Driver

決定組態參數 (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(字串 ).

Page 21: Driver

決定組態參數 (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 指令來檢視參數指令 .

Page 22: Driver

訊息除錯法 (1/4)

printk() 函式 通用的除錯技巧 , 對應用程式而言使用 printf();對

於核心程式而言則使用 printk(). printk() 能讓你指定訊息的 loglevel(等級 ),共分為八纇 . 定義在 <linux/kernel.h>裡 .

•KERN_EMERG:緊急訊息 ,出現在系統崩潰前 . •KERN_ALERT:危險通知 ,發生在需要立即採 取行動的事件 . •KERN_CRIT:嚴重狀況 ,涉及硬體或軟體故障

Page 23: Driver

訊息除錯法 (2/4)

•KERN_ERR:錯誤狀況回報 ,通常回報硬體上的困難 .•KERN_WARNING:緊急訊息 ,程度上不影響系統 .•NERN_NOTICE:通知 ,意料中會發生且值得注意的 狀況 .•KERN_INFO: 資訊性訊息 ,driver 在啟動階段所印出 的硬體資訊 .•KERN_DEBUG: 供除錯用途的訊息 .

Page 24: Driver

訊息除錯法 (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.

Page 25: Driver

訊息除錯法 (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

Page 26: Driver

核心訊息的輸出流程 (1/2)

printk() 會將訊息寫入一個環型 queue,長度為 LOG_BUF_LEN( 定義在 kernel/printk.c),且喚醒

•正在等待訊息的行程 (使用 syslog()). •正在讀取 /proc/kmesg 的行程 . 在環型 queue 被填滿時 ,printk() 將會繞回原點 ,將

新訊息覆蓋在最就訊息上 ,其優點 : •固定記憶體 ,既使沒跑日誌紀錄引擎 (klogd,syslogd) 也不至於消耗所有的記憶體 . •核心內部到處都可以直接呼叫 printk().

Page 27: Driver

核心訊息的輸出流程 (2/2)

klogd 所取得的核心訊息會被轉交給 syslogd, 由它依據 /etc/syslog.conf來決定如何處裡收到的訊息 .

若系統沒跑 klogd, 核心訊息將會一值留在環型佇列 ,直到有人讀取它或者是被新訊息蓋掉 .

若不希望 driver 所發出得訊息擾亂的系統的日誌檔 ,可以用 -f 選項來從新啟動 klogd 並將其訊息寫入特定檔案 .

kernel /proc/kmsg [klogd] /dev/log [syslogd] syslog.conf

Page 28: Driver

查詢除錯法 (1/6)( 使用 /proc 檔案系統 )

/proc 是靠軟體模擬出來的特殊檔案系統 , 並不實際存在於硬碟上 , 而是核心提供給 user-space 的資訊窗口 .

在 proc/ 下的每一個檔案 ,接聯繫到核心內的專屬函式 ,這些函式在使用者讀取檔案時 ,及時產生檔案的內容 .

•ex: 以 /proc/modules為例 , 當你讀取它時它會顯示目前載入哪些模組 ,但此檔案系統的長度都為 0.

Page 29: Driver

查詢除錯法 (2/6)( 使用 /proc 檔案系統 )

Linux許多系統工具 , 如 ps,top,uptime等都是從 /proc 取得它們所需要的資訊 . 建立 /proc 檔案 :driver 必須製作一備查程式 , 讓它在

檔案被存取時 ,及時供應資料 , 核心也會配置一記憶頁給它

備查程式介面 :

int (*read_proc)(char *page,char **start,off_t offset,int count,int *eof ,void *data);

read() /proc 備查函式 記憶頁 User-space

Page 30: Driver

查詢除錯法 (3/6)( 使用 /proc 檔案系統 )

page 指標 :指向核心預先配置的記憶頁 *start 與 offset: 若檔案超過一記憶頁大小 , 可利用

分批傳輸的方式 ,先將 *start 指向 page 再利用 offset 指向下一各位元組 .

skcull 程式中有對 read_proc 的實作 :

Page 31: Driver

查詢除錯法 (4/6)( 使用 /proc 檔案系統 )

定義好 read_proc 作業方式後 ,必須為它在 /proc 下設置一個入口點 ,使用 create_proc_read_entry().

若希望使用者能透過 /proc/scullmem 取得該函式提供的資料 ,則 driver 需宣告如下 :

static void scull_create_proc(){ create_proc_read_entry(“scullmem”, 0 /* 預設模式 (0x444) */, NULL /* 上層目錄 (*proc_dir_entry)*/, scull_read_procmem, NULL /* 提供給 read_proc 使用的資料 */);}

/proc 入口點名稱檔案權限

入口點上層目錄

read_proc 作業方法的指標

傳給 read_proc 的資料的指標

Page 32: Driver

查詢除錯法 (5/6)( 使用 /proc 檔案系統 )

第三參數的說明 : •在 /proc 檔案系統下的每一個子目錄 ,都有各自 專屬的 proc_dir_entry 結構來描述 . •ex:描述 /proc/driver子目錄的結構為 proc_root_driver,描述 /proc/bus子目錄的結 構為 proc_bus. •若此引數設定為 NULL 代表入口點設置在 /proc 目錄下 .

Page 33: Driver

查詢除錯法 (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);

Page 34: Driver

觀測除錯法

strace, 檢驗位於 kernel-space 的程式碼 . 能顯示出由 user-space 程式所發出的所有 system call.

#strace ls /dev > /dev/scull0

Page 35: Driver

排除重大系統錯誤 (1/2)

除了監視 ,除錯技術 , 驅動程式可能還是意料之外的bug 存在 ,嚴重可能造成 system fault.

fault(失誤 ) 不等於 panic(死當 ),失誤通常會摧毀目前的行程 , 系統本身能正常 .

若 fault發生在 process context 之外 ,或是破壞系統的關鍵部分 ,即有可能早成 panic.

oops訊息 :往往發生在不當的操作指標所引起 , 如 dereference( 提領 )或者是誤用指標的值 .

Page 36: Driver

排除重大系統錯誤 (2/2)

page fault: 在 protect mode( 保護模式 )下 ,所使用的是 virtual address(虛擬記憶體 ),藉由 page table換算出 physical address(實體位置 ), 若程式提領一個無效指標 ,分頁機制則沒有辦法算出實體位置 ,因而發生 page fault.

若在 user-space發生提領無效 , 後果頂多是無法” page in “該位址 .

若發生在 kernel則迫使核心發出 oops訊息 .

Page 37: Driver

Ksymoops 用法

ksymoops 用法 : systme.map 檔 (-v) /usr/src/linux/system.map

模組清單 (-l): /proc/modules 核心符號表 (-k): /proc/ksyms映像檔 (-v): 模組 object 檔存放位置 (-o):

Page 38: Driver

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

Page 39: Driver

其他核心除錯器

kdb kgdb

Page 40: Driver

核心的計時間隔

中斷— CPU暫停目前工作 ,然後執行 ISR來處理中斷 .(CH9)

計時器中斷 ,固定間隔觸發的中斷事件 ,核心依據 HZ( 定義在 <linux/param.h>) 的值來設定間隔長度 ,硬體平台不同 ,值也不同 .

每次發生計時器中斷 ,jiffies 變數的值就會被遞增一次 ,宣告在 <linux/sched.h>,型別為 unsigned long volatile, 核心確保溢位之後還能正確運作 ,不必擔心 ,只須注意 .

Page 41: Driver

處理器特有的暫存器 量測指令本身執行時間 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 的說明文件 .

Page 42: Driver

取得目前時間 (1/2)

jiffies 從開機到至今的時間 ,與驅動程式生命期無關 ,也不可能跨越開關機時間 .

驅動程式可利用 jiffies 的現值來估算兩事件之間的間隔時間 ,如 mouse

驅動程式不需要知道牆鐘時刻 (wall-clock time),若真的需要靠自己處理當時的時刻 ,do_gettimeofday()或許可派上用場 .

此函式並非直接告知今天是星期幾 , 而是將一般的秒與微秒填入一個 struct timeval, 原型如下

#include <linux/time.h> void do_gettimeofday(struct timeval *tv);

Page 43: Driver

取得目前時間 (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

Page 44: Driver

延遲執行

延遲—通常是為了讓硬體有足夠充裕的時間完成某些工作

需要考慮的重點之一 ,是延遲時間是否超過一個時脈單位

較長的延遲 , 可以利用系統時鐘來計時 ,較短的延遲 ,則通常以軟體迴圈來應付

Page 45: Driver

長期延遲 (1/4)

最簡單也是最蠢的做法 ,稱為忙著等待 (busy waiting):

unsigned long j = jiffies + jit_delay * HZ; while (jiffies < j) /* 發呆… */ ; 因為 jiffies 是 volatile 變數 ,使得 C編譯器會落實每次的讀取動作 (不使用快取技術 ).

在延遲期間 ,處理器是被鎖死的 ,因為這是核心裡的回圈 ,排程器不會岔斷進入核心行程 .

若中斷失效時進入迴圈 ,jiffies 不會更新 ,迴圈無法終止 , 只能使用 reset按鈕 .

Page 46: Driver

長期延遲 (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

Page 47: Driver

長期延遲 (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絕對值 .

Page 48: Driver

長期延遲 (4/4)

範例 /proc/jitqueue wait_queue_head_t wait; init_waitqueue_head (&wait); interruptible_sleep_on_timeout (&wait, jit_dela

y*HZ);

但沒有人會對這個待命佇列呼叫 wake_up(), 所以必定是因為超過 timeout期限而甦醒 , 所以這種延遲計完美又合法 .

若不須等待其他事件 ,還有更直接了當的方法 : set_current_state (TASK_INTERRUPTIBLE); schedule_timeout (jit_delay*HZ); 但實際延遲時間 ,有可能略為超過你原本預期的時間 .

time cat /proc/jitqueue

time cat /proc/jitself

Page 49: Driver

短期延遲

在計算非常短暫的延遲 ,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()

Page 50: Driver

核心內建的工作佇列

要延遲特定工作的開始執行時間 ,最簡便的辦法是利用核心所維護的佇列 .其中有三個可供驅動程式運用 (宣告在 <linux/tqueue.h>), 分別是 :

排程器佇列 (scheduler queue) 在行程環境內運作 ,所以限制較寬鬆 .2.4是以專用的 kernel thread 來執行的 ,稱為 keventd, 並且必須透過 schedule_task() 來存取 . 計時器佇列 (tq_timer) 由計時器時脈訊號觸發的佇列 . 必須遵守中斷模式規則 即期佇列 (tq_immediate) 可能再系統呼叫返回之前或是排程器介入時 ,看何者先 到 .會在中斷期被消化掉 .

Page 51: Driver
Page 52: Driver

kmalloc 的來龍去脈 (1/3)

定義在 <linux/malloc.h>,其函式的速度很快 ,因為它不會清理所配置到的記憶體 .

void *kmalloc(size_t size, int flags); size 引數: 核心只能以頁為單位來整塊配置 ,並且採用一種特殊的分頁配置技術 , 以充分利用系統的 RAM.

Linux 處理記憶體配置的方法 , 是製作一組記憶體物件集散區 ,同一集散區的記憶物件都有固定容量 .

核心只能配置幾種預定容量的位元組陣列 , 如果沒有剛好你要的容量 ,你就必須選擇大一級的容量 .

如你要配置一個 2000bytes 的緩衝區 ,最好選擇 2000而不要選擇2048.

kmalloc() 一次可配置的容量上限是 128KBytes.

Page 53: Driver

kmalloc 的來龍去脈 (2/3)

flags 引數: GFP_KERNEL 平常的核心記憶體配置 ,配置到的記憶空間屬於 c

urrent 行程 .有可能休眠 . GFP_BUFFER 用於管理快取暫存區 ,容許配置者可以休眠 . 不同

於 GFP_KERNEL之處 , 在於它是藉由出清髒頁到磁碟 ,以換取可用的記憶空間 .

GFP_ATOMIC 供 interrupt handler 以及任何在行程外部的程式

來配置記憶體 .絕不會休眠 .

Page 54: Driver

kmalloc 的來龍去脈 (3/3)

GFP_USER 代替 user-process 配置記憶體 ,有可能休眠 , 而且是屬於低優先度的要求 .

GFP_HIGHUSER 類似 GFP_USER,但是從高址劃分區取得可用空間 . __GFP_DMA 配置可供 DMA傳輸使用的記憶體 .可搭配 GFP_KERNEL 或 GFP_ATOMIC的其中之一

__GFP_HIGHMEM 要求配置高址劃分區 .在沒有高址劃分區的平台上 ,

此旗標沒有作用 .它是 GFP_HIGHUSER遮罩的一部分 ,除此之外幾乎沒有作用 .

Page 55: Driver

前瞻快取 (Lookaside Caches) (1/2)

驅動程式經常需要一次又一次配置許多同樣大小的物件 ,為此核心提供一些特殊的集散區 ,稱之為前瞻快取 .

Linux記憶快取的型別為 kmem_cache_t, 可藉由呼叫 kmem_cache_create() 來產生 :

kmem_cache_t *kmem_cache_create(const char *name, size_t size, size_t offset, unsigned long flags, void (*constructor)(void *,kmem_cache_t *,unsigned long flags), void (*destructor)(void *,kmem_cache_t *,unsigned long flags));

name: 同時代表函式與快取的符號 ,名稱最長不得超過 19個字元 ( 包含’ \0’在內 ).

offset: 第一個物件在記憶頁裡的起始位置 ,用來確保配置到的物件一定放在特定的對齊位置上 ,用 0核心會幫你完成 .

Page 56: Driver

前瞻快取 (Lookaside Caches) (2/2)

flags: SLAB_NO_REAP: 保護快取不因系統記憶不足而縮減容量 .通常不需要設立 . SLAB_HWCACHE_ALIGN: 要求每個資料物件都要對齊一條快取線 . SLAB_CACHE_DMA: 要求每個資料物件都配置在 DMA可存取的記憶體 . 物件快取產生之後 ,呼叫 kmem_cache_alloc(kmem_*cache, int f

lags) 來配置物件 cache: 先前建立的快取 flags:和 kmalloc() 相同 使用 void kmem_cache_free(kmem_cache_t *cache, const void

*obj)釋放物件 使用 int kmem_cache_destory(kmem_cache_t *cache)釋放空間

Page 57: Driver

get_free_page( ) 與其相關函式 (1/3)

如果模組需要比較大塊的記憶體 ,通常最好使用分頁配置技術 . 下列函式用來配置記憶頁 : get_free_page() 回傳一指向新記憶頁的指標 ,內容全清為 0. __get_free_page() 類似 get_free_page(),但不清除內容 . __get_free_pages() 配置一個跨頁的連續記憶區 ,傳回指向記憶區第一位元組的指

標 .內容不被清除 . __get_dma_pages() 類似 __get_free_pages(),但保證配置到的記憶體可作為 DMA

用途 . 在 Linux2.2 之後 ,使用 __get_free_pages()搭配 __GFP_DMA也可達到相同效果 .

Page 58: Driver

get_free_page( ) 與其相關函式 (2/3)

上述原型如下

這裡的 flags,意義與 kmalloc() 一樣 , 而且同樣以 GFP_KERNEL 或 GFP_ATOMIC最常用 ,頂多是在搭配 __GFP_DMA 或 __GFP_HIGHMEM.

order 是你要配置或釋放頁數的 2的對數 .如值為 0則為三頁 ;8頁則其值為 3.

在 Linux2.0,order值的上限是 5,Linux2.2 之後最高值為 9.無論如何 ,階數越大 ,配置失敗的機會就越高 .

unsigned long get_free_page(int flags);unsigned long __get_free_page(int flags);unsigned long __get_free_pages(int flags, unsigned long order);unsigned long __get_dma_pages(int flags, unsigned long order);

Page 59: Driver

get_free_page( ) 與其相關函式 (3/3)

當程式不再需要先前配置的記憶頁時 ,就應該以下列函式將它們釋回 .

void free_pages (unsigned long addr, unsigned long order);

如果你釋放的頁數 ,不等於先前要求配置的頁數 ,則記憶表將會損毀 , 而系統很快就會遇到大麻煩 .

由於核心會盡力滿足配置的要求 , 所以 ,要把系統搞垮 , 使其反應遲鈍 , 時在是輕而易舉 – 只要配置夠多的記憶體就成了 .(癱瘓服務 – DoS的原理 )

Page 60: Driver

vmalloc() 與其相關函式 (1/3)

另一種的記憶體配置函式 ,這是讓你可以在虛擬位址空間(virtual address space) 配置一個連續記憶區的 vmalloc() 函式 .

請注意 ,構成連續虛擬位址的實體記憶體 ,不見得是連續的 ,而只是被核心是為一段連續的位址範圍而已 .

vmalloc() 與其親戚 ioremap()( 不算是配置函式 )的原型如下 :

#include <linux/vmalloc.h>void *vmalloc(unsigned long size);void vfree(void *addr);void *ioremap(unsigned long offset, unsigned long size);void iounmap(void *addr);

Page 61: Driver

vmalloc() 與其相關函式 (2/3)

kmalloc()和 get_free_pages() 所傳的位址 ,也是虛擬位址 ,而非真正用於定位實際記憶晶片的實體位址 .

CPU內部的 MMU會自動將虛擬位址轉換成實體位址 . vmalloc()和 ioremap 所用的位址範圍 ,則是完全靠人工虛構出來

的 ,每一次的配置動作 ,都必須適當修改分頁表 ,才能建構出(虛擬的 )記憶區 .

它們之間的差異在某些平台上 (x86),vmalloc() 傳回的位址 ,只是高於 kmalloc 所傳回的位址 .vmalloc() 的位址範圍從 VMALLOC_START到 VMALLOC_END, 定義在 <asm/pgtable.h>

利用 vmalloc() 配置的位址 ,不能用於 CPU之外 ( 如 DMA),因為只有 MMU 才能處理這類位址 . 所以可使用 vmalloc() 的正確時機 ,是當你需要配置一大塊僅供軟體使用的連續緩衝區時 .

Page 62: Driver

vmalloc() 與其相關函式 (3/3)

ioremap() 也會建構新的分頁表 ,和 vmalloc() 相同 ;不同的是它實際上並不配置任何記憶體 .它所傳回的值 , 是一個特殊的虛擬位址 ,可用來存取指定的實體位址範圍 .

ioremap() 最有用之處 , 是可用來將 PCI 緩衝區的 (實體 )位址映射到 kernel-space(虛擬 ) 位址 .例如 ,讓驅動程式用來存取 PCI顯示卡的視訊記憶體 .

基於相容性考量 , 不要將 ioremap() 傳回的位址當成記憶體的指標 ,就是不要直接存取該位址 .這個工作應交給 readb()和 CH8的 I/O函式來達成 .(Alpha沒有這項能力 ,和 PCI不相容 )

vmalloc() 能配置的容量 , 以及 ioremap() 可映射到 CPU位址空間的記憶體範圍 , 可說是幾乎沒有限制 . 兩者都是分頁導向技術(透過修改分頁表來達成要求 ).

vmalloc() 有個小缺點 ,它不能用於中斷期 ,因為它內部使用了 kmalloc(GFP_KERNEL) 來取得分頁表的儲存空間 ,因此有休眠的可能 .

Page 63: Driver

I/O 埠 v.s I/O 記憶體

I/O 暫存器 & 傳統記憶體 (RAM) 最大差別 : I/O 操作有副作用 ,記憶體操作沒有 記憶體存取速度對 CPU 效能影響致為關鍵 (ex: Cache,

重新安排 read/ write 指令順序 ) for 傳統記憶體 : 最佳化存取速度是有益的 for I/O暫存器 : 干擾到 I/O指令的副作用 ,可能發生錯誤

Solution: 設 memory barrier , 讓 compiler知道這段程式 不要最佳化

Page 64: Driver

Memory barrier

#include <linux/kernel.h> void barrier (void);/宣告一個 memory barrier /

#include <asm/system.h> void rmb (void); / 保證在barrier 前的 read 動作都完成 / void wmb (void);/ 保證在barrier 前的 write 動作都完成 / void mb (void);

P.S memory barrier 會影響效率 ,真正需要才使用

Page 65: Driver

Memory barrier 的用法

writel (dev->registers.addr, io_destination_address);

writel (dev->registers.size, io_size); writel (dev->registers.operation, DEV_READ); wmb (); writel (dev->registers.control, DEV_GO);

確認所有動作皆完成

write memory barrier

Page 66: Driver

使用 I/O 埠 (1/3)

驅動程式用 I/O埠前 ,必須先配置他們 ,才可讀 or寫埠 <asm/io.h>定義用來存取 I/O埠的內插函式 #include <linux/ioport.h> int check_region(unsigned long start,unsigned lo

ng len); struct resource *request_region(unsigned long st

art,unsigned long l

en,char *name); void release_region(unsigned long start,unsigned

long len);

Page 67: Driver

使用 I/O 埠 (2/3)

For 1 byte 埠 unsigned inb(unsigned port); void outb(unsigned char byte,unsigned port); For 16bits埠 (word寬度 ) unsigned inw(unsigned port); void outw(unsigned short word,unsigned port); For 32bits埠 (longword寬度 ) unsigned inl(unsigned port); void outl(unsigned longword,unsigned port);

Page 68: Driver

使用 I/O 埠 (3/3)

String instruction 有些 processor 有提供字串指令 , 讓一連串同大小的 b

ytes,words,long 讀 or寫到 I/O埠 , 達到迴圈效果 暫停 I/O 舊裝置如 ISA跟不上處理器的傳輸速度 , 需要暫停函式 ,

如 : inb_p() ,outb_p() 平台相依性 I/O指令和處理器有高度相依性 , 必須針對特定平台 (ex: Alpha ,ARM, PowerPC, SPARC…)

Page 69: Driver

數位 I/O 埠 (1/2)

把值寫出到輸出埠 , 會被轉換成對應輸出腳位的電子信號 每台 PC 都有兩個並列埠 ,第一個介面從 0X378開始 ;第二個從 0X278開始

每個並列埠都有三個暫存器 : 資料暫存器 (bidirectional to pin2~pin9) 狀態暫存器 (read-only) 控制暫存器 (output-only) 唯一與腳位信號無關 :控制埠的 bit4 (0x10), 可發出 irq

See figure below…

Page 70: Driver

數位 I/O 埠 (2/2)

Page 71: Driver

使用 I/O 記憶體

I/O記憶體只是類似 RAM的特殊區域 ,不同的是處理器可從其匯流排存取特定硬體裝置

Driver 必須要求 kernel 將實體位址搬入 driver 的可見範圍 , 所以要先呼叫 ioremap()

直接映射記憶體 ex: PDA用的 MIPS 處理器 ,內有兩段位置範圍 ( 各 51

2 Mb)直接映射到實體位址 ,在此區 MMU不會理會 ,也無Cache.

軟體映射 I/O記憶體 PCI 裝置的位置是由系統軟體指派 , 每次開機都會不同 ,

所以要用 ioremap() 將虛擬位址指向裝置