1.1 Linux内核模块编写

简介

Linux内核模块是一段单独编译的内核代码,它在Linux内核空间运行,在需要时被加入内核,在不需要时也可从内核中卸载

内核模块结构

一个内核模块通常包括以下几个部分

  • 包含相应头文件,一般都有
  • 模块加载函数,用于在加载函数的过程中完成对模块的初始化
  • 模块卸载函数,用于卸载模块时清理相应的资源
  • 模块信息,必须要需要包含MODULE_LICENSE(“GPL”),以表示此模块遵循GPL协议,其他的如MODULE_AUTHOR(“CSDN”)、MODULE_DESCRIPTION(“module test”)、MODULE_ALIAS(“module”)等均是可选项

模块加载函数详解

模块加载函数在模块加载阶段运行,它通常用于初始化内核模块中所需的资源,其原型为“int __init init_module(void)”(其中”__init“表示它只在初始化阶段运行一次,编译器在处理时会将其链接到特定的代码段中,在模块加载完成后释放此内存),此函数返回0表示执行成功,它通常需要采用宏定义“module_init”进行导出。

module_init宏

”module_init“宏用于导出内核模块的加载函数,因为内核模块由两种编译方式,分别是编译为独立模块和与内核核心代码编译到一起
当编译为一个独立的模块时”module_init“宏展开如下:

#define module_init(initfn)													\
	/* 用于校验模块函数类型,当函数不是int init_module(void)无法编译通过 */	\
	static inline initcall_t __maybe_unused __inittest(void)				\
	{ return initfn; }														\
	/* 给入口函数取别名为init_module模块加载时通过别名调用加载函数 */			\
	int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));

当与内核编译到一起时”module_init“宏展开如下:

#define module_init(x)	__initcall(x);
#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn)		__define_initcall(fn, 6)
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
#define ___define_initcall(fn, id, __sec)									\
	static initcall_t __initcall_##fn##id __used							\
		__attribute__((__section__(#__sec ".init"))) = fn;
/* 对上面宏进行展开 */
#define module_init(x)																	\
	/* 在”.initcall6.init“代码段定义一个”__initcall_##x6“的变量,用于存储加载函数地址 */	\
	/* 系统初始化时遍历此代码段,找到模块加载函数并执行 */									\
	static initcall_t __initcall_##x6 __used 											\
		__attribute__((__section__(".initcall6.init"))) = x

模块卸载函数讲解

模块卸载函数在模块被卸载的时候执行,它通常用于清理模块初始化时创建或注册的资源,其原型为”void __exit cleanup_module(void)“(”__exit“表示此函数只在模块卸载时执行一次,当与内核编译到一起时,编译器不会对此函数进行链接),它通常需要采用宏定义“module_exit”进行导出。

module_exit宏

当编译为一个独立的模块时”module_exit“宏展开如下:

#define module_exit(exitfn)													\
	/* 检查函数类型 */														\
	static inline exitcall_t __maybe_unused __exittest(void)				\
	{ return exitfn; }														\
	/* 为模块卸载函数取别名,模块卸载时通过别名调用 */							\
	void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));

当与内核编译到一起时,因为无需对内核模块进行卸载,所以”module_exit“宏无实际意义。

模块信息

开源协议声明

内核模块必须通过”MODULE_LICENSE“声明开源协议类型,通常采用”MODULE_LICENSE(“GPL”)“,表示执行GPL开源协议,如果不正确声明开源协议类型则无法正常访问内核中通过”EXPORT_SYMBOL_GPL“导出的函数或者变量

其他信息

常用的模块信息声明还有:

  • MODULE_AUTHOR 声明作者
  • MODULE_DESCRIPTION 添加模块描述
  • MODULE_ALIAS 模块别名

内核模块管理指令

insmod 加载内核模块,必须先加载此模块所依赖的其他模块
modprobe 自动加载内核模块,可以自动处理内核模块将的依赖关系,在使用此命令前需要对模块进行安装操作
rmmod 卸载模块,必须要先卸载依赖于此模块的其他模块
modinfo 查看模块的详细信息
depmod 查看模块的依赖关系

内核模块代码讲解

//包含必要头文件
#include 
#include 
#include 

//模块加载函数,在模块加载时会被调用一次
//__init 表示此函数只在模块加载时被调用一次
//返回0,表示成功,失败通常返回一个负值
static int __init driver_init(void)
{
	printk("module test\r\n");
	return 0;
}

//模块卸载函数,在模块卸载时会被调用一次
//__exit 表示此函数只在模块卸载时被调用一次
static void __exit driver_exit(void)
{
	printk("module exit\r\n");
}

//导出模块加载函数
module_init(driver_init);
//导出模块卸载函数
module_exit(driver_exit);

//表示遵循GPL开源协议
MODULE_LICENSE("GPL");
//模块作者信息
MODULE_AUTHOR("csdn");
//模块描述
MODULE_DESCRIPTION("module test");
//模块别名
MODULE_ALIAS("module");

Makefile讲解

以下是一个在内核源码树外编译内核模块的Makefile

ifeq ($(KERNELRELEASE),)
#第一次执行时 KERNELRELEASE 为空,执行此分支

#内核源码目录,需要先编译内核源码,在编译源码外的内核模块
#必须使用编译内核模块时编译出来的内核,否则可能会出现内核模块加载失败
KERNELDIR ?= /home/lf/workspace/source/my_source/linux/linux-5.4.31
#NFS跟文件系统目录
ROOTFS ?= /home/lf/workspace/rootfs
#当前目录
PWD := $(shell pwd)

#$(MAKE) 相当于 make
#-C $(KERNELDIR) 执行 KERNELDIR 目录的Makefile
#M=$(PWD) 内核源码树之外的一个目录
#modules 只编译模块
module:
	$(MAKE) -C $(KERNELDIR) ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- M=$(PWD) modules
install:
#	因为采用insmod加载模块,所以拷贝到NFS跟文件系统的root目录即可
#	cp *.ko $(ROOTFS)/root/
#	若采用modprobe加载模块,则需要进行安装操作
	$(MAKE) -C $(KERNELDIR) ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- M=$(PWD) INSTALL_MOD_PATH=$(ROOTFS) modules_install
clean:
	rm -rf *.o  .*.cmd *.mod.* *.mod modules.order Module.symvers .tmp_versions
else
#当从源码目录的 Makefile 再次进入时 KERNELRELEASE 已经赋值,执行此分支
#obj-m 表示编译成模块
	obj-m := module_test.o
endif

实验

  1. 在内核源码树中编译一遍Linux内核

  2. 从”https://gitcode.net/lf282481431/linux_driver_demo/-/tree/master/01-kernel_module/01-module“下载源码

  3. 修改makefile文件中的”KERNELDIR“和”ROOTFS“,”KERNELDIR“为内核源码树跟目录,”ROOTFS“为NFS跟文件系统跟目录
    1.1 Linux内核模块编写_第1张图片

  4. 在内核模块代码目录执行make
    1.1 Linux内核模块编写_第2张图片

  5. 根据需求执行”make copy“或“make install”,”make copy“用于将编译出来的模块拷贝到NFS跟文件系统的root目录,以方便采用”imsmod“进行加载,“make install”则是将内核模块安装到跟文件系统中,以方便采用”modprobe“对模块进行加载
    在这里插入图片描述
    1.1 Linux内核模块编写_第3张图片

  6. 通过NFS挂在启动开发板

  7. 通过root用户进入开发板系统,并切换到root目录

  8. 执行”insmod test_module.ko“即可完成模块加载,加载过程中”driver_init“函数被执行,控制台输出”module init“字符串
    在这里插入图片描述

  9. 执行”rmmod test_module.ko“命令,模块被卸载,卸载过程中”driver_exit“函数被执行,控制台输出”module exit“字符串
    在这里插入图片描述

  10. 如果前面对模块执行了安装操作,这里还可以通过”modprobe test_module.ko“加载模块
    在这里插入图片描述

  11. 通过modprobe命令加载的模块最好使用modprobe -r 进行卸载
    在这里插入图片描述

你可能感兴趣的:(linux,运维,服务器)