文章目录
- 一、微内核和宏内核
- 二、内核模块
- 三、内核模块code
- 四、Makefile
- 注意
- 注:+=,?=, := 区别
- 五、prink
- 查看prink打印优先级
- 修改printk的打印优先级
- 六、内核模块参数
- module_param与module_param_array宏定义的使用
- 七、内核符号表--全局共享函数接口与变量
- 1.内核符号表
- 2.用到的宏定义EXPORT_SYMBOL、EXPORT_SYMBOL_GPL
- 八、多个源文件组合为内核模块
一、微内核和宏内核
微内核:内核中只有最基本的调度、内存管理。驱动、文件系统等都是用户态的守护进程去实现的。优点是超级稳定,驱动等的错误只会导致相应进程死掉,不会导致整个系统都崩溃,做驱动开发时,发现错误,只需要kill掉进程,修正后重启进程就行了,比较方便。缺点是效率低。
宏内核:简单来说,就是把很多东西都集成进内核,例如linux内核,除了最基本的进程、线程管理、内存管理外,文件系统,驱动,网络协议等等都在内核里面。优点是效率高。缺点是稳定性差,开发过程中的bug经常会导致整个系统挂掉。
二、内核模块
驱动程序在内核中,都是独立的模块,例如led驱动、蜂鸣器驱动,它们驱动之间没有相互的联系,可以**通过应用程序将两个驱动联系在一起。
内核模块编译成功之后,会输出*.ko(kernel object)文件。
加载内核模块到内核:insmod led_drv.ko
从内核卸载内核模块:rmmod led_drv
若查看当前的内核模块是否加载成功,使用命令: lsmod
驱动是安装在内存中正在运行的内核上
C语言应用程序 | 内核模块 | |
---|---|---|
运行空间 | 用户空间 | 内核空间 |
入口 | main | module_init函数指定 |
出口 | - | module_exit函数指定 |
编译 | gcc | Makefile |
运行 | ./直接运行 | insmod |
退出 | exit | rmmod |
三、内核模块code
参考内核源码:
\kernel\drivers\watchdog\wdt.c
#include <linux/module.h>
#include <linux/init.h>
static int __init wdt_init(void)//入口函数
{
return 0;
}
static void __exit wdt_exit(void)//出口函数
{
}
module_init(wdt_init);//驱动入口 insmod
module_exit(wdt_exit);//驱动出口 rmmod
MODULE_AUTHOR("Alan Cox");//作者信息
MODULE_DESCRIPTION("Driver for ISA ICS watchdog cards (WDT500/501)");//模块功能说明
MODULE_LICENSE("GPL");//许可证:驱动遵循GPL协议
__init
的初始化函数,往往只是被调用一次,在调用完成后,该函数不应该再被调用。所以它占用的内存必须得释放,只要该函数加上__init
关键字就能够达到这个目的。
__exit
也是用于修饰清除退出函数,和__init
的效果也是一样,被调用之后,就会释放内存。
在kernel\arch\um\include\shared\init.h文件中
#define __init __section(.init.text)
#define __exit __section(.exit.text)
这个标志符和函数声明放在一起,表示gcc编译器在编译的时候需要把这个函数放.text.init、.exit.text section中
__ection
属性专门用于确定标记了该对象的存储位置。 ELF二进制格式将目标文件排列为命名部分,使用这样的属性允许程序员更精确地指定标记目标的信息将放置在目标对象中的位置
四、Makefile
参考文档:
kernel/Documentation/kbuild/makefiles.txt
kernel/Documentation/kbuild/modules.txt
$(obj-m) specify object files which are built as loadable
kernel modules.
A module may be built from one source file or several source
files. In the case of one source file, the kbuild makefile
simply adds the file to $(obj-m).
obj-m +=led_drv.o
KERNEL_DIR :=linux/kernel
CROSS_COMPILE :=linux/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
obj-m+=led_drv.o
make的第一阶段将源程序编译为目标文件led_drv.o
make的第二阶段将led_drv.ko编译成一个模块,即led_drv.ko。
KERNEL_DIR :=linux/kernel
内核源码路径:查找编译所需的头文件、函数原型、Makefile…
CROSS_COMPILE:=linux/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文件中的工具,将led_drv.o文件编译为一个内核模块led_drv.ko。
注意
make命令对路径是很敏感的,不能有中文或空格出现
注:+=,?=, := 区别
= 是最基本的赋值
:= 是覆盖之前的值
?= 是如果没有被赋值过就赋予等号后面的值
+= 是添加等号后面的值
五、prink
\kernel\include\linux\printk.h
#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 */
查看prink打印优先级
cat /proc/sys/kernel/printk
#7 7 1 7
该printk文件总共有4个值。
7:控制台打印级别,默认的消息日志级别优先级高于该值,消息就能够被打印到控制台。
7:默认的消息日志级别,例如printk(“hello\n”);
1:最低的控制台日志级别:控制台日志级别可被设置的最小值(最高优先级)
7:缺省的控制台日志级别:控制台日志级别的缺省值
修改printk的打印优先级
echo 7 3 1 7 > /proc/sys/kernel/printk
cat /proc/sys/kernel/printk
#7 3 1 7
#include <linux/kernel.h>
printk("<3>""hello world\n");
printk(KERN_ERR"hello world\n");
六、内核模块参数
模块参数允许用户在加载模块的时候,通过命令行指令参数值,内核支持的参数:bool、invbool(反转bool值)、charp(字符串指针)、short、int、long、ushort、uint、ulong类型,这些类型可以对应于整型、数组、字符串。
module_param与module_param_array宏定义的使用
module_param(name,type,perm)
module_param_array(name,type,nump,perm)
name:变量的名字
type:变量或数组元素的类型
nump:保存数组元素个数的指针,可选。默认写NULL。
perm:在sysfs文件系统中对应的文件的权限属性,决定哪些用户能够传递哪些参数,如果该用户权限过低,则无法通过命令行传递参数给该内核模块。
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
static int baud = 9600; //baud变量
static int port[4]={0,1,2,3}; //port数组
static int port_cnt=0; //记录module_param_array传递数组的长度
static char *name="vcom"; //name变量
//通过以下宏定义来接收命令行的参数
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 myled_init(void)
{
printk("myled 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 myled_exit(void)
{
printk("myled exit\n");
}
module_init(myled_init);//驱动程序的入口:insmod led_drv.ko
module_exit(myled_exit);//驱动程序的出口:rmmod led_drv
MODULE_LICENSE("GPL"); //许可证:驱动遵循GPL协议
make编译模块
obj-m +=led_drv.o
KERNEL_DIR :=linux/kernel
CROSS_COMPILE :=linux/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
$(MAKE) clean
clean:
rm *.o *.order .*.cmd *.mod.c *.symvers .tmp_versions -rf
加载模块
insmod led_drv.ko baud=115200 port=1,2,3,4 name="tcom"
查看参数
ls -l /sys/module/led_drv/parameters/
#total 0
#-rw-r--r-- 1 root root 4096 Dec 1 02:06 baud
#-rw-r--r-- 1 root root 4096 Dec 1 02:06 name
#-rw-r--r-- 1 root root 4096 Dec 1 02:06 port
七、内核符号表–全局共享函数接口与变量
1.内核符号表
内核符号表是记录了内核中所有的符号(函数、全局变量)的地址及名字,这个符号表被嵌入到内核镜像中,使得内核可以在运行过程中随时获得一个符号地址对应的符号名。
注:
符号:指的是函数、全局变量的地址及名字。
2.用到的宏定义EXPORT_SYMBOL、EXPORT_SYMBOL_GPL
EXPORT_SYMBOL(符号名):导出的符号可以给其他模块使用。
EXPORT_SYMBOL_GPL(符号名):导出的符号只能让符合GPL协议的模块才能使用。
sum.c
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
static unsigned int g_count=0x100000;
static int myadd(int a,int b)
{
return (a+b);
}
EXPORT_SYMBOL_GPL(myadd);
EXPORT_SYMBOL_GPL(g_count);
MODULE_LICENSE("GPL"); //许可证:驱动遵循GPL协议
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
extern int myadd(int a,int b);
extern unsigned int g_count;
//入口函数
static int __init myled_init(void)
{
printk("myled init\n");
printk("myadd(10+110)=%d",myadd(10,110));
return 0;
}
//出口函数
static void __exit myled_exit(void)
{
printk("myled exit\n");
}
module_init(myled_init);//驱动程序的入口:insmod led_drv.ko
module_exit(myled_exit);//驱动程序的出口:rmmod led_drv
MODULE_LICENSE("GPL"); //许可证:驱动遵循GPL协议
加载模块
加载成功后,能够在内核符号表找到myadd函数,说明了该函数已经嵌入了内核符号表。
grep -r myadd /proc/kallsyms
#cd125000 t myadd [sum]
八、多个源文件组合为内核模块
有些时候不想通过符号表全局共享函数与全局变量,可以使用以下方法实现多个文件编译,实现局部访问。
\kernel\Documentation\kbuild\makefiles.txt
Makefile
在这个Makefile中,obj-m说明最终生成的内核模块目标文件是led_drv.ko,并且它必须依赖两个文件led.c和sum.c。因此,通过模块名加-objs的形式可以定义整个模块所包含的文件。
obj-m +=led_drv.o
led_drv-objs=led.o sum.o
KERNEL_DIR :=linux/kernel
CROSS_COMPILE :=linux/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
$(MAKE) clean
clean:
rm *.o *.order .*.cmd *.mod.c *.symvers .tmp_versions -rf