没志青年
发布于 2025-06-14 / 19 阅读
0

3 Linux 内核模块

Linux 是单内核操作系统,所有的内核功能被整体编译在一起,形成一个单独的内核镜像文件。

优点是执行效率非常高,缺点是要增加、删除、修改内核的某个功能,需要重新编译和重启整个系统。

后来 Linux 引入内核模块来弥补这一缺点。

内核模块是单独编译的一段代码,在 Linux 运行时可动态的加载和卸载,不需要重启整个系统。。

内核模块有利于减小内核镜像文件的体积,因为一个产品并不会使用所有的功能,把需要的部分编译进镜像中就行了。

内核模块并不特指驱动程序。

模块编程与应用编程的区别:

不同点

内核模块

应用程序

API来源

不能使用任何库函数

各种库函数均可以使用

运行空间

内核空间

用户空间

运行权限

特权模式运行

非特权模式运行

编译方式

静态编译进内核镜像或编译特殊的 ko 文件

elf 格式的应用程序可执行文件

运行方式

模块中的函数在需要时被动调用

从 main 函数开始顺序执行

入口函数

init_module

main

退出方式

cleanup_module

main函数返回或调用exit

浮点支持

一般不涉及浮点运算,因此 printk 不支持浮点数据

支持浮点运算,printf 可以打印浮点数据

并发考虑

需要考虑多种执行流并发的竞态情况

只需考虑多任务并行的竞态

程序出错

可能会导致整个系统崩溃

只会让自己崩溃

内核模块基本格式

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

static int __init mydrv_init(void) {
    printk("Hello linux module init!\n");
    return 0;
}

static void __exit mydrv_exit(void) {
    printk("Hello linux module exit!\n");
}

module_init(mydrv_init);
module_exit(mydrv_exit);

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("HLTJ");
MODULE_DESCRIPTION("Driver Description");
MODULE_VERSION("1.0");
MODULE_ALIAS("my-drv");
  • module_init() 宏:模块被加载时,返回0表示成功,返回负值表示失败。

  • module_exit() 宏:模块被卸载时

老版本的是固定的函数名 init_module、cleanup_module,已经不用了。

初始化和销毁函数都只会被调用一次,所以该函数所占用的内存应该被释放掉,加上 __init 和 __exit 就是这个作用。

函数名可以自定义带来的一个问题是,可能和内核中的某个函数重名了,导致编译时出现函数重复定义问题。

C 语言没有 C++ 中的命名空间,所以使用 static 将函数的链接属性设置为内部,从而解决该问题。

内核模块信息宏:

MODULE_LICENSE("GPL"); 版权声明,这个是必须的,防止你写的代码污染了开源的内核的版,没有这行代码,内核中的某些功能函数无法使用,并且会报错:

常用:

  • GPL

  • GPL v2

  • MODULE_AUTHOR:作者

  • MODULE_DESCRIPTION:模块的描述

  • MODULE_VERSION:

  • MODULE_ALIAS:可以有多个别名

编译内核模块

(1)编译进Linux内核镜像

当模块调试没问题后直接编译到内核中。

1、学习阶段的驱动,没啥实际意义,放在 /drivers/misc/ 杂项驱动目录中。

mydrv.c

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

static int __init mydrv_init(void) {
    printk("Hello linux module init!\n");
    printk("####################\n####################\n####################\n");
    return 0;
}

static void __exit mydrv_exit(void) {
    printk("Hello linux module exit!\n");
    printk("####################\n####################\n####################\n");
}

module_init(mydrv_init);
module_exit(mydrv_exit);

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("HLTJ");
MODULE_DESCRIPTION("Driver Description");
MODULE_VERSION("1.0");
MODULE_ALIAS("my-drv");

2、在 /drivers/misc/Kconfig 中添加一行

menu "Misc devices"

config MY_DRV
	tristate "test driver"
	default y
	help
	  The driver only for study.

default y 这里默认是选中的。

3、在 /drivers/misc/Makefile 中添加一行

obj-$(CONFIG_MY_DRV)	+= mydrv.o

config 名字 和 CONFIG_名字,这俩的名字要保持一致。

mydrv.o 是 mydrv.c 这俩的名字要保持一致。

4、图形界面中勾选

5、编译

make zImage -j$(nproc)

6、看启动日志,驱动被正确加载

编译到内核中的模块不支持卸载。

(2)编译出单独的模块

用于模块调试阶段,编译模块之前需要先完整的编译一次内核镜像,因为 ko 模块的编译依赖内核生成的文件。

需要 Linux 内核支持模块动态加载,一般默认都是支持的:

模块随便放在哪里。

Makefile 文件模板:

KERNELDIR := /opt/Linux_Workspace/i.MX6ULL/zdyz/linux
CURRENT_PATH := $(shell pwd)
obj-m := mydrv.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules -j$(nproc)
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

编译:

make

多个源文件编译为一个内核模块:

内核模块操作

1)加载模块

insmod 命令

insmod 模块文件名.ko

modprobe 命令,推荐使用。

modprobe 模块文件名
modprobe 模块别名

模块加载后使用 dmesg 命令,查看模块的输出信息:

2)卸载模块

rrmod 模块文件名

3)查看模块信息

modinfo 模块文件名

内核模块传参