Skip to content

Linux Device Driver系列 2 (上)

前置

設定kernel版本至2.6

可以使用kernel source tree
一個資料夾存放所有kernel相關資源,可以在此建立新kenrel 、安裝、並且重啟新的kernel

Ubuntu 實例

參照這裡How to install an old kernel
Ubuntu版本要舊到可以兼容kernel 2.6

  • 下載對應版本三檔
  • 移動到某資料夾,sudo dpkg -i *.deb
  • sudo reboot 並在期間長壓shift 進入grub模式,並選擇對應的kernel 開機
  • 最後使用uname -r 檢查版本是否正確

雷點之一:

dpkg 發生相依問題

解法:注意錯誤訊息,看缺少的東西
比如此次缺少module-init-tools,故需
sudo apt install module-init-tools

雷點之二:

Failed to symbolic-link /boot/initrd.img-<version> to initrd.img: File exists

解法: sudo rm /initrd.img
注意這個行為非常危險,需要先行備份或使用虛擬機等安全情況下使用

雷點之三:

開機時顯示 FATAL kernel is too old

解法: 要安裝舊版本的ubuntu系統

小知識1:

Ubuntu 很多檔案格式是與Debian共用的,所以可以看到這次下載的副檔名皆為deb

小知識2:

uname 可以看很多系統資訊 uname –help 看選擇

小知識3:

GNU GRUB(簡稱GRUB)是一個來自GNU項目的啟動引導程序。
GRUB是多啟動規範的實現,它允許用戶可以在計算機內同時擁有多個操作系統,並在計算機啟動時選擇希望運行的操作系統。 GRUB可用於選擇操作系統分區上的不同內核,也可用於向這些內核傳遞啟動參數

如何用18.x 的Ubuntu寫kernel ?

  1. 製作Makefile
obj-m += <yourfile>.o
all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

其中檔案要用C語言寫,並且最後make成.ko檔

  1. 查看訊息使用dmesg

hello world - 第一個module

#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
static int hello_init(void)
{
 printk(KERN_ALERT "Hello, world\n");
 return 0;
}
static void hello_exit(void)
{
 printk(KERN_ALERT "Goodbye, cruel world\n");
}
module_init(hello_init);
module_exit(hello_exit);

此程式碼大綱:

  • MODULE_LICENSE提供證明,才進的去kernel
  • 創建兩個函式,一個用在載入kernel時,讓kernel顯示訊息,另一個用在移除kernel時,讓kernel顯示訊息
  • module_init 與 module_exit 則是link上述function 至所說的時點中

printk函數

存在於Linux kernel函數庫中,類似於C libarary的printf,但因為是kernel,所以獨立於(不需要)C libarary
透過insmod插入kernel後即可取用kernel內的公開符號(public symbols)
KERN_ALERT 則告知此訊息的重要性(priorty)

insmod , rmmod

用於插入與移除寫好的module
比如insmod ./hello.ko 就會將hello.ko放入kernel,並依函數定義顯示Hello, world
rmmod hello 就會將hello.ko移出kernel,並依函數定義顯示Goodbye, cruel world

  • 註: 不同系統不一定會將結果顯示在螢幕上,可能也需要dmesg去看,或者去/var/log/messages 找

init 與 exit 的工作

兩者有點像事件導向的程式設計
init告訴kernel,功能已準備就緒,隨時可以調用我
exit告訴kernel,功能已被移除,不要再調用我
同時exit,也必須負責回收init所要過的資源

關於module

kernel module與一般程式碼之不同

  1. kernel module 幾乎無法link外部函式,只能用內部kernel已存在的(如printk)
  2. kernel module 發生error是很危險的,可能導致系統崩潰

user mode 與 kernel mode

OS 必須盡到分配資源 與 阻止未授權行為的功能,因此user mode 與 kernel mode出現於OS 理論中
實作上會在CPU內建立gate,以區分執行等級,低階等級下某些重要操作不被允許
同時Unix/Linux系統則利用這個硬體特性,做出了sudo與user兩個模式

這兩個模式也當然會有自己的memory分區

當有system call 發生 或 hardware interrupt,程式就會由user mode 轉入 kernel mode
Kernel code 的system call會執行於原呼叫程式(caller),並且能調用原程式的資訊 (比如printk(var) 的 var變數紀錄於原process的記憶體中)
而處理interrupt的程式則獨立於任何process

最後,module的角色可視為kernel功能的延伸,因此是在kernel space中執行,並且可能同時執行system call 與 interrupt handle

Concurrency

很直覺的,kernel , device 的資源一定是被眾多process競逐的
所以需注意Critical section問題
並且linux適用於多核心與支持interrupt,preempt 因此 Concurrency是很重要的議題 (效率 與 確保資訊正確 )

linux 的kernel 包含 driver 需要有可重入(reentrancy)的考量

關於 可重入介紹
簡單來說就是要考慮到共用的問題

Current Process

kernel可經由一個current 的 global item得到當下執行的process (current 定義於 asm/current.h)
current 會回傳一個process的 struct task_struct 指標 (struct task_struct 定義於 linux/sched.h)
所謂當下執行的process,即為呼叫kernel system call的process (比如 open , read …)

然而,在現今多核心的情況下,再加上current 常常被呼叫到,因此current並不是真正意義上的global, 而只是被存於kernel stack 的 task_struct 指標

device driver 可以僅引入linux/sched.h就可以呼叫
比如

printk(KERN_INFO "The process is \"%s\" (pid %i)\n",
 current->comm, current->pid);

會印出process名字 與其 process id (名字就是file name )
同時也可以看出current會儲存這兩個資訊

其他注意事項

  1. kernel分配到的記憶體stack非常小,通常只有一張4096-byte page
    所以需要龐大變數時,最好動態的創造(heap)
    Review: 記憶體規劃架構
  2. kernel API 有一部分 double underscore (__)的函式
    這些函式非常底層,使用時需要額外注意
  3. kernel 不(輕易)做浮點數運算 參考這裡的討論
    簡而言之是為了效率問題,管理FPU(float point unit)太花時間

編譯與載入 module

關於深入文檔

可以由Documentation/kbuild、Documentation/Changes 查到完整的記載
其中ubuntu 要apt install linux-doc 並於 /usr/share/doc/linux-doc 查看

基礎的makefile (編譯部分)

# If KERNELRELEASE is defined, we've been invoked from the
# kernel build system and can use its language.
ifneq ($(KERNELRELEASE),)
 obj-m := hello.o   
# Otherwise we were called directly from the command
# line; invoke the kernel build system.
else
 KERNELDIR ?= /lib/modules/$(shell uname -r)/build
 PWD := $(shell pwd)
default:
 $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

其中obj-m是設定目標檔
KERNELDIR 是設定插入的kernel路徑 M=$(PWD) 則是泛化的指定當前路徑 ,M option是指會移到該路徑進行make
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules 寫死的例子是
make -C ~/kernel-2.6 M=`~/driver_learning` modules
modules是一個target,會去尋找obj-m裡面的清單進行make

載入部分

前面提及的insmod可以完成此任務
通常載入時會順便給Module Parameters,這個行為被稱為load-time configuration, 相較於compile-time configuration會更有彈性

insmod被實做於kernel/module.c
其中sys_init_moudle會分配 kernel memory給module使用、解析module內的kernel reference對應到kernel symbol table
並且呼叫module 的 init 函式

(注意: 在kernel中,system calls都會有 sys_ 的前綴詞,grep它們會很方便)
(注意2: 上次解決新電腦wifi問題時,也有使用到的modprobe功能基本上和insmod一樣是插入module,不一樣的是 modprobe會在其他現有的module中尋找欲插入module中未定義的kernel symbol,簡單來說就是比較適合在已有的龐大系統中,加入新的小設定 )

rmmod 會在module被kernel相信仍在作用時拒絕移出,有強制移除,但為了安全考量通常reboot會比較適當

lsmod 會列出現在已載入kernel的module
lsmod其實就是去讀取/proc/modules這個虛擬檔案
(也可以從/sys/module得到看到一樣的資訊) 如下例

Kernel Symbol Table

insmod會透過public kernel symbols table 來對應undfined symbol
(比如 printk , KERN_INFO)
該table會含global kernel item的地址,也就是函數與變數的位址

module在載入kernel時,所exported的symbol也會變成kernel symbols table 的一部分
未來可供其他module使用 (modprobe的例子)

module stacking :
可依高低階區分module,將高階的module建立在其他module之上 (使用他們定義的symbol)

可藉由
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);
兩種指令引入新變數,其中後者只能給GPL認證的使用

開始撰寫Device Driver

必備標頭檔

<linux/module>有眾多定義好的kernel symbol可以調用
<linux/init> 則有module_init() module_exit() 的定義

以上資訊在ubuntu中可以於 /lib/modules/$( uname -r )/build/include/linux 中找到

習慣上會將 MODULE_ 開頭的變數至於檔案最後

init , exit function

static int __init initialization_function(void)
{
 /* Initialization code here */
}
module_init(initialization_function);

是泛化的init module架構
同時因為只允許該檔案使用,通常會宣告static

Review static function Static function只能被宣告的Compilation Unit使用

__init關鍵字,會讓kernel知道這個函數在init完後就沒用了,可以丟棄

module可以註冊(register)許多不同的設備,包含device ,filesystem…
會由一個特殊的kernel function負責註冊,並且會此函數要吃被註冊的設備的資料結構的指標
同時此資料結構內也會有指標指向module
註冊函數會以register_ 開頭

同理 exit函數也有架構:

static void __exit cleanup_function(void)
{
 /* Cleanup code here */
}
module_exit(cleanup_function);

需要有exit函數,kernel才會允許卸載(unloaded)
__exit關鍵字表達此函數只能在卸載時被呼叫

參考資料

Ch2: Building and Running Modules
https://lwn.net/Kernel/LDD3/