编写内核模块小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

发布了32 篇原创文章 · 获赞 1 · 访问量 227

猜你喜欢

转载自blog.csdn.net/qq_41714908/article/details/105239168