1. 程式人生 > >linux裝置驅動第二篇:構造和執行模組

linux裝置驅動第二篇:構造和執行模組

上一篇介紹了linux驅動的概念,以及linux下裝置驅動的基本分類情況及其各個分類的依據和差異,這一篇我們來描述如何寫一個類似hello world的簡單測試驅動程式。而這個驅動的唯一功能就是輸出hello world。

在編寫具體的例項之前,我們先來了解下linux核心下除錯程式的一個重要函式printk以及幾個重要概念。

printk類似c語言的printf,是核心中輸出列印資訊的函式。以後驅動除錯中的重要性不言而喻,下面先做一個簡單介紹。

printk的級別

日誌級別一共有8個級別,printk的日誌級別定義如下(在include/linux/kernel.h中):  
#define KERN_EMERG 0/*緊急事件訊息,系統崩潰之前提示,表示系統不可用*/  
#define KERN_ALERT 1/*報告訊息,表示必須立即採取措施*/  
#define KERN_CRIT 2/*臨界條件,通常涉及嚴重的硬體或軟體操作失敗*/  
#define KERN_ERR 3/*錯誤條件,驅動程式常用KERN_ERR來報告硬體的錯誤*/  
#define KERN_WARNING 4/*警告條件,對可能出現問題的情況進行警告*/  
#define KERN_NOTICE 5/*正常但又重要的條件,用於提醒*/  
#define KERN_INFO 6/*提示資訊,如驅動程式啟動時,列印硬體資訊*/  
#define KERN_DEBUG 7/*除錯級別的訊息*/

沒有指定日誌級別的printk語句預設採用的級別是:DEFAULT_ MESSAGE_LOGLEVEL(這個預設級別一般為<4>,即與KERN_WARNING在一個級別上),其定義在kernel/printk.c中可以找到。在驅動除錯過程中開啟所有日誌資訊可使用echo 7 > /proc/sys/kernel/printk,相對應關閉日誌使用echo 0 > /proc/sys/kernel/printk

下面再來介紹幾個重要的概念,這些概念可以先做一個瞭解,後續的文章中還會提到。

核心空間和使用者空間

linux系統分為兩個級別。核心執行在最高級別,可以進行所有的操作。而應用程式執行在最低級別,處理器控制著對硬體的直接訪問以及對記憶體的非授權訪問。核心空間和使用者空間不僅有不同的優先順序等級,而且有不同的記憶體對映,有各自的地址空間。詳見記憶體管理。

應用程式只能通過系統呼叫或中斷從使用者空間切換到核心空間,其中系統呼叫是軟中斷(0x80號中斷)。執行系統呼叫的系統程式碼執行在程序上下文中,它代表呼叫程序執行操作,因此能夠訪問程序地址空間的所有資料。而處理硬體中斷的核心程式碼和程序是非同步的,與任何一個特定程序無關。

核心中的併發

核心程式設計區別於常見應用程式程式設計的地方在於對併發的處理。大部分應用程式除多執行緒外,通常是順序執行的,不需要關心由於其他事情的發生而改變它的執行環境。核心程式碼不是這樣,同一時刻,可能有多個程序使用訪問同一個模組。

核心程式設計要考慮併發問題的原因:1.linux是通常正在執行多個併發程序,並且可能有多個程序同時使用我們的驅動程式。2.大多數裝置能夠中斷處理器,而中斷處理程式非同步進行,而且可能在驅動程式正試圖處理其它任務時被呼叫。3.一些類似核心定時器的程式碼在非同步執行。4.執行在對稱多處理器上(SMP),不止一個cpu在執行驅動程式。5.核心程式碼是可搶佔的。

當前程序

核心程式碼可通過訪問全域性項current來獲得當前程序。current指標指向當前正在執行的程序。在open、read、等系統呼叫的執行過程中,當前程序指的是呼叫這些系統呼叫的程序。核心程式碼可以通過current指標獲得與當前程序相關的資訊。

核心中帶“__”的函式:核心API函式具有這種名稱的,通常都是一些介面的底層函式,應該謹慎使用。實質上,這裡的雙下劃線就是要告訴程式設計師:謹慎呼叫,否則後果自負。以__init為例,__init表明該函式僅在初始化期間使用。在模組被裝載之後,模組裝載器就會將初始化函式扔掉,這樣可以將函式佔用的記憶體釋放出來,已做它用。注意,不要在結束初始化之後仍要使用的函式(或者資料結構)上使用__init、__initdata標記。這裡摘抄網上的一段總結,如下。

__init, __initdata等屬性標誌,是要把這種屬性的程式碼放入目標檔案的.init.text節,資料放入.init.data節──這一過程是通過編譯核心時為相關目標平臺提供了xxx.lds連結指令碼來指導ld完成的。
   對編譯成module的程式碼和資料來說,當模組載入時,__init屬性的函式就被執行;
   對靜態編入核心的程式碼和資料來說,當核心引導時,do_basic_setup()函式呼叫do_initcalls()函式,後者負責所有.init節函式的執行。
   在初始化完成後,用這些關鍵字標識的函式或資料所佔的記憶體會被釋放掉。
1) 所有標識為__init的函式在連結的時候都放在.init.text這個區段內,在這個區段中,函式的擺放順序是和連結的順序有關的,是不確定的。 
2) 所有的__init函式在區段.initcall.init中還儲存了一份函式指標,在初始化時核心會通過這些函式指標呼叫這些__init函式指標,並在整個初始化完成後,釋放整個init區段(包括.init.text,.initcall.init等),注意,這些函式在核心初始化過程中的呼叫順序只和這裡的函式指標的順序有關,和1)中所述的這些函式本身在.init.text區段中的順序無關。 

下面我們來看一個驅動程式的hello world程式是如何實現的:

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

核心模組的編譯與應用程式的編譯有些區別,此hello world模組的編譯命令為:

make -C /xxx/xxx/kernel_src/ M=$(PWD) modules

其中/xxx/xxx/kernel_src/ 為已經配置編譯過的核心原始碼路徑,ubuntu下一般在/lib/modules/$(shell uname -r)/build目錄下。

此函式只有兩個函式,一個是hello_init,在insmod的時候執行,這個是模組的初始化函式,另一個是hello_exit,在rmmod的時候執行,是模組解除安裝時要執行的函式。此模組的唯一功能就是在insmod的時候輸出Hello,world,在rmmod的時候輸出Goodbye,cruel world。

在編寫應用程式時,我們一般都是由多個原始檔組成的,這個時候編譯肯定就不能繼續使用命令列編譯了,就要使用到Makefile。同樣,驅動模組的編譯也需要使用的makefile,下面就是一個在編譯含有多個原始碼檔案的驅動模組時可以參考的Makefile檔案。

Makefile模板

ifndef CROSS_COMPILE
export CROSS_COMPILE ?=arm-none-linux-gnueabi-
endif

ARCH ?= arm

SRC_DIR := /home/XXX/XXX
OBJ_DIR  := $(SRC_DIR)/obj
PWD := $(shell pwd)

LINUX_SRC ?= /home/XXX/kernel

CFG_INC = -I$(SRC_DIR) \
	-I$(DIR_A) \
	-I$(DIR_B)

CFG_FLAGS += -O2
EXTRA_CFLAGS  += $(C_FLAGS) $(CFG_INC) $(CFG_INC)

obj-m := mymodule.o

mymodule-objs := a.o
mymodule-objs += b.o
mymodule-objs += c.o

modules:
	@make ARCH=$(ARCH) -C $(LINUX_SRC) M=$(PWD) modules

clean:
	@echo "cleaning..."
	rm -f mymodule.ko mymodule.o mymodule.mod.* modules.order Module.symvers
	rm -f $(mymodule-objs)

以上就是這一篇的內容,下一篇會從簡單的字元驅動開始,介紹驅動編寫的主要內容。