目录
现代内核派系
宏内核
微内核
搭建实验环境(野火i.mx 6ull为例)
内核模块1
内核模块头文件
内核模块加载与卸载
内核模块出入口
内核模块信息声明
打印函数printk
内核模块实验1
helloworld.c文件
Makefile文件
执行过程
内核模块2
模块参数
内核模块实验2
module_param.c文件
Makefile文件
执行过程
内核模块3
符号共享
内核模块实验3
module_param.c文件
calculation.c文件
calculation.h文件
Makefile文件
Makefile文件
执行过程
内核模块4
模块手动加载
模块自动加载
关键功能(基本功能,不可裁剪、扩展)和服务功能(如文件系统、设备驱动、网络服务等,可裁剪、扩展)均在内核空间提供。运行效率高。扩展性较差。system call(系统调用)能够先入内核态来使用内核提供的服务。
内核空间只提供关键功能,服务功能在用户空间提供。运行效率较低。安全性、扩展性较高。
在Linux内核源码中有超过50%的代码都与设备驱动相关。Linux为宏内核架构(windows、鸿蒙等为微内核架构),如果开启所有的功能,内核就会变得十分臃肿。内核模块实现了某个功能的一段内核代码,在内核运行过程,可以加载这部分代码到内核,从而动态地增加了内核的功能。基于这种特性,进行设备驱动开发时,以内核模块的形式编写设备驱动,只需要编译相关的驱动代码即可,无需对整个内核进行编译。
在设备驱动开发过程中,用户可以随意将正在测试的驱动程序添加到内核或从内核中移除,每次修改内核模块的代码不需要重新启动内核。
在开发板上,用户也不需要将内核模块程序,或设备驱动程序的ELF文件存放在开发板,免去占用不必要的存储空间。当需要加载内核模块时,可以挂在NFS服务器,将存放在其它设备的内核模块加载到开发板上。
开发板烧录好Debian镜像。启动开发板,搭建好nfs客户端,挂载共享文件夹。获取Debian镜像的内核源码并编译。(选择4.19.71版本内核,内核模块的功能需要依赖内核提供的各种底层接口。)
注:cat /etc/issue查看镜像日期。新版内核是4.19.35版本(22年之后),看驱动文档的“驱动章节实验环境搭建”。
下载linux内核源码(基于野火linux开发板,存放着内核相关)
git clone -b ebf_4.19.35_imx6ul https://github.com/Embedfire/ebf_linux_kernel.git 或
git clone https://gitee.com/Embedfire/ebf_linux_kernel_6ull_depth1
安装必要环境工具库(make工具、gcc交叉编译链、gcc编译工具、bison语法分析器、flex词法分析器、libssl-dev OpenSSL通用库、lzop LZO压缩库的压缩软件)
sudo apt install make gcc-arm-linux-gnueabihf gcc bison flex libssl-dev dpkg-dev lzop
前往ebf-buster-linux目录,执行脚本,一键编译内核
sudo ./make_deb.sh
获取编译出来的内核相关文件(make_deb.sh脚本指定存放路径)
/home/couvrir/桌面/ebf_linux_kernel_6ull_depth1/build_image/build
make_deb.sh如下:
deb_distro=bionic
DISTRO=stable
build_opts="-j 6"
build_opts="${build_opts} O=build_image/build" //O执行编译文件的存放路径
build_opts="${build_opts} ARCH=arm"
build_opts="${build_opts} KBUILD_DEBARCH=${DEBARCH}"
build_opts="${build_opts} LOCALVERSION=-imx-rl"
build_opts="${build_opts} KDEB_CHANGELOG_DIST=${deb_distro}"
build_opts="${build_opts} KDEB_PKGVERSION=l${DISTRO}"
build_opts="${build_opts} CROSS_COMPILE=arm-linux-gnueabihf-"
build_opts="${build_opts} KDEB_SOURCENAME=linux-upstream"
make ${build_opts} npi_v7+defconfig
make ${build_opts}
make ${build_opts} bindeb-pkg
为解决linux内核可扩展性和可维护性相对较差的缺陷。我们编写的内核模块,经过编译,最终形成.ko为后缀的ELF文件。ko文件是elf格式,是一种普通的可重定位目标文件。这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类。
#include /*包含module_init()和module_exit()函数的声明*/
#include /*包含内核模块信息声明的相关函数*/
#include /*包含内核提供的各种函数,如printk*/
加载内核模块:insmod
卸载内核模块:rmmod
module_init():加载模块时该函数自动执行,进行初始化操作
module_exit():卸载模块时该函数自动执行,进行清理操作
MODULE_LICENSE():表示模块代码接受的软件许可协议,Linux内核遵循GPL V2开源协议,内核模块与linux内核保持一致即可。
MODULE_AUTHOR():描述模块的作者信息。
MODULE_DESCRIPTION():对模块的简单介绍。
MODULE_ALIAS():给模块设置一个别名。
printf:glibc库实现的打印函数,工作于用户空间。
printk:内核模块无法使用glibc库函数,内核自身实现的一个类printf函数,但是需要指定打印等级。
#include
/*printk打印等级,数字越小,等级越高*/
#define KERN_EMERG "<0>" /*通常是系统崩溃前的信息*/
#define KERN_ALERT "<1>" /*需要立即处理的消息*/
#define KERN_CRIT "<2>" /*严重情况*/
#define KERN_ERR "<3>" /*错误情况*/
#define KERN_WARNING "<4>" /*有问题的情况*/
#define KERN_NOTICE "<5>" /*注意信息*/
#define KERN_INFO "<6>" /*普通消息*/
#define KERN_DEBUG "<7>" /*调试信息*/
cat /proc/sys/kernel/printk:查看当前系统printk打印等级。
终端输出:
当前控制台日志级别(小于该等级才能打印在当前控制台)
默认消息日志级别
最小的控制台级别(当前控制台日志级别的最小值)
默认控制台日志级别(没指定等级时默认级别)
dmesg:打印内核所有打印信息。
lsmod:查看当前系统加载内核情况。
#include
#include
#include
static int __init hello_init(void) //__init指将这段函数指定保存在__init段内存
{
printk(KERN_EMERG "[ KERN_EMEGR ] Hello World Module Init\n"); //指定等级
printk("[ default ] Hello World Module Init\n"); //默认等级
return 0;
}
static void __exit hello_exit(void) //__exit指将这段函数指定保存在__exit段内存
{
printk("[ default ] GoodBye\n"); //默认等级
}
module_init(hello_init); //hello_init作为模块函数入口
module_exit(hello_exit); //hello_exit作为模块函数出口
MODULE_LICENSE("GPL2"); //表示模块代码接受的软件许可协议
MODULE_AUTHOR("couvrir"); //描述模块的作者信息
MODULE_DESCRIPTION("hello world module"); //对模块的简单介绍
MODULE_ALIAS("test module"); //给模块设置一个别名
KERNEL_DIR=/home/couvrir/桌面/ebf_linux_kernel_6ull_depth1/build_image/build //将内核源码编译生成的内核文件存放在该路径
ARCH=arm //交叉编译的目标架构
CROSS_COMPILE=arm-linux-gnueabihf- //交叉编译的编译工具链
export ARCH CROSS_COMPILE //将架构和编译链导出到子Makefile,使其在当前shell环境中生效
obj-m:=helloworld.o
all: //默认目标,用于编译所有模块
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules //执行内核源码目录的Makefile文件,并将当前目录作为模块的源码目录
//通过执行Makefile文件,可以将helloworld.c文件编译成一个内核模块
.PHONY:clean copy
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean //执行内核源码目录的Makefile文件的clean目标,清理编译生成的文件
copy:
sudo cp *.ko /home/couvrir/桌面/sharedir //将ko文件存到共享文件夹
obj-m:=<模块名>.o:指定要编译的模块文件。
-C指定子Makefile的路径,M=描述内核关于内核模块的源码路径。CURDIR为当前目录路径。
make modules:执行linux顶层Makefile的伪目标,它实现内核模块的源码读取并编译为.ko文件。
虚拟机:执行make和make copy。生成.ko文件。
开发板(在挂载目录下执行):
sudo insmod helloworld.ko
dmesg:用于显示内核ring buffer(环形缓冲区)的内容,它可以显示在系统启动期间产生的内核消息,包括硬件设备探测、驱动加载、错误报告等(打印内核所有打印信息)。
lsmod:查看当前系统加载内核情况。
sudo rmmod helloworld
根据不同应用场合给内核模块传递不同参数,提高内核模块灵活性。首先定义一个常见变量,然后使用module_param宏把传参值赋给变量。
/*
name: 参数名
type: 参数类型,如int、bool、byte、charp等
perm: 读写权限
不允许设置可执行权限
在“/sys/module/模块名/parameters”目录下,会生成该参数对应的文件名(0除外)
*/
module_param(name, type, perm);
#include
#include
#include
static int itype = 0;
module_param(itype, int, 0);
static bool btype = 0;
module_param(btype, bool, 0644);
static char ctype = 0;
module_param(ctype, byte, 0);
static char *stype = 0;
module_param(stype, charp, 0644);
static int __init param_init(void)
{
printk(KERN_ALERT"param_init!\n");
printk(KERN_ALERT"itype = %d\n", itype);
printk(KERN_ALERT"btype = %d\n", btype);
printk(KERN_ALERT"ctype = %d\n", ctype);
printk(KERN_ALERT"stype = %s\n", stype);
return 0;
}
static void __exit param_exit(void)
{
printk(KERN_ALERT"module exit\n");
}
module_init(param_init);
module_exit(param_exit);
MODULE_LICENSE("GPL2");
MODULE_AUTHOR("couvrir");
MODULE_DESCRIPTION("param module");
MODULE_ALIAS("test module");
按照内核模块实验1,将helloworld.o修改成module_param.o文件。
虚拟机:执行make和make copy。生成.ko文件。
开发板(在挂载目录下执行):
sudo insmod module_param.ko itype=123 btype=1 ctype=111 stype="abc"
sudo rmmod module_param
内核模块可以把自己特有的一些符号给导出来,然后可以在不同的内核模块之间进行一个共享。即内核模块共享导出的符号表(变量共享、函数共享)。
EXPORT_SYMBOL(sym) //参数sym,变量名、函数名
查看符号表:cat /proc/kallsyms | grep xxx
#include
#include
#include
static int itype = 0;
module_param(itype, int, 0);
static bool btype = 0;
module_param(btype, bool, 0644);
static char ctype = 0;
module_param(ctype, byte, 0);
static char *stype = 0;
module_param(stype, charp, 0644);
static int __init param_init(void)
{
printk(KERN_ALERT "param_init!\n");
printk(KERN_ALERT "itype = %d\n", itype);
printk(KERN_ALERT "btype = %d\n", btype);
printk(KERN_ALERT "ctype = %d\n", ctype);
printk(KERN_ALERT "stype = %s\n", stype);
return 0;
}
static void __exit param_exit(void)
{
printk(KERN_ALERT "module exit\n");
}
EXPORT_SYMBOL(itype);
int my_add(int a, int b)
{
return a+b;
}
EXPORT_SYMBOL(my_add);
int my_sub(int a, int b)
{
return a-b;
}
EXPORT_SYMBOL(my_sub);
module_init(param_init);
module_exit(param_exit);
MODULE_LICENSE("GPL2");
MODULE_AUTHOR("couvrir");
MODULE_DESCRIPTION("param module");
MODULE_ALIAS("test module");
#include
#include
#include
#include "calculation.h"
static int __init calculation_init(void)
{
printk(KERN_ALERT "calculation init!\n");
printk(KERN_ALERT "itype + 1 = %d, itype - 1 = %d\n", my_add(itype, 1), my_sub(itype, 1));
return 0;
}
static void __exit calculation_exit(void)
{
printk(KERN_ALERT "calculation exit\n");
}
module_init(calculation_init);
module_exit(calculation_exit);
MODULE_LICENSE("GPL2");
MODULE_AUTHOR("couvrir");
MODULE_DESCRIPTION("calculation module");
MODULE_ALIAS("test module");
#ifndef __CALCULATION_H__
#define __CALCULATION_H__
extern int itype;
int my_add(int a,int b);
int my_sub(int a,int b);
#endif /*__CALCULATION_H__*/
KERNEL_DIR=/home/couvrir/桌面/ebf_linux_kernel_6ull_depth1/build_image/build //将内核源码编译生成的内核文件存放在该路径
ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH CROSS_COMPILE
obj-m:=module_param.o calculation.o
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
.PHONE:clean copy
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
sudo rm /home/couvrir/workdir/*.ko
copy:
sudo cp *.ko /home/couvrir/桌面/sharedir
按照内核模块实验1,将helloworld.o修改成module_param.o文件。
虚拟机:执行make和make copy。生成.ko文件。
开发板(在挂载目录下执行):
sudo insmod module_param.ko itype=123 btype=1 ctype=200 stype="abc"
sudo insmod calculation.ko
sudo rmmod calculation
sudo rmmod module_param
加载时,必须先加载相关依赖模块;卸载时,顺序相反。
uname -r:打印当前内核版本。
sudo cp /mnt/*.ko /lib/modules/4.19.35-imx6/:所有内核模块统一放到“/lib/modulers/内核版本”目录下。
sudo depmod -a:建立模块依赖关系。
cat /lib/modules/4.19.35-imx6/modules.dep | grep calculation:查看模块依赖关系。
sudo modprobe calculation:加载模块及其依赖模块。
sudo modprobe -r calculation:卸载模块及其依赖模块。