基于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