Linux 內核模塊編程是一個很重要的知識點。尤其是編寫底層驅動程序時,一定會涉及到它。內核模塊編程也是 Tiger 哥學習 Linux 時第一節課所接觸的知識。由此可以看出它的 important, 也可以看出其實它很 easy 。
一前言:
1. 什麼是內核模塊
1> 內核模塊是具有獨立功能的程序。它可以被單獨編譯,但是不能單獨運行,它的運行必須被鏈接到內核作爲內核的一部分在內核空間中運行。
2> 模塊編程和內核版本密切相連,因爲不同的內核版本中某些函數的函數名會有變化。因此模塊編程也可以說是內核編程。
3> 特點:
模塊本身不被編譯進內核映像,從而控制了內核的大小;
模塊一旦被加載,就和內核中的其他部分完全一樣。
2 . 用戶層編程和內核模塊編程的區別
|
應用程序 |
內核模塊程序 |
使用函數 |
libc 庫 |
內核函數 |
運行空間 |
用戶空間 |
內核空間 |
運行權限 |
普通用戶 |
超級用戶 |
入口函數 |
main() |
module_init |
出口函數 |
exit() |
module_exit |
編譯 |
gcc |
makefile |
鏈接 |
gcc |
insmod |
運行 |
直接運行 |
insmod |
調試 |
gdb |
kdbug 、 kdb 、 kgdb |
二 . 說了這麼多,那麼怎麼編寫一個內核模塊的程序呢?
1. 我們先來看兩個最簡單的函數實例,也是幾乎所有程序員在學習一門新語言時都會編寫的程序:輸出 hello world!
現在我們分別用模塊編程輸出 hello world! ,和在用戶層編程輸出 hello wrold !。通過這兩個程序我們來分析下如何來編寫一個內核模塊程序。
用戶層編程: hello.c
#include<stdio.h>
int main(void)
{
printf("hello world/n");
}
內核編程 : module.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("Dual BSD/GPL");
static int hello_init(void)
{
printk(KERN_ALERT "hello,I am edsionte/n");
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "goodbye,kernel/n");
}
module_init(hello_init);
module_exit(hello_exit);
// 可選
MODULE_AUTHOR("Tiger-John");
MODULE_DESCRIPTION("This is a simple example!/n");
MODULE_ALIAS("A simplest example");
Tiger-John 說明:
1.> 相信只要是學過 C 語言的同學對第一個程序都是沒有問題的。但是也許大家看了第二個程序就有些不明白了。
可能有人會說: Tiger 哥你沒瘋吧,怎麼會把 printf() 這麼簡單的函數錯寫成了 printk() 呢。
也有的人突然想起當年在大學學 C 編程時,老師告訴我們“一個 C 程序必須要有 main() 函數,並且系統會首先進入 main() 函數執行 ",那麼你的程序怎麼沒有 main() 函數呢?沒有 main() 函數程序是怎麼執行的呢?
可能也會有更仔細的人會發現:怎麼兩個程序頭文件不一樣呢?不是要用到輸入和輸出函數時,一定要用到 <stdio.h> 這個頭文件,你怎麼沒有呢?
--------------------------------------------------------------------------------------------
Tiger 哥很淡定的告訴大家其實第二個程序是正確的,現在我們就來看看到底如何來編寫一個內核模塊程序。
2. 內核模塊編程的具體實現
第一步: 首先我們來看一下程序的頭文件
#include<linux/kernel.h>
#include<linux/module.h>
#include<linux/init.h>
這三個頭文件是編寫內核模塊程序所必須的 3 個頭文件 。
Tiger-John 說明:
1> 由於內核編程和用戶層編程所用的庫函數不一樣,所以它的頭文件也和我們在用戶層編寫程序時所用的頭文件也不一樣。
2> 我們在來看看在 Linux 中又是在那塊存放它們的頭文件
a. 內核頭文件的位置 : /usr/src/linux-2.6.x/include/
b. 用戶層頭文件的位置 : /usr/include/
現在我們就明白了。其實我們在編寫內核模塊程序時所用的頭文件和系統函數都和用層 編程時所用的頭文件和系統函數是 不同的。
第二步: 編寫內核模塊時必須要有的兩個函數 :
1> 加載 函數:
static int init_fun(void)
{
// 初始化代碼
}
函數實例:
static int hello_init(void)// 不加 void 在調試時會出現報警
{
printk("hello world!/n");
return 0;
}
2> 卸載函數 無返回值
static void cleaup_fun(void)
{
// 釋放代碼
}
函數實例:
static void hello_exit(void)// 不加 void 會出現報警 , 若改爲 static int 也會報錯 , 因爲出口函數是不能返會值的
{
printk("bye,bye/n");
}
在模塊編程中必須要有上面這兩個函數;
Tiger-John 補充:
註冊函數和卸載函數還有另一中寫法:
1> 模塊加載 函數
static int __init init_fun(void)
{
// 初始化代碼
}
函數實例:
static int __init hello_init(void)
{
printk("hello tiger/n");
return 0;
}
2> 卸載函數 無返回值
static void __exit cleaup_fun(void)
{
// 釋放代碼
}
函數實例:
static void __exit exit(void)
{
printk("bye bye!/n");
}
Tiger-John 補充:
通過比較我們可以發現第二中函數的寫法與第一中函數的寫法主要不同就是加了 __init 和 __exit 前綴。 (init 和 exit 前面都是兩個下劃線 )
那麼第二種方法比第一種有什麼好處呢:
_init 和 __exit 是 Linux 內核的一個宏定義,使系統在初始化完成後釋放該函數,並釋放其所佔內存。因此它的優點是顯而易見的。所以建議大家啊在編寫入口函數和出口函數時採用第二中方法。
(1) 在 linux 內核中,所有標示爲 __init 的函數在連接的時候都放在 .init.text 這個區段內,此外,所有的 __init 函數在區段 .initcall.init 中還保存了一份函數指針,在初始化時內核會通過這些函數指針調用這些 __init 函數,並在初始化完成後釋放 init 區段(包括.init.text,.initcall.init 等)。
(2) 和 __init 一樣, __exit 也可以使對應函數在運行完成後自動回收內存。
3 > 現在我們來看一下 printk() 函數
a. 上面已經說了,我們在內核 編程時所用的庫函數和在用戶態下的是不一樣的。 printk 是內核態信息打印函數,功能和比準 C 庫的printf 類似。 printk 還有信息打印級別。
b. 現在我們來看一下 printk() 函數的原型:
int printk(const char *fmt, ...)
消息打印級別:
fmt---- 消息級別:
#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>" /* 調試級別的消息 */
Tiger-John 說明:
不同級別使用不同字符串表示,數字越小,級別越高。
c. 爲什麼內核態使用 printk() 函數,而在用戶態使用 printf() 函數。
printk() 函數是直接使用了向終端寫函數 tty_write() 。而 printf() 函數是調用 write() 系統調用函數向標準輸出設備寫。所以在用戶態(如進程 0 )不能夠直接使用 printk() 函數,而在內核態由於它已是特權級,所以無需系統調用來改變特權級,因而能夠直接使用 printk()函數。 printk 是內核輸出,在終端是看不見的。我們可以看一下系統日誌。
但是我們可以使用命令: cat /var/log/messages ,或者使用 dmesg 命令看一下輸出的信息。
第三步: 加載模塊和卸載模塊
1>module_init(hello_init)
a. 告訴內核你編寫模塊程序從那裏開始執行。
b.module_init() 函數中的參數就是註冊函數的函數名。
2>module_exit(hello_exit)
a. 告訴內核你編寫模塊程序從那裏離開。
b.module_exit() 中的參數名就是卸載函數的函數名。
Tiger-John 說明:
我們一般在註冊函數裏進行一些初始化比如申請內存空間註冊設備號等 。那麼我們就要在卸載函數進行釋放我們所佔有的資源。
(1) 若模塊加載函數註冊了 XXX, 則模塊卸載函數應該註銷 XXX
(2) 若模塊加載函數動態申請了內存,則模塊卸載函數應該註銷 XXX
(3) 若模塊加載函數申請了硬件資源(中斷, DMA 通道)的佔用,則模塊卸載函數應該釋放這些硬件資源。
(4) 若模塊加載函數開啓了硬件,則卸載函數中一般要關閉硬件。
第四步 : 許可權限的聲明
1> 函數實例:
MODULE_LICENSE("Dual BSD/GPL") ;
2> 此處可有可無,可以不加系統默認 ( 但是會報警 )
模塊聲明描述內核模塊的許可權限,如果不聲明 LICENSE ,模塊被加載時,將收到內核的警告。
在 Linux2.6 內核中,可接受的 LICENSE 包括" GPL","GPL v2","GPL and additional rights","Dual BSD/GPL","Dual MPL/GPL","Proprietary" 。
第五部:模塊的聲明與描述(可加可不加)
MODULE_AUTHOR(“author”);// 作者
MODULE_DESCRIPTION(“description”);// 描述
MODULE_VERSION(”version_string“);// 版本
MODULE_DEVICE_TABLE(“table_info”);// 設備表
對於 USB , PCI 等設備驅動,通常會創建一個 MODULE_DEVICE_TABLE
MODULE_ALIAS(”alternate_name“);// 別名
Tiger-John: 總結
經過以上五步(其實只要前四步)一個完整的模塊編程就完成了。
第六步 : 常用的模塊編程命令:
1> 在 Linux 系統中,使用 lsmod 命令可以獲得系統中加載了的所有模塊以及模塊間的依賴關係
2> 也可以用 cat /proc/modules 來查看加載模塊信息
3> 內核中已加載模塊的信息也存在於 /sys/module 目錄下,加載 hello.ko 後,內核中將包含 /sys/module/hello 目錄,該目錄下又包含一個 refcnt 文件和一個 sections 目錄,在 /sys/module/hello 目錄下運行 tree -a 可以看到他們之間的關係。
4> 使用 modinfo < 模塊名 > 命令可以獲得模塊的信息,包括模塊的作者,模塊的說明,某塊所支持的參數以及 vermagic.
但是,前面我們已經說過了。內核編程和用戶層編程它們之間的編譯
鏈接也不相同。那麼我們 如何對模塊程序進行編譯,鏈接,運行呢?
現在我麼繼續深入來學習 Makefile 文件的編寫:
三 .make 的使用以及 Makefile 的編寫
1. 什麼是 Makefile , make
1>Makefile 是一種腳本,這種腳本主要是用於多文件的編譯
2> make 程序可以維護具有相互依賴性的源文件,但某些文件發生改變時,它能自動識別出,
並只對相應 文件進行自動編譯
2.Makefile 的寫法
Makefile 文件由五部分組成:顯示規則 含規則 變量定義 makefile 指示符和註釋
一條 Make 的規則原型爲:
目標 ... :依賴 ..
命令
...
…
makefile 中可以使用 Shell 命令,例如 pwd , uname
簡單的 makefile 文件:
obj-m := hello.o
kernel_path=/usr/src/linux-headers-$(shell uname -r)
all:
make -C $(kernel_path) M=$(PWD) modules
clean:
make -C $(kernel_path) M=$(PWD) clean
obj -m:= hello.o // 產生 hello 模塊的目標
kernel_path // 定義內核源文件目錄
all :
make -C $(kernel_path) M=$(PWD) modules
// 生成內核模塊參數爲內核源代碼目錄以及模塊所在目錄
clean:
make -C $(kernel_path) M=$(PWD) clean
// 清除生成的模塊文件以及中間文件
Tiger-John 說明:
1> 在 all 和 clean 下面的一行,即 make 之前必須用 Table 符隔開,不能用空 格隔開,否則編譯錯誤 。
2> 其中 -C 後指定的是 Linux 內核源代碼的目錄,而 M= 後指定的是 hello.c 和 Makefile 所在的目錄
3.Makefile 實例:
1 obj-m:=module.o
2
3
4 CURRENT_PATH :=$(shell pwd)
5 VERSION_NUM :=$(shell uname -r)
6 LINUX_PATH :=/usr/src/linux-headers-$(VERSION_NUM)
7
8 all :
9 make -C $(LINUX_PATH) M=$(CURRENT_PATH) modules
10 clean :
11 make -C $(LINUX_PATH) M=$(CURRENT_PATH) clean
----------------------------------------------------------------------
經過上面的模塊編程和 Makefile 的編程,我們就可以對我們的程序進行編譯鏈接和運行了
四 . 內核模塊的操作過程
1> 在控制檯輸入 make 進行編譯鏈接
2> 正確後在控制檯輸入 sudo insmod module.ko (加載模塊)
3> 在控制檯輸入 dmesg 查看結果
4> 在控制檯輸入 rmmod tiger( 卸載模塊 )
5> 輸入 dmesg 查看結果
( 或者用 cat /var/log/messages 查看系統日誌文件)
6>make clean( 去除中間生成的文件)
----------------------------------------------------------------------
現在我們就總體來實踐一下 , 來體驗一下。編寫內核模塊程序的樂趣
module.c
1 #include<linux/kernel.h>
2 #include<linux/init.h>
3 #include<linux/module.h>
4 MODULE_LICENSE("Dual BSD/GPL");
5
6 static int __init hello_init(void)
7 {
8 printk("Hello world/n");
9 return 0;
10 }
11
12 static void __exit hello_exit(void)
13 {
14 printk("Bye Corne/n");
15
16 }
17 module_init(hello_init);
18 module_exit(hello_exit);
Makefile
1 obj-m:=module.o
2
3
4 CURRENT_PATH :=$(shell pwd)
5 VERSION_NUM :=$(shell uname -r)
6 LINUX_PATH :=/usr/src/linux-headers-$(VERSION_NUM)
7
8 all :
9 make -C $(LINUX_PATH) M=$(CURRENT_PATH) modules
10 clean :
11 make -C $(LINUX_PATH) M=$(CURRENT_PATH) clean
在終端輸入 make
think@ubuntu:~/work/moudule/mokua_biancheng$ make
make -C /usr/src/linux-headers-2.6.32-25-generic M=/home/think/work/moudule/mokua_biancheng modules
make[1]: 正在進入目錄 `/usr/src/linux-headers-2.6.32-25-generic'
Building modules, stage 2.
MODPOST 1 modules
make[1]: 正在離開目錄 `/usr/src/linux-headers-2.6.32-25-generic'
think@ubuntu:~/work/moudule/mokua_biancheng$
think@ubuntu:~/work/moudule/mokua_biancheng$ sudo insmod module.ko
think@ubuntu:~/work/moudule/mokua_biancheng$ dmesg
[19011.002597] Hello world
Tiger-John :當程序沒有錯誤時,當我們輸入 dmesg 時就可以看到程序運行的結果了
轉載:blog.csdn.net/tigerjibo/article/details/6010997