linux内核模块
1.内核模块三要素
入口、出口、许可证
入口:安装驱动的时候执行(insmod),资源申请
出口:卸载驱动的时候执行(rmmod),资源释放
(在内核模块中申请的资源,不手动释放是不会自动释放的,除非重启)
许可证:编写内核模块要遵循GPL协议。
linux内核是开源的,所以在内核中写的驱动也必须都是开源的
GPL是GNU Public License的缩写
GNU是一个开源组织,理查德斯托曼建立的,例如notepad++
李纳斯托瓦斯linus 在编写内核的时候,使用到了GUN组织内开发的一个自由软件,相当于加入了GNU组织,这个组织写的所有代码全都是免费的
2.内核模块的编写
2-1.新建一个work目录 mkdir work
2-2.在此目录下创建文件夹以及demo.c
#include <linux/init.h>
#include <linux/module.h>
// 入口
// static:限定作用域,只能在当前文件被使用
// int:当前函数的返回值类型
// __init:告诉编译器将驱动的入口函数放在.init.text段中
// #define __init __section(".init.text")
// demo_init:驱动入口函数的名字,例如led_init,adc_init,uart_init
// (void):当前函数没有参数
static int __init demo_init(void)
{
return 0;
}
// 出口
// static:限定作用域,只能在当前文件被使用
// void:没有返回值
// __exit:告诉编译器将驱动的出口函数放在.exit.text段中
// #define __exit __section(".exit.text")
// demo_exit:驱动出口函数的名字,例如led_exit,adc_exit,uart_exit
// (void):当前函数没有参数
static void __exit demo_exit(void)
{
}
module_init(demo_init); //告诉内核入口函数的地址
module_exit(demo_exit); //告诉内核出口函数的地址
MODULE_LICENSE("GPL"); // 许可证
2-3 设置头文件查找路径
2.3.1 添加 头文件路径 的 配置文件
应用层中c代码的头文件在ubuntu的user的include中找的。
底层头文件找不到的原因是:他会在user的include中查找 ,发现找不到。
所以要添加配置。配置文件如下:
c_cpp_properties.json文件。
配置文件的查找方法:
添加成功结果如下:.json会在.vscode目录下
这时我们把自己写好的.json文件拖进去,基本可以满足使用。
我们自己写的.json文件的路径是/home/linux/linux-5.10.61/xxxxxxx
这时我们要在/home/linux 这个路径下创建内核源码的软链接 才能找到头文件位置
如果找不到内核源码的路径怎么办?
find -name linux-5.10.61
2.3.2 创建内核源码的软链接
将内核源代码创建一个软连接到用户主目录下(驱动开发没有man这样的功能,所以我们需要手动搜索,使用vscode的搜索功能)
并且
我们要在/home/linux 这个路径下创建内核源码的软链接 才能找到头文件位置
ln -s 自己的内核路径 软链接的路径
ln -s /home/linux/kernel/fsmp1a-linux-5.10.61/linux-5.10.61 /home/linux/linux-5.10.61
2.3.3分析配置文件
指定完成后整个文件就没有错误警告了。
在公司的开发中如果用的不是linux-5.10.61这个版本,要尝试更新defines 和includePath 。
即 把公司使用的内核目录下的.config 文件的所有内容 复制到defines下,且把=m或者=y全部改为=1即可。
更新 includePath 则需要修改文件路径(/home/linux/linux-5.10.61),后边部分基本不用改,因为内核的组织结构都是一样的。
配置内核模块一共需要两步:创建软链接、把修改好的.json文件放在指定位置。
2-4 分析__init
2-4-1 如何使用vscode的搜索功能查找__init的头文件
2-4-2 内核的链接脚本 — vmlinux.lds
查找内核中(/linux-5.10.61)链接脚本的命令:find -name vmlinux.lds
linux@ubuntu:~/linux-5.10.61$ find -name vmlinux.lds
./arch/arm/kernel/vmlinux.lds
./arch/arm/boot/compressed/vmlinux.lds
./arch/h8300/boot/compressed/vmlinux.lds
vmlinux --(经过objcopy转换生成了)–> Image --(经过几z的压缩生成了)–>zImage --(加上64z的数据头生成了)–>uImage
2-4-3 __init分析
通过分析内核的链接脚本文件得知
#define __init __section(“.init.text”)
__init:告诉编译器将驱动的入口函数放在.init.text段中
如果不加__init也可以,但问题是他可以被放在任意位置,每一个驱动都不在同一个位置,所以驱动启动的时候效率会低。最好加上__init 。
面试题:内核的链接脚本是干什么的?被用了几次?
编译时:
告诉编译器怎么去编译文件,怎么把这个文件最终放到uImage中,告诉编译最终存放uImage的位置。
在内核启动时:
根据对应的链接脚本来uImage中解析到对应的代码来执行。
所以会被用到两次。
2-5 指定搜索成员个数
设置–>设置–>search max–>修改最大成员个数
3.内核模块的编译
3-1.内部编译
在内核源码目录下编译就是内部编译(适合产品节点)
1-将demo.c拷贝到内核源代码目录下
cp ~/work/day1/01demo/demo.c ~/linux-5.10.61/drivers/char
2-在Kconfig中添加选项菜单
config HQYJ_DEMO
tristate "this is first driver code"
3-执行make menuconfig进行选配
<M> this is first driver code
选配的选项被保存在了.config文件中
CONFIG_HQYJ_DEMO=m
4-修改Makefile
obj -$(CONFIG_HQYJ_DEMO) +=demo.o
5-编译模块
make modules #编译生成demo.ko模块
make uImage LOADADDR=0xc2000000 #编译内核生成uImage
make dtbs #编译生成设备树文件
3-2.外部编译
在内核源码目录之外编译就是外部编译(适合开发阶段)
就是自己写一个makefile文件去编译它。
3-2-1makefile文件编写
KERNELDIR:= /home/linux/linux-5.10.61
#在Makefile定义一个KERNELDIR的变量,赋值内核的路径
PWD := $(shell pwd)
#Makefile中的变量,在Makefile中起一个shell终端,执行pwd命令,将结果赋值给PWD变量
all: @#makefile里的标签--当make后边不写标签时默认执行第一个标签
make -C $(KERNELDIR) M=$(PWD) modules
@#make -C $(KERNELDIR)切换路径到内核顶层目录下,对着内核顶层目录执行make
@#在内核执行make modules,表示要进行模块化编译,将内核中配置为m编译生成xxx.ko
@#M=$(PWD):M是Makefile中的一个变量,表示编译模块的路径,是当前目录
clean:
make -C $(KERNELDIR) M=$(PWD) clean
@#make -C $(KERNELDIR)切换路径到内核顶层目录下,对着内核顶层目录执行make
@#在内核执行make clean,清除编译生成的中间文件
@#M=$(PWD):M是Makefile中的一个变量,表示清除当前目录的文件
obj-m:= demo.o #编译编译的模块是demo
以上写的makefile文件只能在开发板ARM编译,因为KERNELDIR:= /home/linux/linux-5.10.61 我们指定的这个路径是开发板的路径。
3-2-2修改makefile文件使也可以在ubuntu中编译
如果我们指定ubuntu的内核路径,那编译的模块就可以在ubuntu安装。
ubuntu内核路径在 cd /lib/modules—>记住这个路径
查询当前用的内核版本: uname -r
linux@ubuntu:/lib/modules/5.4.0-150-generic/build目录就是内核路径
只要指定这个路径,那编译出来的模块就可以在ubuntu上执行
linux@ubuntu:~/linux-5.10.61$ cd /lib/modules
linux@ubuntu:/lib/modules$ ls //查看内核版本
4.15.0-213-generic 5.4.0-146-generic 5.4.0-148-generic 5.4.0-42-generic
5.4.0-144-generic 5.4.0-147-generic 5.4.0-150-generic 5.4.0-52-generic
linux@ubuntu:/lib/modules$ uname -r //查询当前用的内核版本
5.4.0-150-generic
linux@ubuntu:/lib/modules$ cd 5.4.0-150-generic //进入内核
linux@ubuntu:/lib/modules/5.4.0-150-generic$ ls
build modules.alias.bin modules.dep modules.softdep
initrd modules.builtin modules.dep.bin modules.symbols
kernel modules.builtin.bin modules.devname modules.symbols.bin
modules.alias modules.builtin.modinfo modules.order vdso
linux@ubuntu:/lib/modules/5.4.0-150-generic$ cd bulid
linux@ubuntu:/lib/modules/5.4.0-150-generic/build$
3-2-3通用性makefile
通过传参的方式 使makefile通用ARM和X86 以及模块名的通用
arch ?= arm
modname ?= demo
ifeq ($(arch),arm)
KERNELDIR:= /home/linux/linux-5.10.61
else
KERNELDIR := /lib/modules/$(shell uname -r)/build/
endif
#在Makefile定义一个KERNELDIR的变量,赋值内核的路径
PWD := $(shell pwd)
#Makefile中的变量,在Makefile中期一个shell终端,执行pwd命令,将结果赋值给PWD变量
all:
make -C $(KERNELDIR) M=$(PWD) modules
@#make -C $(KERNELDIR)切换路径到内核顶层目录下,对着内核顶层目录执行make
@#在内核执行make modules,表示要进行模块化编译,将内核中配置为m编译生成xxx.ko
@#M=$(PWD):M是Makefile中的一个变量,表示编译模块的路径,是当前目录
clean:
make -C $(KERNELDIR) M=$(PWD) clean
@#make -C $(KERNELDIR)切换路径到内核顶层目录下,对着内核顶层目录执行make
@#在内核执行make clean,清除编译生成的中间文件
@#M=$(PWD):M是Makefile中的一个变量,表示清除当前目录的文件
obj-m:= $(modname).o #编译编译的模块是demo
编译ARM格式:make arch=arm modname=demo
编译X86格式 : make arch=x86 modname=demo
makefile一定要回写!!!
4模块操作相关命令
安装命令:
sudo insmode xxx.ko
卸载命令:
sudo rmmod xxx
查看命令:
lsmod
5.内核模块中的printk使用
在内核目录下搜索
linux@ubuntu:~/linux-5.10.61$ grep "printk" * -nR
随便查看一个文件,查看printk的用法
printk(KERN_ERR "%s: unexpected interrupt, "
"status=0x%02x, count=%ld\n",
hwif->name, stat, count);
printk语法格式
printk(内核打印级别 "控制格式",变量);
或
printk("控制格式",变量);
printk打印级别
ctrl点击:KERN_ERR
可以看到
#define KERN_EMERG KERN_SOH "0" /* system is unusable */---系统调用
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */--立即处理
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
#define KERN_ERR KERN_SOH "3" /* error conditions */---错误
#define KERN_WARNING KERN_SOH "4" /* warning conditions */警告
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */提示
#define KERN_INFO KERN_SOH "6" /* informational */正常打印信息
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */
printk打印语句的打印级别一个有8种类,数值越小打印级别越高,
打印级别可以用于过滤打印信息。
过滤信息的使用方法
只有当消息的级别高于终端级别的时候消息才会在终端上显示
cat /proc/sys/kernel/printk
4 4 1 7
4:终端级别
4:默认消息级别
1:终端的最大级别
7:终端的最小级别
printk使用实例
#include <linux/init.h>
#include <linux/module.h>
static int __init demo_init(void)
{
// 入口函数中的打印语句
printk(KERN_ERR "this is test first driver demo...\n");
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
static void __exit demo_exit(void)
{
// 出口函数中的打印语句
printk(KERN_ERR "good bye first driver demo...\n");
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
现象说明:
对于ubuntu的终端不管任何级别的消息都不会主动回显,可以使用虚拟终端验证
上述的实例,进入和退出虚拟终端的方法如下:
进入虚拟终端:fn+ctrl+alt+[F2~F6]
退出虚拟终端:fn+ctrl+alt+F1
注:修改默认消息的级别
su root
echo 4 3 1 7 > /proc/sys/kernel/printk
printk打印信息查看命令
ubuntu中把打印的信息做了修改之后给了一条命令dmesg 查看
把内核从启动到目前的打印信息全部展示出来。
[ 2453.154746] demo: loading out-of-tree module taints kernel.
[ 2453.155104] demo: module verification failed: signature and/or required key missing - tainting kernel
[ 2453.156431] this is test first driver demo...
[ 2453.156438] /home/linux/work/day1/02printk/demo.c:mycdev_init:8
[ 2483.900519] good bye first driver demo...
[ 2483.900526] /home/linux/work/day1/02printk/demo.c:mycdev_exit:15
前边的数字是时间—秒数
dmesg :打开内核打印信息(红色级别高于终端,白色级别低于终端)
dmesg --level=err,warn:只查看err,warn级别的信息
sudo dmesg -C/-c :清除打印信息 大写的C是直接清空 小写的c是先回显再清空
复习:在系统移植的时候 添加了一句话
cd rootfs
cd /etc/init.d
rcS 文件
第十行
rcS是系统启动之后第一个脚本文件
10 echo 4 3 1 7 >/proc/sys/kernel/printk
//修改消息级别为3
在工作开发的过程中printk是什么 打印级别 就写什么级别,不能偷懒!!!
6.内核模块传参
在应用程序中可以通过argc 和argv传递参数、
在底层是用API传参数的
内核模块传参的API
char类型对应的byte一次对应一个字节
module_param(name, type, perm)
功能:接收命令传递的参数
参数:
@name:变量名
@type:变量类型
/* Standard types are:
* byte, hexint, short, ushort, int, uint, long, ulong
* charp: a character pointer
* bool: a bool, values 0/1, y/n, Y/N.
* invbool: the above, only sense-reversed (N = true).
*/
@perm:变量的权限
返回值:无
MODULE_PARM_DESC(_parm, desc)
功能:对传参变量进行描述,可以通过modinfo命令查看描述
参数:
@_parm:变量名
@desc:描述的字符串
内核模块传参的实例
#include <linux/init.h>
#include <linux/module.h>
int a = 10;
module_param(a,int,0664);
MODULE_PARM_DESC(a,"this is int type var");
static int __init demo_init(void)
{
printk("init:a = %d\n",a);
return 0;
}
static void __exit demo_exit(void)
{
printk("exit: a = %d\n",a);
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
内核模块传参测试
1.先查一下可传参的变量名和变量类型
如上图可知 可传参的变量名为a 类型为byte
变量名为p 类型为为charp
2.安装驱动的时候传参–传参方式1
sudo insmod demo.ko a=123
查看传参的信息dmesg查询
3.通过属性文件传参 前提:模块安装了----传参方式2
当安装模块的时候会在**/sys/module/驱动模块名/parameters**目录下就有模块里面变量命名的文件 文件的权限是我们刚刚指定的权限
/sys/module/驱动模块名/parameters/a
su root
echo 255 > a #修改a变量的值
cat a #查看a变量的值
学习传参以后有什么用?
假设有屏幕驱动 只给你 .ko的文件 不给你.c 文件
比如背光灯的亮度
backlight.c文件
#include <linux/init.h>
#include <linux/module.h>
int backlight=127;
module_param(backlight,int,0664);
MODULE_PARM_DESC(backlight,"this is backlight var,range:[0-255],default 127");
// 入口
static int __init demo_init(void)
{
printk("init:backlight = %d\n",backlight);
return 0;
}
// 出口
static void __exit demo_exit(void)
{
printk("exit:backlight = %d\n",backlight);
}
module_init(demo_init); //告诉内核入口地址
module_exit(demo_exit); //告诉内核出口
MODULE_LICENSE("GPL");// 许可证
通过 modinfo backlight.ko 命令
就可以知道 .ko文件中有哪些可传参的变量
此时 sudo insmod backlight.ko backlight=230 即可安装
驱动模块叫 backlight
linux@ubuntu:~/work/day2/01backlight$ sudo insmod backlight.ko backlight=230
dmesg查看
[10153.962797] init:backlight = 230
假设说在驱动安装的状态下还要修改它的值要
cd /sys/module 目录下
cd 自己的驱动模块名
cd parameters
ls -l
cat backlight
su root
echo 255 > backlight
exit
cat backlight
cd /sys/module/backlight/parameters
linux@ubuntu:/sys/module/backlight/parameters$ cat backlight
230
linux@ubuntu:/sys/module/backlight/parameters$ su root
密码:
root@ubuntu:/sys/module/backlight/parameters# echo 255 > backlight
root@ubuntu:/sys/module/backlight/parameters# exit
exit
linux@ubuntu:/sys/module/backlight/parameters$ cat backlight
255
注意:如果文件的权限给0444 说明它只有读的权限 不能修改
内核模块传参练习
通过内核模块传参给char ch变量传参;
通过内核模块传参给char *p变量传参;
#include <linux/init.h>
#include <linux/module.h>
char ch='a';
module_param(ch,byte,0664);
MODULE_PARM_DESC(ch,"value rang[a~z] default a");
char *p="hello world";
module_param(p,charp,0664);
MODULE_PARM_DESC(p,"this is a character pointer");
// 入口
static int __init demo_init(void)
{
printk("init:a = %c\n",ch);
printk("init:p = %s\n",p);
return 0;
}
// 出口
static void __exit demo_exit(void)
{
printk("exit:a = %c\n",ch);
printk("exit:p = %s\n",p);
}
module_init(demo_init); //告诉内核入口地址
module_exit(demo_exit); //告诉内核出口
MODULE_LICENSE("GPL");// 许可证
注:
1.byte类型传递的时候不能够传参字符,只能够传递一个字节的整数
例如:
2.charp类型传递的时候字符串间不能有空格
例如:
3.属性文件的最大权限是0664
例子:
4.模块安装的命令如下:
sudo insmod demo.ko ch=65 p=hello_DC23041_everyone!
查看结果:
查看一下*.ko的信息
安装模块、查看结果
7.linux内核导出符号表
引入
假设有两个进程 A和B ,假如A进程有一个add函数,B进程没有add函数,问B进程能不能直接调用这个add函数?不能直接调用。A B 进程相互独立。即使B进程拿到A进程add函数的地址也不行,因为是A的地址拿到B上不一定是add函数了。因为各自运行在自己的0-3G的内存中。
但是可以间接调用。使用共享内存实现,如图:
假如说变成A模块和B模块是可以的。因为他们运行在同一个3-4G的内核中。进程中0-3G有多份,3-4G只有一份。
什么是导出符号表
因为内核模块都是运行在3-4G的内核空间中,假如demoA模块中有一个add函数,需要将add函数的符号表导出,在编译demoB模块的时候使用这个符号表,此时在运行demoB模块的时候就可以调用demoA模块中的add函数。(符号表可以理解为A模块的地址)
导出符号表的API
EXPORT_SYMBOL_GPL(sym)
功能:导出符号表
参数:
@sym:被导出函数的名字
返回值:无
!!!扩展
do{}while() 这种宏没有返回值
例如:
do{
perror(errmsg);
printf(“%s:%s:%d\n”,_FILE_,_func_,_LINE_);
return -1; //这个-1 并不是返回值 -1不能返回 语句没有返回值
} while(0)({ }) 这样的宏是有返回值的
#define MAX(a,b) ({int max;if(a >b) max=a;else max=b;max;})
多条语句最后一句话的结果就是返回值的结果共同点:这两种写法都可以有多条语句
不同点:一个有返回值 一个没有返回值
这两种写法在底层中常用
导出符号表的实例
demoA.c
#include <linux/init.h>
#include <linux/module.h>
int add(int a,int b)
{
return (a+b);
}
EXPORT_SYMBOL_GPL(add); //导出符号表
static int __init demoA_init(void)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
static void __exit demoA_exit(void)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
}
module_init(demoA_init);
module_exit(demoA_exit);
MODULE_LICENSE("GPL");
demoB.c
#include <linux/init.h>
#include <linux/module.h>
extern int add(int a,int b);
static int __init demoB_init(void)
{
printk("sum = %d\n",add(100,200));
return 0;
}
static void __exit demoB_exit(void)
{
}
module_init(demoB_init);
module_exit(demoB_exit);
MODULE_LICENSE("GPL");
导出符号表的模块编译
先编译demoA模块,会生成符号表文件Module.symvers
0x72f367e8 add /home/linux/work/day1/04demo_export/demoA/demoA EXPORT_SYMBOL_GPL
在编译demoB模块钱需要将这个符号表文件拷贝到demoB目录下
编译才能通过,否则会提示add undefined!
在5.10(新)内核版本中,符号表不支持拷贝的方式,可以在demoB模块的Makefile中
指定demoA模块下的符号表路径。
KBUILD_EXTRA_SYMBOLS += /home/linux/work/day1/04demo_export/demoA/Module.symvers
导出符号表的模块安装
先安装demoA模块,在安装demoB模块,因为demoB模块依赖demoA模块
导出符号表的模块卸载
先卸载demoB模块,在卸载demoA模块
导出符号表的使用实例:后面会讲到内核中的函数接口,函数接口通过导出符号表提供给我们的,我们不用实现这个函数,就可以直接用这个函数