1、引言
記得在學習VC++和C語言的時候,一開始都會以一個HELLO WORLD的例子作為示範,將學者逐漸引入殿堂,這個幾乎成了電腦程式設計語言學習必經的一個入門之路。
當然,在學習linux編程的時候也是這樣,下面的例子應該是再熟悉不過了:
首先用VI編寫一個C程式:vi hello.c
#include "stdio.h"
int main()
{
printf("hello world!!!/n");
return 0;
}
接著用GCC進行編譯:gcc -o hello hello.c
最後運行該程式:./hello
在終端上你會看到:hello world!!!
上
面的是在作業系統基礎上進行的使用者應用程式的開發。然而對於linux驅動程式的開發是絕然不同的,因為驅動程式的開發是運行在核心空間的,而應用程式是
運行在使用者空間的。雖然hello
world是一個簡單得不能再簡單的程式,但是對於嵌入式linux驅動程式的初學者來說,通過這個過程的操作可以對linux驅動程式開發的過程和其中
的一些概念有一個深刻的認識。所以,我在這裡也就以前學習linux的基礎上整理了一下,寫了這篇部落格。一方面是自己對這方面知識的回顧和鞏固,另一方面
更是希望這裡的內容能給大家提供那麼一點點有用的資訊,小弟心裡就很高興了。當然希望有高手可以做下評價和指導,及時糾正小弟的錯誤,謝謝先。
2、概念
驅動程式作為系統核心的一部分,它工作在核心態,而應用程式工作在使用者態。也就是說,程式不能直接通過指標,把使用者空間的資料地址傳遞給核心(因為MMU
映射的地址根本不一樣)。要想在應用程式和驅動程式之間傳遞資料(指標),就需要經過轉換。把使用者態“看到”的空間地址轉換成核心態可訪問的地址。
Linux系統提供了一系列方便的函數實現這種轉換,如get_user、put_user、copy_from_user、copy_to_user
等,它們自己負責存取權限的檢查,使用時,不需要關係更多的問題。
Linux核心把驅動程式劃分為3種類型:字元裝置、塊裝置和網路裝置。字元裝置和塊裝置可以像檔案一樣被訪問。它們的主要區別不在於能否seek,而是
在於系統對於這兩種類型裝置的管理方式。應用程式對於字元裝置的每一個I/O操作,都會直接傳遞給系統核心對應的驅動程式;而應用程式對於塊裝置的操作,
要經過系統的緩衝區管理,間接傳遞給驅動程式處理。塊裝置的這種管理方式是為儲存提供最佳化的;而字元裝置的管理方式是為操作提供最佳化的。至於網路裝置,它
在Linux系統中是一類比較特殊的裝置它不像字元裝置或塊裝置那樣通過對應的裝置檔案節點去訪問,核心也不再通過read和write等調用去訪問網路
裝置。Linux的網路系統主要是基於BSD
UNIX的通訊端機制,在系統和驅動程式之間有專門的資料結構進行資料轉送,系統支援對資料發送和資料接收緩衝,提供流量控制機制,提供更多的協議支援。
在linux系統中,驅動程式都做成模組的形式,也就是module。簡單的說,一個模組提供一個功能,這些模組是可以按照需要隨時裝入核心空間和從核心空間卸載的。因此,核心模組是為了給核心動態增減功能而設計的,並不僅僅是限於驅動程式。
關於核心模組初始化(載入)函數
當使用者輸入命令“insmod
模組檔案名稱”(或者其他方式)載入核心模組時,系統會檢測此模組能否被載入,如果能被載入,核心調用模組的初始化函數。在linux
2.4中,核心模組的初始化函數名為init_module()。但如果驅動程式需要編譯進核心,則初始化函數不能與核心的其他部分(包括其他核心模組)
的函數同名。這樣,如果程式可能編譯到核心中時就比較麻煩。不過在這個標頭檔中定義了宏module_init(),用來屏蔽兩者的差別,事實上,使用這個宏可以使驅動程式向上相容linux 2.6,而相容linux 2.0也比較方便。
關於核心模組清除(卸載)函數
當使用者輸入命令“rmmod
模組檔案名稱”(或者其他方式)卸載核心模組時,此時,系統會檢測此模組是否能被卸載,核心將調用模組清除函數。在linux
2.4中,清除函數的函數名稱為cleanup_module()。但如果驅動程式需要編譯進核心,則初始化函數不能與核心的其他部分(包括其他核心模
塊)的函數同名。這樣,如果程式可能編譯到核心中時就比較麻煩。不過在這個標頭檔中定義了宏module_exit(),用來屏蔽兩者的差別。
3、執行個體
因為核心模組需要載入到核心空間,所以其程式的編寫與一般應用程式不同,在裡面再也找不到類似main()這樣的入口函數,下面對應函數相應的原始碼
hello.c,介紹一個驅動模組的寫法。(由於角括弧不能在部落格上顯示,固用雙括弧代替,在編程調試的時候換回來就可以了)。
#ifndef __KERNEL__
#define __KERNEL__
#endif
#ifndef MODULE
#define MODULE
#endif
#include 《linux/module.h》 //所有模組都需要的標頭檔
#include 《linux/sched.h》
#include 《linux/kernel.h》
#include 《linux/init.h》 // init和exit相關宏
MODULE_LICENSE("GPL");
int text_init(void){
printk("<0>Hello World!");
return 0;
}
void text_cleanup(void){
printk("<0>Goodbye World!");
}
module_init(text_init); //註冊載入時執行的函數
module_exit(text_cleanup); //註冊卸載時執行的函數
4、調試
一個Linux核心模組需包含模組初始化和模組卸載函數,前者在insmod的時候運行,後者在rmmod的時候運行。初始化與卸載函數必須在宏module_init和module_exit使用前定義,否則會出現編譯錯誤。
程式中的MODULE_LICENSE("GPL")用於聲明模組的許可證。
編譯:
gcc -c -I /usr/src/linux-2.4/include/ hello.c
運行:
insmod hello.o
終端上會顯示:
localhost kernel:Hello World!
同時在/proc/modules裡面會看到相應的裝置資訊:
more /proc/modules
你會看到(或許後面的數值不一樣):
hello 844 0 (unused)
.......
卸載驅動程式:
rmmod hello
你將會在終端上面看到:
localhost kernel:Goodbye World!
5、注意:
<1>在gcc編譯選項中增加-c
<2>在gcc編譯選項中定義兩個宏:-DMODULE -D__KERENL__
或直接在源檔案中定義這兩個宏:
#define MODULE
#define __KERNEL__
<3>在源檔案中包括module.h檔案:
#include "linux/module.h"
<4>假定你現在啟動並執行核心的源碼目錄絕對路徑是MyKernelSrcPath,在gcc編譯時間增加選項:
-I $MyKernelSrcPath/include (如-I /usr/src/linux/include)
<5>某些時候用insmod -f能夠成功載入,但需謹慎使用。
<6>如果看不到用printk列印的資訊,可以用dmesg命令看。
<7>列印訊息受層級的限制,訊息層級可以通過printk設定,如:
printk(" 《n》something"); /* 其中0<=n<=7 */
假設控制台的訊息層級為m, 當n
這樣一方面可以提高要列印訊息本身的層級(數字越小層級越高),
另一方面可以改變控制台的訊息層級(可從1到8),如改為8可用以下命令:
# echo "8" > /proc/sys/kernel/printk