開發驅動程式的第一步

最初的程式

#include <linux/module.h>
#include <linux/init.h>

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
    printk(KERN_ALERT "driver loaded\n");

    return 0;
}

static void hello_exit(void)
{
    printk(KERN_ALERT "driver unloaded\n");
}

module_init(hello_init);
module_exit(hello_exit);

建構驅動程式 Makefile:

obj-m := hello.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

rth rtjh rtj

「obj-m」指定最後要建立的 .ko 檔是以哪些 .o 檔構成的。 想建立「hello.ko」,這邊指定的就是「hello.o」,「:=」為 GNU make 的擴充指派運算子。

make 指令的 「-C」選項,是指定 Makefile 的位置,此範例是以 uname 取得 kernel 版本,如果編譯給其它 kernel 使用,需修改路徑。

執行 make 即會得到所需的 「hello.ko」。

file hello.ko 檢查,會發現它是個 ELF binary 檔案。
modinfo hello.ko 檢查,可以取得驅動程式的授權與版本標記。

如果想看 pre-processor (預處理;cpp) 之後的結果,並將這個結果存成檔案的話,可以在 Makefile 中多加這一行:

CFLAGS += -E

不需要行號的話,可以再加「-P」。
make 之後,就會在 gcc 的 -o 指定檔案存入 pre-process 結果。

驅動程式的載入與卸載
透過 insmod 指令可以載入 module。 lsmod 指令可以列出現在載入的所有 kernel module。 卸載驅動程式的工作可由 rmmod 指令完成。 「/proc/modules」會列出所有已載入的驅動程式。

modprobe modprobe 同樣可用來載入驅動程式,但有一些不同:
引數不是檔名,而是「module名」。
會自動到 /lib/modules/'uname -r' 搜尋檔案會參考 /lib/modules/'uname -r'/modules.dep ,如果有需要用到其它的 modules,就會自動一起載入

開機時載入驅動程式

Linux 在開機時自動載入驅動程式的功能,是在 rc script 之內以 insmod 與 modprobe 指令完成的。 常用的 rc script 如下:

/etc/rc.local
/etc/rc.sysinit
/etc/rc.d/rc?.d

自訂 Makefile

加上 「V=1」就可以看到詳細的建構過程。

make -C /lib/modules/$(shell uname -r)/build M=$(PWD) V=1 modules

支援許多個原始檔:

CFILES := main.c sub.c
obj-m := hello.o

hello-objs := $(CFILES:.c=.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

這邊要注意的是 CFILES 中的檔案名稱,不能與 obj-m 的名稱有相同的情況,否則 make 會產生 confusion。

補充說明

對多個 .c 檔情況做一個說明:希望創建一個名叫 hello 的 module,有三個 .c 檔,分別為 hello.c, file1.c 和 file2.c。

這樣是有問題的,因為在 Makefile 中 obj-m := hello.o,這是指定模塊的名稱, hello-objs := file1.o file2.o hello.o,這裡是說 hello module 包括的 .obj 檔,如果在裡面不填寫 hello.o,那麼實際並沒有編譯hello.c,而是在 CC[M] file1.o 和 file2.o,通過 LD[M] 得到模塊 hello.o,如果在這裡填寫了 hello.o,那麼在 obj-m 和 hello-objs 中都含有 hello.o,對 make 來講會產生循環和混淆,因此不能這樣寫。

如果我們由多個 .c 檔來構造一個模塊,那麼 .c 檔的名字不能和 module 名字一樣, 在這個例子中我們可以將 hello.c 改名為 main.c,在Makefile中obj-m := hello.o ,hello-objs = file1.o file2.o hello_main.o。

  • main.c
#include <linux/module.h>
#include <linux/init.h>

MODULE_LICENSE("Dual BSD/GPL");

extern void sub(void);

static int hello_init(void)
{
    printk(KERN_ALERT "driver loaded\n");

    sub();

    return 0;
}

static void hello_exit(void)
{
    printk(KERN_ALERT "driver unloaded\n");
}

module_init(hello_init);
module_exit(hello_exit);
  • sub.c
#include <linux/module.h>
#include <linux/init.h>

void sub(void)
{
    printk("%s: sub() called\n", __func__);
}
  • Makefile
CFILES = main.c sub.c
obj-m += hello.o
hello-objs := $(CFILES:.c=.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

加到 kernel 配置選單

可以將自己寫的驅動程式加到 kernel 配置選單內,這邊試著把它加到 「Character devices」分類底下。 首先,到 Linux kernel 的原始碼目錄(/usr/src/kernels/2.6.23.1-42.fc8-i686)內的 devices/char 建立子目錄,然後將原始檔放到裡面。

cd drivers/char
mkdir hello
cd hello
ls

Makefile main.c sub.c 並修改此 Makefile 成如下:

obj-$(CONFIG_HELLO) += hello.o
hello-objs := main.o sub.o

接著修改 Character devices 本身的 Makefile 與 Kconfig。 在 Makefile 中,新增一行:

obj-$(CONFIG_HELLO) += hello/

在 Kconfig 中,新增四行:

config HELLO
tristate "hello driver"
---help---
This is a sample driver.

做好上述修正後,在配置 kernel 的選單中,就會看到「hello」驅動程式了。

make menuconfig

驅動程式的依賴關係

當驅動程式需要呼叫另一個驅動程式提供的函式時,就是「這兩個驅動程式彼此依賴」。

要查閱 kernel 內含的 symbol name ,可以查閱 /proc/kallsyms

4-2、建立靜態裝置驅動程式

直接連結到 vmlinux 的驅動程式類型,其建立方式與 kernel module 幾乎相同,只有連結的方式不一樣。
在從 kernel 配置選單建構驅動程式時,需要在 kernel 配置選單中,將驅動程式的類型設為「*」 設成「*」後,就會靜態連結到 kernel 之內,不會產生獨立的 .ko 檔。
以此種 kernel 開機,會在 runlevel 1 的階段就載入驅動程式。
啟動後,用 lsmod 看不到此驅動程式,因為它不是 kernel module。

4-3、原始碼解說

檔頭檔位於 /lib/modules/uname -r/build/include 目錄內。
接著透過 MODULE_LICENSE 巨集定義驅動程式的授權方式。
載入裝置驅動程式的進入點,函式名稱可透過 module_init 巨集指定,基本上,函式都要加上 static,讓命名空間階制在檔案內。
printk() 輸出的資料會跑到 kernel buffer,可以用 dmesg 指令查閱。
int printk(const char *fmt, ...); 格式化字串(fmt) 的開頭加上「<編號>」的話,就會指定訊息的等級,在 include/linux/kernel.h 之內定義有相關巨集。
以 rmmod 卸除驅動程式時,呼叫函式的進入點是透過 module_exit 巨集指定。
hello.ko在建立時,原始碼裡不過只有兩個函式,在建構時,會加入一些其它符號,用 readelf 指定列出檔案中的符號就可以看到。
readelf -s hello.ko

4-4、進入點

呼叫 main 函式的流程 一般應用程式中,都覺得啟動程式後馬上會呼叫 main() 是理所當然的,但實際上在呼叫之前需要做不少事情。 用 strace 與 ltrace 之類的指令,可以看到系統呼叫、函式庫呼叫的內容。

file - determine file type
ldd - print shared library dependencies
strace - trace system calls and signals
ltrace - A library call tracer
gcc test.c
file a.out
ldd ./a.out
strace ./a.out
ltrace ./a.out

這樣就能發現 C語言的 main() 其實是由名為 glibc 的 C 函式庫呼叫的,而在 main() 執行 return 之後也是回到 glibc。
對應用程式來說,main()可以說是要讓 glibc 呼叫的入口,這個入口就稱為「進入點(entry point)」。

裝置驅動程式的進入點

驅動程式的進入點與一般應用程式不同,必須準備很多個,其中至少需要兩個進入點:
insmod 與 modprobe 呼叫的初始化函式
rmmod 呼叫的結束函式
以上述範例來說,分為是 hello_init() 及 hello_exit()。
其它的進入點還包含:
系統呼叫 中斷服務程序 計時器程序…等等。

「系統呼叫」是應用程式與 kernel 交換資訊用的介面,產生系統呼叫時,會在 CPU 引發中斷 (INT 80h),此時執行環境會從 user space 轉到 kernel space。

系統呼叫會在使用 open()與 read()、close()、ioctl() 之類的函式時自動產生。 驅動程式必須為每個系統呼叫分別準備進入點。
「中斷服務程序(ISR: Interrupt Service Routine)」是驅動程式在所控制的硬體產生「中斷」時,被呼叫用來處理這類信號(signal)的函式。
中斷服務程序負責處理中斷,但不知何時會發生中斷,所以是「非同步(asynchronous;ASYNC)」,系統呼叫則是「同步(synchronous:SYNC)」。

「計時器程序」是由 kernel 的計時器處理程序在經過特定的時間、或是以固定間隔執行特定工作時回呼的進入點。 由於計時器處理程序本身也是硬體「計時器中斷」的處理程式,所以被它呼叫的計程器程序也算中斷服務程序的一種。

執行環境

要理解驅動程式的運作,就必須先理解「context(執行環境)」。 裝置驅動程式的 context 的意義是
從進入點開始,到最後一個函式的整個處理流程。
對驅動程式而言,只有下面這2種 context:
Process context(一般行程執行環境)
Interrupt context(中斷執行環境)

驅動程式的 Process context 個數為應用程式進入點的個數總和。
同樣地, interrupt context 的個數是核心進入點的個數總和。
重要的是:
所有的 context 都可能同時發生
Context 何時都可能發生(非同步)