基於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
命令來查看驅動信息。
內核模塊的參數.
內核支持: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