文章目录
- 一、概念
- (一)驱动种类及所在层次
- 1. 字符设备驱动
- 2. 块设备驱动
- 3. 网络设备驱动
- 二、Linux内核模块
- (一)linux内核模块三要素
- 1. 入口:
- 2. 出口:
- 3. 许可证:
- (二)linux内核模块编写
- 1. linux内核模块
- 2. 链接脚本文件
- 3. 添加vscode配置文件
- (三)内核模块编译
- 1. 内部编译方法
- 2. 外部编译方式
- ② Makefile文件
- (四)模块操作相关命令
- 1. 安装模块 insmod
- 2. 查看模块 lsmod
- 3. 卸载模块 rmmod
- 4. 问题总结
- 三、printk打印语句
- 1. 内核中的打印级别
- 2. 通过级别过滤打印信息
- 3. 修改默认消息级别
- 4. 测试消息级别
- ① 原始终端查看
- ② dmesg命令
- 5. 打印信息的宏定义
- pr_emerg
- pr_alert
- pr_crit
- pr_err
- pr_warn
- pr_notice
- pr_info
- pr_cont
- 四、内核模块传参
- (一)内核模块传参的接口
- 1. module_param
- 2. MOSULE_PARM_DESC
- (二)传参示例
- 1. 安装模块传参
- 2. 通过属性文件传参(需要模块在已安装的情况下使用)
- 五、模块导出符号表
- (一)什么是到处符号表
- (二)导出符号表的API
- (三)使用示例
- (四)总结
- 1.代码
- 2. 编译
- 2. 安装
- 3. 卸载
一、概念
(一)驱动种类及所在层次

1. 字符设备驱动
按照 字节流 来访问的,只能 顺序 不能无序访问的设备
LED、鼠标、键盘、TS屏
2. 块设备驱动
按照 block(512字节) 来访问,可以 顺序或无序 访问
eMMC、U盘、NAND Flash
3. 网络设备驱动
借助网络协议栈,负责网络数据收发工作的代码
- 注:
- 完成的是数据链路和物理层的工作
- 没有设备节点
RTL8211网卡、DM9000、CS8900
二、Linux内核模块
(一)linux内核模块三要素
1. 入口:
安装驱动时执行的函数,在入口分配资源
2. 出口:
卸载驱动时执行的函数,在出口做资源释放工作
3. 许可证:
linux内核是开源的,所以基于linux内核编写的驱动也必须开源,要遵从GPL开源协议
GNU(组织):gnu is not unix
GPL(开源协议):General Public License
(二)linux内核模块编写
1. linux内核模块
#include <linux/init.h>
#include <linux/module.h>//static:限定作用域
//__init:注意此处是两个下划线,他是linux内核中的一个宏,
// 这个宏的作用是告诉编译器将入口函数放在.init.text
// #define __init __section(".init.text")
static int __init demo_init(void){return 0;
}static void __exit demo_exit(void){
}module_init(demo_init); //告知内核入口
module_exit(demo_exit); //告知内核出口
MODULE_LICENSE("GPL"); //许可证
- 注:
- 内核中错误码是负数,无错误返回0
- 入口函数有返回值,出口函数没有返回值

2. 链接脚本文件
内核开发的链接脚本文件在Linux内核中通常被称为vmlinux.lds.S(或类似的名称,具体取决于架构)。这个文件是一个汇编文件,用于指导链接器如何将编译后的目标文件(.o文件)和库文件链接成最终的内核映像(vmlinux)
链接脚本:告诉编译器该代码存放在指定的位置
链接脚本文件vmlinux.lds.S通常位于内核源代码树的arch/xxx/kernel/目录下,其中xxx代表CPU的架构
3. 添加vscode配置文件
① 为linux内核路径创建软链接
ln -s /home/linux/kernel/fsmp1a-linux-5.10.61/linux-5.10.61 ~/linux-5.10.61
这是因为c_cpp_properties.json文件中配置内核的路径在家目录下,因此此处在家目录下创建一个内核源码的软链接
② 新建.vscode目录
将c_cpp_properties.json放到.vscode目录下即可
(三)内核模块编译
1. 内部编译方法
Kconfig文件:生成选项菜单的文件
make uImage LOADADDR=0xc2000000 => 将驱动编译到uImage
make modules => 将驱动编译成独立模块,生成一个.ko文件
2. 外部编译方式
将内核模块放在内核源代码目录外进行编译,需要编写Makefile文件
uname -r 查看内核版本

/lib/modules/5.15.0-116-generic/build ubuntu的源代码路径
② Makefile文件
arch?=arm
modname ?= demo
#注意变量后面同一行不要加注释
ifeq ($(arch),arm)KERNELDIR := /home/linux/linux-5.10.61
elseKERNELDIR := /lib/modules/$(shell uname -r)/build/
endifall:make -C $(KERNELDIR) M=$(PWD) modulesclean:make -C $(KERNELDIR) M=$(PWD) cleanobj-m:=$(modname).o
使用开发板的linux源码目录的路径下编译的.ko文件

使用ubuntu源码目录下的Makefile文件编译的.ko文件

(四)模块操作相关命令
1. 安装模块 insmod
sudo insmod hello.ko

2. 查看模块 lsmod
lsomd

3. 卸载模块 rmmod
sudo rmmod demo

- 注:卸载模块,不要加.ko
4. 问题总结
安装时不加sudo

卸载时不加sudo

三、printk打印语句
语法格式:printk(打印级别 “控制格式”,变量)参数:如果不输入打印级别,会采用默认消息级别备注:打印级别和后面中间用 **空格** 分隔
printf是用户空间的;printk是内核空间的。
printfk也有缓冲区,行缓冲区。
1. 内核中的打印级别
用于过滤打印信息的,共分为8种打印级别,分别是0-7。
#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 */
数字越小优先级越高,越大优先级越低
2. 通过级别过滤打印信息
只有当消息级别高于终端级别消息才会在终端上显示
cat /proc/sys/kernel/printk

终端消息级别 | 默认消息级别 | 终端最高消息级别 | 终端最低消息级别
3. 修改默认消息级别
需要先进入root模式才能修改
echo 4 3 1 7 > /proc/sys/kernel/printk


4. 测试消息级别
① 原始终端查看
ctrl+Fn+Alt+F2~F6
Ctrl + Fn +Alt + F1
② dmesg命令
dmesg命令
-c:先显示再清除所有打印信息
-C:清除打印信息
- 注:加上这两个参数需要加sudo权限
红色是高于终端级别,白色是等于或者小于终端级别
5. 打印信息的宏定义
在printk基础上又封装了一层宏函数
pr_emerg
pr_alert
pr_crit
pr_err
pr_warn
pr_notice
pr_info
pr_cont

四、内核模块传参
(一)内核模块传参的接口
1. module_param
module_param(name,type,perm)
功能:
参数:@name:变量名@type:byte, hexint, short, ushort, int, uint, long, ulongcharp: a character pointerbool: a bool, values 0/1, y/n, Y/N.invbool: the above, only sense-reversed (N = true).@perm:权限
- 注:参数perm传参时要注意进制问题,八进制前面必须加0
2. MOSULE_PARM_DESC
MOSULE_PARM_DESC(_parm,desc)
功能:对传参的变量进行描述
参数:@_parm 参数名@desc 参数描述信息,使用双引号引起来
用户可以通过使用 modinfo 模块.ko 命令打印相关信息

(二)传参示例
有以下模块源码:
#include <linux/init.h>
#include <linux/module.h>int a=123;
char ch = 'A';
char *p = "hello";module_param(a,int,0664); //int整型
module_param(ch,byte,0664); //字符类型
module_param(p,charp,0664); //字符指针类型MODULE_PARM_DESC(a, "This is a int var");
MODULE_PARM_DESC(ch, "This is a char var");
MODULE_PARM_DESC(p, "This is a string var");static int __init demo_init(void){pr_info("%s:a=%d\n",__func__,a);pr_info("%s:ch=%c\n",__func__,ch);pr_info("%s:p=%s\n",__func__,p);return 0;
}static void __exit demo_exit(void){pr_info("%s:a=%d\n",__func__,a);pr_info("%s:ch=%c\n",__func__,ch);pr_info("%s:p=%s\n",__func__,p);
}module_init(demo_init); //告知内核入口
module_exit(demo_exit); //告知内核出口
MODULE_LICENSE("GPL"); //许可证
1. 安装模块传参
在安装模块时,通过命令行模式传参

- 注:
- byte类型只能传递整数,不能传递字符
- charp类型传递字符串时,字符串之间不能有空格,用双引号也不行
- 属性文件最大权限是 0664 (rw, rw, r)
2. 通过属性文件传参(需要模块在已安装的情况下使用)
模块安装后,在 /sys/module/模块名/parameters 目录下,会生成以变量名来命名的普通文件

可以切换到root命令下,对文件写入数据来改变变量的值

下图可以看到a变量的值被改变了

五、模块导出符号表
(一)什么是到处符号表
在linux内核种所有模块都是运行在同一个3-4G内存空间,如果一个模块实现了某个函数,另一个模块知道这个函数的地址,就可以调用这个函数执行。模块将函数地址告诉其他模块的过程就是导出符号表
(二)导出符号表的API
EXPORT_SYMBOL_GPL(sym)
功能:导出符号表
参数:@sym:被导出的函数名
如果不加这个接口,Modules.symvers文件会是一个空文件

加入这个接口后,会生成符号表信息

(三)使用示例
以在demoA模块中定义函数,在demoB模块中使用函数
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)
{return 0;
}
static void __exit demoA_exit(void)
{
}
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 ,int );
static int __init demoB_init(void)
{printk("100+200=%d\n",add(100,200));return 0;
}
static void __exit demoB_exit(void)
{
}
module_init(demoB_init);
module_exit(demoB_exit);
MODULE_LICENSE("GPL");
(四)总结
1.代码
在demoA中定义add函数,然后使用EXPORT_SYMBOL_GPL导出符号表;
在demoB中使用函数,使用extern声明add函数是在外部定义的。
在编译demoB模块前,需要在它的Makefile中指定上述符号表路径
KBUILD_EXTRA_SYMBOLS+= /home/linux/work/day1/04export/demoA/Module.symvers
2. 编译
先编译demoA模块,会生成符号表文件Modules.symvers
再编译demoB模块,demoB才能编译成功,否则会出现add函数undefined!错误

2. 安装
先安装demoA模块,在安装demoB模块
可以看到demoB可以使用add函数

3. 卸载
先卸载demoB模块,在卸载demoA模块
- 注:如果试图先卸载demoA模块,会报错,提示demoB依赖这个模块

