内核模块的作用

解决linux内核可扩展性和可维护性相对较差的缺陷

现代内核派系

宏内核:关键功能和服务功能均在内核空间提供

  • 运行效率高

  • 扩展性较差

微内核:内核空间只提供关键功能,服务功能在用户空间提供

  • 运行效率较低

  • 安全性、扩展性较高

左侧:宏内核(Monolithic Kernel)架构

这是 Linux、Unix 等传统操作系统的典型设计:

  • 内核是一个整体:所有核心功能(VFS、IPC、进程调度、设备驱动等)都运行在内核态,作为一个单一的大程序。
  • 应用程序通过系统调用(system call)与内核交互:APP 无法直接操作硬件或核心功能,必须通过统一的系统调用接口进入内核态,由内核完成操作。
  • 优点
    • 性能高:模块间直接函数调用,无额外开销。
    • 功能集成度高:各子系统深度整合,协作高效。
  • 缺点
    • 内核庞大复杂,维护难度高。
    • 一处模块崩溃(如驱动)可能导致整个内核宕机。

右侧:微内核(Microkernel)架构

这是 QNX、Minix 等系统采用的设计思路:

  • 内核极度精简:只保留最基础的功能(如内存管理、进程调度、基础 IPC),运行在核心态。
  • 大部分功能移到用户态:文件系统、设备驱动、应用层 IPC 等都作为独立的用户态服务进程运行。
  • 应用程序通过消息传递与服务交互:APP 直接与用户态服务通信,服务之间也通过内核提供的 IPC 机制通信。
  • 优点
    • 稳定性高:单个服务崩溃不会影响整个系统。
    • 模块化强,易于扩展和移植。
  • 缺点
    • 性能开销大:频繁的进程间消息传递和上下文切换。
维度 宏内核(左侧) 微内核(右侧)
核心思想 功能集中,效率优先 功能分离,稳定优先
交互方式 系统调用(进入内核态) 消息传递(用户态服务间)
故障影响 内核模块崩溃 → 系统宕机 服务崩溃 → 仅该功能不可用
代表系统 Linux, FreeBSD, Windows QNX, Minix, seL4

内核模块加载/卸载

  • 使用insmod命令加载

  • 使用rmmod命令卸载

内核模块入口/出口

  • module_init():加载模块式该函数自动执行,进行初始化操作

  • module_exit():卸载模块时函数自动执行,进行清理操作

内核模块信息声明

  • MODULE_LICENSE():表示模块代码接受的软件许可协议,Linux内核遵循GPL V2开源协议,内核模块与linux内核保持一致即可。

  • MODULE_AUTHOR():描述模块的作者信息

  • MODULE_DESCRIPTION():对模块的简单介绍

  • MODULE_ALIAS():给模块设置一个别名

内核模块实验一

hellomoudle.c

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

static int __init hello_init(void)
{
    printk(KERN_EMERG "[ KERN_EMERG ]  Hello  Module Init\n");
    printk( "[ default ]  Hello  Module Init\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk("[ default ]   Hello  Module Exit\n");
}

module_init(hello_init);
module_exit(hello_exit);

//MODULE_LICENSE("GPL2");
MODULE_AUTHOR("embedfire ");
MODULE_DESCRIPTION("hello world module");
MODULE_ALIAS("test_module");

内核模块头文件

- #include <linux/module.h>:包含内核模块信息声明的相关函数
- #include <linux/init.h>:包含了 module_init()和 module_exit()函数的声明
- #include <linux/kernel.h>:包含内核提供的各种函数,如printk

内核模块打印函数

- printf:glibc实现的打印函数,工作于用户空间

- printk:内核模块无法使用glibc库函数,内核自身实现的一个类printf函数,但是需要指定打印等级。
  - \#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>" 调试信息

查看当前系统printk打印等级:`cat /proc/sys/kernel/printk`

- 当前控制台日志级别
- 默认消息日志级别
- 最小的控制台级别
- 默认控制台日志级别

打印内核所有打印信息:dmesg

- 内核log缓冲区大小有限制,缓冲区数据可能被冲掉

简单来说:printk 是内核输出日志的 “入口”,打印等级决定了哪些日志会被内核记录;dmesg 是读取这些已记录日志的 “出口”,我会用通俗的方式拆解它们的关联逻辑。

(日志体系理解)先理解 cat /proc/sys/kernel/printk 的输出含义

执行该命令会得到 4 个数字(示例:4 4 1 7),这 4 个数字是 printk 的核心规则,直接决定哪些日志会被记录:

表格

位置 含义 作用
第 1 个(console_loglevel) 控制台输出等级 只有「消息等级 < 该值」的日志会输出到终端(如串口 / 控制台)
第 2 个(default_message_loglevel) 默认消息等级 若 printk 未指定等级,消息会使用这个默认等级
第 3 个(minimum_console_loglevel) 最小控制台等级 控制台等级不能被设置得比这个更小
第 4 个(default_console_loglevel) 默认控制台等级 系统启动时的初始控制台等级

关键规则:内核仅会记录「消息等级 < console_loglevel」的日志到缓冲区(比如示例中 console_loglevel=4,那么等级 0-3 的日志会被记录,等级 4 及以上的不会)。

(日志体系理解)printk 等级如何影响 dmesg 能看到的内容

举个具体例子,帮你理解:假设你在内核模块中写了 3 行 printk 代码:

printk(KERN_ERR "这是错误消息(等级3)\n");    // 等级3
printk(KERN_WARNING "这是警告消息(等级4)\n");// 等级4
printk(KERN_INFO "这是普通消息(等级6)\n");   // 等级6

此时系统 printk 默认等级是 4 4 1 7(console_loglevel=4):

  • KERN_ERR(3):3 < 4 → 写入内核日志缓冲区 → dmesg 能看到;
  • KERN_WARNING(4):4 不小于 4 → 不写入缓冲区 → dmesg 看不到;
  • KERN_INFO(6):6 不小于 4 → 不写入缓冲区 → dmesg 看不到。

makefile

KERNEL_DIR=../../ebf_linux_kernel/build_image/build

ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-
export  ARCH  CROSS_COMPILE

obj-m := hellomodule.o
all:
	$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules

.PHONE:clean copy

clean:
	$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean	

copy:
	sudo  cp  *.ko  /home/embedfire/workdir

Makefile分析

  • KERNEL_DIR:指向linux内核具体路径

  • export:导出变量给子Makefile使用

  • obj-m := <模块名>.o:定义要生成的模块

  • $(MAKE):Makefile的默认变量,值为make

  • 选项”-C”:让make工具跳转到linux内核目录下读取顶层Makefile

  • M=:表示内核模块源码目录

  • $(CURDIR):Makefile默认变量,值为当前目录所在路径

  • make modules:执行Linux顶层Makefile的伪目标,它实现内核模块的源码读取并编译为.ko文件

成功截图

内核模块实验二

模块参数

根据不同应用场合给内核模块传递不同参数,提高内核模块灵活性

  • 定义一个常见变量

  • 使用module_param宏把传参值赋给变量

module_param(name, type, perm)
  • name:参数名

  • type:参数类型,如int、byte、bool、charp...

  • perm:读写权限

    • 不允许设置可执行权限

    • 在“/sys/module/模块名/parameters”目录下,会生成该参数对应的文件名

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

static int itype=0;
module_param(itype,int,0);

static bool btype=0;
module_param(btype,bool,0700);

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("embedfire ");
MODULE_DESCRIPTION("module_param");
MODULE_ALIAS("module_param");

符号共享

内核模块可以共享导出的符号表

  • 变量共享

  • 函数共享

EXPORT_SYMBOL(sym)
  • sym:变量名或者函数名

查看符号表

cat /proc/kallsyms | grep xxx

Makefile修改

  • 有依赖关系的模块,放在一起进行编译

  • clean伪目标增加清除共享文件夹指令

模块手动加载

  • 加载时,必须先加载相关依赖模块

  • 卸载时,顺序相反

在上面程序中,导出了一个变量和两个函数,导出的这些变量函数我们可以放到其他模块中进行声明和使用

calculation.h

#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.c

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

#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("embedfire ");
MODULE_DESCRIPTION("calculation module");
MODULE_ALIAS("calculation_module");

由于要使用parametermodule.c导出的参数和变量,所以这里的calculation依赖parametermodule

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐