您的位置:首页 > 教育 > 培训 > linux系统驱动(一)模块

linux系统驱动(一)模块

2025/12/30 12:22:37 来源:https://blog.csdn.net/weixin_44254079/article/details/140806174  浏览:    关键词:linux系统驱动(一)模块

文章目录

  • 一、概念
    • (一)驱动种类及所在层次
      • 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依赖这个模块
    在这里插入图片描述

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com