一個簡單的驅動
下面我們來編寫第一個驅動程式,它很簡單,在運行時會輸出‘Hello World’訊息。
// hello.c #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> static int __init hello_init(void) { printk(KERN_ALERT "Hello World!/n"); return 0; } static void __exit hello_exit(void) { printk(KERN_ALERT "Goodbye World!/n"); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); |
這就是一個簡單的驅動程式,它什麼也沒做,僅僅是輸出一些資訊,不過對於我們來說這已經足夠了。儲存這個程式,命名為hello.c。在寫一個Makefile檔案用來編譯它,Makefile和hello.c檔案儲存在同一個目錄下。
##Makefile ifneq ($(KERNELRELEASE),) MODULE_NAME = helloworld $(MODULE_NAME)-objs := hello.o obj-m := $(MODULE_NAME).o else KERNEL_DIR = /lib/modules/`uname -r`/build MODULEDIR := $(shell pwd) .PHONY: modules default: modules modules: make -C $(KERNEL_DIR) M=$(MODULEDIR) modules clean distclean: rm -f *.o *.mod.c .*.*.cmd *.ko rm -rf .tmp_versions endif |
編譯並運行這個模組:
//需要root許可權來運行 make
insmod helloworld.ko
rmmod helloworld.ko |
儘管我們對它的一些細節還不夠瞭解,它確實神奇的工作了,這個Hello World資訊輸出到了螢幕終端上(不是VT),或者系統的Kenrel log裡(/var/log/messages),你可以通過運行dmesg來看到這些資訊。
驅動基礎
我們通過分析上面的代碼來瞭解一個驅動程式的基本概念。
就像你寫C程式需要包含C庫的標頭檔那樣,Linux核心編程也需要包含Kernel標頭檔,大多的Linux驅動程式需要包含下面三個標頭檔:
#include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> |
-
- init.h
定義了驅動的初始化和退出相關的函數,
- kernel.h
定義了經常用到的函數原型及宏定義
- module.h 定義了核心模組相關的函數、變數及宏。
- 初始化
任何一個驅動都去需要提供一個初始化函數,當驅動載入到核心中時,這個初始化函數就會被自動執行,初始化的函數原型定義如下:
typedef int (*initcall_t)(void); |
驅動程式是通過module_init宏來聲明初始化函數的:
static int __init hello_init(void) { printk(KERN_ALERT "Hello World!/n"); return 0; } module_init(hello_init); |
__init
宏告訴編譯器如果這個模組被編譯到核心則把這個函數放到(.init.text)段,這樣當函數初始化完成後這個地區可以被清除掉以節約系統記憶體。
Kenrel啟動時看到的訊息“Freeing unused kernel memory: xxxk freed”同它有關。
初始化函數是有傳回值的,只有在初始化成功是才返回0,否則返回錯誤碼(errno)。
如果驅動程式編譯成模組(動態載入)模式,那麼它需要一個清理函數。當移除一個核心模組時這個函數被調用執行清理工作。清理函數的函數原型定義為:
typedef void (*exitcall_t)(void); |
驅動程式是通過module_exit宏來聲明清理函數的:
static void __exit hello_exit(void) { printk(KERN_ALERT "Goodbye World!/n"); } module_exit(hello_exit); |
同__init類似,如果驅動被編譯進核心,則__exit宏會忽略清理函數,因為編譯進核心的模組不需要做清理工作。顯然,__init和__exit對動態載入的模組是無效的。
Linux核心是按照GPL發布的,同樣Linux的驅動程式也要提供著作權資訊,否則當載入到核心中是系統會給出警告資訊。Hello World例子中的著作權資訊是GPL。
二、Kbuild與Makefile
從Linux核心2.6開始,Linux核心的編譯採用Kbuild系統,這同過去的編譯系統有很大的不同,尤其對於Linux核心模組的編譯。在
新的系統下,Linux編譯系統會兩次掃描Linux的Makefile:首先編譯系統會讀取Linux核心頂層的Makefile,然後根據讀到的內容
第二次讀取Kbuild的Makefile來編譯Linux核心。
Linux核心Makefile分類
Kernel Makefile位於Linux核心原始碼的頂層目錄,也叫 Top
Makefile。它主要用於指定編譯Linux
Kernel目標檔案(vmlinux)和模組(module)。這編譯核心或模組是,這個檔案會被首先讀取,並根據讀到的內容配置編譯環境變數。對於內
核或驅動開發人員來說,這個檔案幾乎不用任何修改。
Kbuild系統使用Kbuild
Makefile來編譯核心或模組。當Kernel Makefile被解析完成後,Kbuild會讀取相關的Kbuild
Makefile進行核心或模組的編譯。Kbuild
Makefile有特定的文法指定哪些編譯進核心中、哪些編譯為模組、及對應的源檔案是什麼等。核心及驅動開發人員需要編寫這個Kbuild
Makefile檔案。
ARCH Makefile位於ARCH/$(ARCH)/Makefile,是系統對應平台的Makefile。Kernel Top Makefile會包含這個檔案來指定平台相關資訊。只有平台開發人員會關心這個檔案。
Kbuild Makefile
Kbuild Makefile的檔案名稱不一定是Makefile
,儘管推薦使用Makefile這個名字。大多的Kbuild檔案的名字都是Makefile。為了與其他Makefile檔案相區別,你也可以指定Kbuild Makefile的名字為Kbuild
。而且如果“Makefile”和“Kbuild”檔案同時存在,則Kbuild系統會使用“Kbuild”檔案。
Kbuild Makefile的一個最主要功能就是指定編譯什麼,這個功能是通過下面兩個對象指定的obj-?和xxx-objs:
obj-?指定編譯什麼,怎麼編譯?其中的“?”可能是“y”或“m”,“y”指定把對象編譯進核心中,“m”指定把對象編譯為模組。文法如下;
obj-? = $(target).o
target為編譯對象的名字。如果沒有指定xxx-objs,這編譯這個對象需要的源檔案就是$(target).c或$(target).s。如果指
定了$(target)-objs,則編譯這個對象需要的源檔案由$(target)-objs指定,並且不能有$(target).c
或$(target).s檔案。
xxx-objs指定了編譯對象需要的檔案,一般只有在源檔案是多個時才需要它。只要包含了這兩行,Kbuild Makefile就應該可以工作了。
有時一個對象可能嵌入到另一個對象的目錄下,那個如何編譯子目錄下的對象呢?其實很簡單,只要指定obj_?的對象為子目錄的名字就可以了:obj-? = $(sub_target)/其中“?”可以是“y”或“m”,$(sub_target)是子目錄名字。
儘管在大多數情況下不需要指定編譯器選項,有時我們還是需要指定一些編譯選項的。
- ccflags-y, asflags-y and ldflags-y
這些編譯選項用於指定cc、as和ld的編譯選項
編譯外部模組
有時候我們需要在核心原始碼數的外面編譯核心模組,編譯的基本命令是:make -C $(KERNEL_DIR) M=`pwd` modules
我們可以把這個命令整合到Makefile裡,這樣我們就可以只輸入“make”命令就可以了。回想上一章的那個Makefile,它把Normal Makefile 和Kbuild Makefile整合到一個檔案中了。為了區別Kbuild Makefile 和Normal Makefile,這樣我們改寫Makefile為如下形式,並且添加Kbuild Makefile - “Kbuild”。
##Makefile ifneq ($(KERNELRELEASE),) include "Kbuild" else KERNEL_DIR = /lib/modules/`uname -r`/build MODULEDIR := $(shell pwd) .PHONY: modules default: modules modules: make -C $(KERNEL_DIR) M=$(MODULEDIR) modules clean distclean: rm -f *.o *.mod.c .*.*.cmd *.ko rm -rf .tmp_versions endif |
## Kbuild MODULE_NAME = helloworld $(MODULE_NAME)-objs := hello.o obj-m := $(MODULE_NAME).o |
一般不需要在Makefile裡包含如下代碼,這樣寫完全是為了相容老版本的Kbuild系統。KERNELRELEASE變數在Kernel Makefile裡定義的,因此只有在第二次由Kbuild讀取這個Makefile檔案時才會解析到Kbuild的內容。
ifneq ($(KERNELRELEASE),) include "Kbuild" else ... endif |
外部標頭檔
有時需要串連核心原始碼外部的系統標頭檔,但Kbuild系統預設的系統標頭檔都在核心原始碼內部,如何使用外部的標頭檔呢?這個可以藉助於Kbuild系統的特殊規則:
EXTRA_CFLAGS可以給Kbuild系統添加外部系統標頭檔,
EXTRA_CFLAGS += $(ext_include_path)
一般外部標頭檔可能位於外部模組源檔案的目錄內,如何指定呢?這可以藉助$(src)或$(obj)
$(src)是一個相對路徑,它就是Makefile/Kbuild檔案所在的路徑。同樣$(obj)就是編譯目標儲存的路徑,預設就是原始碼所在路徑。
因此,我們修改Kbuild檔案添加 EXTRA_CFLAGS 來包含外部標頭檔儘管在這個驅動裡沒有引用外部系統標頭檔:
## Kbuild
MODULE_NAME = helloworld
$(MODULE_NAME)-objs := hello.o
EXTRA_CFLAGS := -I$(src)/include
obj-m := $(MODULE_NAME).o