編寫內核模塊小Demo

基於Linux系統的內核編程小Demo.


編寫Linux內核模塊的demo及注意事項.

什麼是內核模塊呢?

首先內核是一個操作系統的最基礎部分,它是一個向所有外部程序和硬件驅動提供一個插口的這麼一個存在,然後內核模塊就是對接這個抽口的模塊。
內核又分了微內核和宏內核,宏內核又分爲單內核和雙內核。而Linux內核屬於宏內核中的單內核,它汲取了微內核的思想和精華,故而提供了模塊化機制,不僅實現了效率高,同時因爲模塊化的存在讓其更加的便於維護和擴展。

更多的內核相關信息可自行上網瞭解

驅動程序在內核中,都是獨立的模塊,例如LED驅動、蜂鳴器驅動,它們驅動之間沒有相互的聯繫,可以通過應用程序將兩個驅動聯繫在一起,例如以下的代碼,LED驅動和蜂鳴器驅動各自都是一個獨立的模塊(module)。
內核模塊編譯成功後會生成一個 (*.ko)(kernel object)文件。

當內核編寫完成後,使用以下兩個命令:

加載內核模塊

insmod *.ko

卸載內核模塊

rmmod *.ko

注意:驅動是安裝在內存中正在運行的內核上。

應用程序代碼結構和內核模塊代碼結構區別:
運行方法|C語言應用程序|內核模塊
–|:–:|–
運行空間|用戶空間|內核空間
出口|main|module_init函數指定
入口|-|module_exit函數指定
編譯|gcc|Makefile
運行|./直接運行|insmod
退出|exit|rmmod


設計一個簡單的內核demo.

默認已經下載好了Linux內核源碼並且解壓好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

//入口函數
static int __init myled_init(void)
{
    printk("myled drvier init !\n");
    return 0;
}

//出口函數
static int __exit myled_exit(void)
{
    printk("myled drvier exit !\n");
    return 0;
}

module_init(myled_init);    //驅動程序的入口:insmod myled.ko調用module_init,module_init又會去調用myled_init函數。
module_exit(myled_exit);    //驅動程序的出口:rmmod myled調用module_exit,module_exit又會去調用myled_exit函數。

//模塊描述
MODULE_AUTHOR("LYQ");               //作者信息
MODULE_DESCRIPTION("myled driver"); //模塊功能說明
MODULE_LICENSE("GPL");              //許可證:驅動遵循GPL協議

關鍵字詳解

至此一個簡單的內核模塊的代碼就完成了,當然這只是冰山一角。
代碼中__init用在初始化函數,加上這個關鍵字代表往往是隻調用一次,往後就不會再被調用了,那它的資源將會被釋放。
__exit關鍵字也一樣,修飾清除退出函數,在退出函數被調用過後,立馬釋放資源。


相關函數詳解

  • printk函數
    • 在驅動開發當中,我們不能使用printf函數,只能使用printk函數,使用方法會跟printf函數相像,但是也有點不同。
    • 具體源碼看#include <linux/kernel.h>
    • 在源碼中可見,printk函數存在優先級打印,可以查看printk優先級:
    • 我們可以通過修改printk文件來獲得需要的默認優先級輸出echo x x 1 7 > /proc/sys/kernel/printk,也可以在printk函數中添加優先級;
      1
      
      printk("<3>""led drvier init\n");
      
      也可以寫爲
      1
      
      printk(KERN_ERR"led drvier init\n");
      
  • printk函數打印優先級的相關宏定義,在<linux/printk.h>當中能夠找到
    1
    2
    3
    4
    5
    6
    7
    8
    
    #define	KERN_EMERG	"<0>"		/* system is unusable			*/
    #define	KERN_ALERT	"<1>"		/* action must be taken immediately	*/
    #define	KERN_CRIT	"<2>"		/* critical conditions			*/
    #define	KERN_ERR	"<3>"		/* error conditions			*/
    #define	KERN_WARNING	"<4>"		/* warning conditions			*/
    #define	KERN_NOTICE	"<5>"		/* normal but significant condition	*/
    #define	KERN_INFO	"<6>"		/* informational			*/
    #define	KERN_DEBUG	"<7>"		/* debug-level messages			*/
    

注意:printk有一個缺點:不支持浮點數打印

由此可見,應用程序和內核模塊的源碼的區別;


內核模塊代碼的編譯.

和應用程序不同,內核模塊一般由Makefile來進行編譯,關於Makefile的編寫,可以閱讀Documentation/kbuild/modules.txt,包含很多編譯內核模塊的操作步驟;具體如下:

1
2
3
4
5
6
7
8
9
10
obj-m += myled.o
KERNEL_DIR := /home/bbigq/6818GEC/kernel
CROSS_COMPILE := /home/bbigq/6818GEC/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8/bin/arm-eabi-
PWD := $(shell pwd)

default:
	$(MAKE) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNEL_DIR) M=$(PWD) modules

clean:
	rm  *.o *.order .*.cmd  *.mod.c *.symvers .tmp_versions -rf

Makefile詳解:

obj-m+=myled.o
make的第一階段將源程序編譯爲目標文件myled.o
make的第二階段將myled.ko編譯成一個模塊,即myled.ko。

KERNEL_DIR :=/home/bbigq/6818GEC/kernel
內核源碼路徑:查找編譯所需的頭文件、函數原型、Makefile…..

CROSS_COMPILE:=/home/bbigq/6818GEC/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8/bin/arm-eabi-
交叉編譯工具,內核使用4.8版本進行編譯,內核模塊最好也是跟內核使用相同的編譯工具

PWD:=$(shell pwd)
當前內核模塊源碼路徑

$(MAKE) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNEL_DIR) M=$(PWD) modules
使用make命令的時候,傳遞多個參數,並調用內核源碼下的Makefie文件,使用該Makefile文件中的工具,將myled.o文件編譯爲一個內核模塊myled.ko。

rm *.o *.order .*.cmd *.mod.c *.symvers .tmp_versions -rf
刪除過程文件及其他文件。

編譯完成之後,可以通過modinfo命令來查看驅動信息。
modinfo命令


擴展.

內核模塊的參數.

內核支持:bool、charp(字符串指針)、short、int、long、ushort、uint、ulong類型,這些類型可以對應於整型、數組、字符串

情景:當你需要編寫一個串口驅動時,要求波特率等信息通過命令行輸入;
led.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

static int baud = 9600;
static int port[4]={0,1,2,3};
static int port_cnt=0;
static char *name="vcom";

//通過以下宏定義來接收命令行的參數
module_param(baud,int,0644); 			//rw- r-- r--
module_param_array(port,int,&port_cnt,0644);	//rw- r-- r--
module_param(name,charp,0644);			//rw- r-- r--

//入口函數
static int __init led_init(void)
{

	printk("led init\n");
	
	printk("baud=%d\n",baud);
	printk("port=%d %d %d %d ,port_cnt=%d\n",port[0],port[1],port[2],port[3],port_cnt);
	printk("name=%s\n",name);

	return 0;
}


//出口函數
static void __exit led_exit(void)
{
	printk("led exit\n");
}

module_init(led_init);
module_exit(led_exit)


//模塊描述
MODULE_AUTHOR("LYQ");		        //作者信息
MODULE_DESCRIPTION("led driver");	//模塊功能說明
MODULE_LICENSE("GPL");                  //許可證:驅動遵循GPL協議

關鍵函數詳解

1
2
module_param(name,type,perm)
module_param_array(name,type,nump,perm)
函數名 作用
name 變量的名字
type 變量或數組元素的類型
nump 保存數組元素個數的指針,可選。默認寫NULL。
perm 在sysfs文件系統中對應的文件的權限屬性,決定哪些用戶能夠傳遞哪些參數,如果該用戶權限過低,則無法通過命令行傳遞參數給該內核模塊。

在編譯成功後,加載模塊時,可以填寫相應的參數:

1
insmod led.ko baud=115200 port=1,2,3,4 name="tcom"

執行後返回信息:

1
2
3
4
led init
baud=115200
port=1 2 3 4 ,port_cnt=4
name=tcom

加載內核模塊後,能夠在/sys/module/led_drv/parameters/目錄下看到對參數的訪問權限。

1
2
3
4
5
#ls -l /sys/module/led_drv/parameters/
total 0
-rw-r--r--    1 root     root          4096 Jan  1 02:06 baud
-rw-r--r--    1 root     root          4096 Jan  1 02:06 name
-rw-r--r--    1 root     root          4096 Jan  1 02:06 port

編譯多個內核模塊.

1
2
3
4
5
6
7
8
9
10
11
obj-m += led.o
obj-m += sum.o
KERNEL_DIR := /home/bbigq/6818GEC/kernel
CROSS_COMPILE := /home/bbigq/6818GEC/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8/bin/arm-eabi-
PWD := $(shell pwd)

default:
	$(MAKE) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNEL_DIR) M=$(PWD) modules

clean:
	rm  *.o *.order .*.cmd  *.mod.c *.symvers .tmp_versions -rf

內核符號表——全局共享函數接口與變量.

內核符號表
內核符號表是記錄了內核中所有的符號(函數、全局變量)的地址及名字,這個符號表被嵌入到內核鏡像中,使得內核可以在運行過程中隨時獲得一個符號地址對應的符號名。
故而每加載一個內核模塊後,該內核模塊中的所有的函數、全局變量等信息都會保存到內核符號表中,比如A內核模塊調用了B內核模塊中聲明的函數時,A內核模塊就會到內核符號表中查找,這個時候會用到以下兩個宏定義

1
2
EXPORT_SYMBOL(符號名):導出的符號可以給其他模塊使用。
EXPORT_SYMBOL_GPL(符號名):導出的符號只能讓符合GPL協議的模塊才能使用。

示例代碼
1:A模塊lcd.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

extern int share;                       //申明B模塊中的全局變量
extern int add_return_sum(int a, int b);//申明B模塊中的函數

//入口函數
static int __init gec6818_led_init(void)
{
	printk("<3>""gec6818 led init\n");
    printk("<3>""add_return_sum(10,11) = %d\n", add_return_sum(10, 11));
	return 0;
} 


//出口函數
static void __exit gec6818_led_exit(void)
{
	printk("<4>""gec6818 led exit\n");
}

//驅動程序的出入口
module_init(gec6818_led_init);
module_exit(gec6818_led_exit)


//模塊描述
MODULE_AUTHOR("LYQ");			//作者信息
MODULE_DESCRIPTION("study kernal first code test");//模塊功能說明
MODULE_LICENSE("GPL");			//許可證:驅動遵循GPL協議

2:B模塊sum.c文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

static int share = 100;

static int add_return_sum(int a, int b)
{
    int sum = a + b;
    return sum;
}

//導出的符號只能讓符合GPL協議的模塊才能使用。
EXPORT_SYMBOL_GPL(share);       
EXPORT_SYMBOL_GPL(add_return_sum);

//模塊描述
MODULE_AUTHOR("LYQ");			//作者信息
MODULE_DESCRIPTION("study kernal first code test");		//模塊功能說明
MODULE_LICENSE("GPL");							//許可證:驅動遵循GPL協議

3:對應的Makefile

1
2
3
4
5
6
7
8
9
10
11
obj-m += led.o
led-objs = led.o sum.o
KERNEL_DIR := /home/bbigq/6818GEC/kernel
CROSS_COMPILE := /home/bbigq/6818GEC/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8/bin/arm-eabi-
PWD := $(shell pwd)

default:
	$(MAKE) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNEL_DIR) M=$(PWD) modules

clean:
	rm  *.o *.order .*.cmd  *.mod.c *.symvers .tmp_versions -rf

此處Makefile要點詳解:
obj-m += led.o obj-m代表着最終生成的驅動文件爲led.ko
led-objs = led.o sum.o並且led.ko必須依賴兩個.o文件就是led.o和sum.o文件,因此通過模塊名加-objs的形式可以定義整個模塊所包含的文件。

坑警告!!!
1:類似於以上情況,A模塊依賴B模塊的調用,在加載模塊時,應先加載B模塊,再加載A模塊,否則會出現錯誤,類似以下:

1
2
led: Unknown symbol add_return_sum (err 0)
insmod: can't insert 'led.ko': unknown symbol in module or invalid parameter

2:如果當前模塊沒有添加許可,也會在編譯或者加載的時候出現報錯現象。


我的GITHUB

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章