linux内核模块

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模块

导出符号表的使用实例:后面会讲到内核中的函数接口,函数接口通过导出符号表提供给我们的,我们不用实现这个函数,就可以直接用这个函数

对比应用程序和内核模块的区别?

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值