# Kernel Module — Simple Example

Kernel modules

Goal

上文我们介绍了内核的环境的构建,其中介绍了在设置编译选项的时候可以选择是否编译成内核模块或者直接编译在内核内的方式。本文就从编译成内核模块的这一点为切入口,了解 Linux 的内核模块机制,并动手编写自己的内核模块

Preconditions

# What is “Kernel Module”

Linux 内核使用宏内核的架构,也就是操作系统内核包含了进程管理,内存管理,进程调度,设备管理等基本的资源管理组件,并且大都在特权模式[1]下进行的。这一部分被编译进内核的各个组件,无法被我们再进行修改和调试,他将成为内核的一部分,和内核一起作为一个整体存在。从这个角度看这部分可以看作是静态内核模块。

既然有静态部分,那么相应就有动态的部分。还记得我们在搭建内核环境时,能够设置为编译成内核模块的部分吗?事实上,这些便是动态的内核模块,为什么需要这一部分的存在呢?还记得静态内核的特点吗?他已经成为内核的一部分,无法作为单独的一个部分进行修改和调试,如果要改动,就需要把整个内核进行从头编译,效率非常低,这种方式加载内存方式叫做:静态加载。而动态内核模块的引入,实际上就是为了解决内核开发者的问题,在内核运行起来后能够加载新编写驱动的机制,我们称之为:动态加载。因为这种方式,实际上放开了内核模块的管控,任何用户都能编写自己的内核模块加载到内核。而这样导致了内核会因为用户编写的代码,内核变得不安全和不稳定。这一部分就对程序员的编写代码能力要求非常高。

而动态加载最主要是以下两个技术

我们这里只讨论 LKM,如果对 DKMS 机制有想了解的话,可以通过链接了解其相关知识。LKM 作为最通用,也是最经典的内核机制,我们将把重点放到这个的讨论上。

LKM:通常用于添加对新硬件(作为设备驱动程序)和 / 或文件系统的支持,或用于添加系统调用。当不再需要 LKM 提供的功能时,可以将其卸载以释放内存和其他资源。

# Basic Ops

命令意义
lsmod查看加载进的内核的 Module 状态
modinfo xx(object)查看目标内核模块的信息
sudo insmod xxx.ko将内核模块加载进内核
sudo rmmod xxx将内核模块从内核中删除
sudo modprobe xxx在内核模块目录中加载 (需要将对象放到该目录)
sudo depmod -a重建内核模块的依赖

# Example Modules

以一个最简单的 Kernel Module 来介绍内核模块的构成,我们完成的介绍一个内核模块的编写和构建文件 Makefile 的编写

image-20210211132644986

# Source Code

这一部分介绍会使用到的宏和函数。

  • 注册
    • module_init(func_name) :
      • 模块的入口定义
      • 这是系统注册会执行的函数,这个宏填入你的初始化函数名
    • module_exit(func_name)
      • 模块的出口定义
      • 这是系统注销会执行的函数,这个宏填入你的注销退出的函数名
  • 函数
    • static void :
      • 使用这个类型,使得其他文件对该函数不可见
      • 其他文件可以定义重名的文件,而不会起冲突(这样不会和内核函数命名冲突)
      • 该类型函数会被分配一个一直使用的存储区,直到退出该程序,避免多次调用函数压栈出栈
    • __init __exit :
      • 函数在链接的时候都放在 .init.text 这个区段
      • 在初始化完成后,用这些关键字标识的函数或数据所占的内存会被释放掉。
  • 信息
    • MODULE_'xxxx' :
      • 使用协议,作者,描述,别名
    • EXPORT_SYMBOL :
      • 对其他模块可见的函数

了解完这些基本的概念后,我们开始来编写实际的代码,并把这些元素用上:

命令行提示符
mkdir km_test
cd km_test
vim hello.c

Code:

hello.c
#include <linux/module.h>   // 内核模块头文件
#include <linux/init.h>     // 相关初始化头文件
static int __init my_test_init(void)   // 载入初始函数 
{
	printk(KERN_EMERG "hello world!!!\n");
	return 0;
}
static void __exit my_test_exit(void)   // 卸载退出函数
{
	printk("nice try\n");
}
int add_sum(int a,int b)    // 提供给外部的函数
{
    return a+b;
}
module_init(my_test_init);  // 注册
module_exit(my_test_exit); // 注册
EXPORT_SYMBOL(add_sum); // 导出全局 modules 可见
// 模块的相关信息 
MODULE_LICENSE("GPL");  // 协议
MODULE_AUTHOR("junwide"); // 作者
MODULE_DESCRIPTION("my first kernel module");  // 描述
MODULE_ALIAS("mytest"); // 代称

# Build Code

这一部分对照这下面 Code 部分去看,我对编写的构建代码比较全面,并且具有通用性。

  • 编译
  • ifneq ($(KERNELRELEASE),)
    • 用来判断是否是第一次执行这个 Makefile
    • 注意逗号后面是空,空就是 NULL
  • name :建议内核模块的前缀加上下划线
  • $(name)-objs :这是模块对象依赖的文件
  • obj-m : 这是模块名
  • KDIR
    • 这是编译内核的 Source Code 路径
    • 如果是交叉编译,就是对应 ARCH 下的源码路径
  • 功能:
  • install:这里完成的是自动化把 Module 安装为可识别 Module
    • 第一步:将文件拷到内核 module 的默认读取目录
    • 第二步:内核重新扫描所有存在内核并关联他的依赖
    • 第三步:使用 Modprobe 自动加载 Module 进内核
  • boot:这里完成的是将内核模块写入内核自启中
  • rboot:这里完成的是取消内核模块自启
  • clean:
    • 第一步:清空编译产生的文件
    • 第二步:去除掉加载系统目录中的文件
    • 第三步:重新建立新的依赖
命令行提示符
cd km_test
vim Makefile

Code:

构建文件
#第一次执行 KERNELRELEASE 是空的,所以执行 else 里面的
ifneq ($(KERNELRELEASE),)
name = _hello
$(name)-objs :=hello.o
obj-m :=$(name).o
#else 块
else
name = _hello
KDIR:= /lib/modules/$(shell uname -r)/build 
INSTALL_MOD_PATH = /lib/modules/$(shell uname -r)/
all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules   #编译
install:
	cp $(name).ko $(INSTALL_MOD_PATH)$(name).ko  #将 modules 复制到指定目录
	depmod -a         # 并更新依赖环境
	modprobe $(name)   # 加载 Modules
boot:
	echo $(name) >> /etc/modules-load.d/modules.conf  #加入开启自启
rboot:
	sed -i "/${name}/d" /etc/modules-load.d/modules.conf  # 删除开机自启
clean:
	rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.order *.mod
	rm -f $(INSTALL_MOD_PATH)$(name).ko
	depmod -a
endif

# Usage

命令解释
make编译 Modules 工程
sudo make install将编译后的 Modules 放进系统目录,建立依赖关系,探测启动 modules
sudo make boot将这个 modules 设置为开机自动加载
sudo make rboot将这个 modules 的开机启动删除
sudo make clean清空这个 modules 编译出来的所有东西

# Test

$ make #编译
$ sudo make install # 安装并启动
$ dmesg   #查看消息

image-20200925135813926

# Multi Modules

正如我们一般开发程序一样,需要多个文件函数相互调用,相互操作。内核模块也可以做到,不过在 coding 上和用户态程序有一些不一样,具体如下所示:

image-20210217175616785

# Source Code

模块之间相互调用需要注意

  • 被调用者:
    • EXPORT_SYMBOL() :需要对其他模块可见的函数 / 变量,再上一个例子中我们没解释的部分,就是这个声明。
  • 调用者:
    • extern :声明其他函数定义过
  • Makefile
    • 调用者在 Makefile 中需要加入 KBUILD_EXTRA_SYMBOLS 指明被调用者的 Module.symvers
    • 模块的依赖关系建立后,被调用者必须先加载到内核,调用者才可以加载进去
    • 调用者设置为开启自启后,所依赖的被调用模块会自动加载到自动开机中

使用参数时:

  • 类型:
    • static :内核程序的全局变量建议使用 static
  • 声明
    • module_param: 用于声明这是内核的可接受参数变量
      • 参数 1:变量命
      • 参数 2:类型
      • 参数 3:权限 (具体参见 stat.h
命令行提示符
#bash
vim add.c
add.c
int add(int a,int b)
{
    return a+b;
}

命令行提示符
vim cite_test.c
cite_test.c
#include <linux/module.h>
#include <linux/init.h>
// 模块参数
static char *name = "yuwei xiao";
static int age = 22;
//S_IRUGO 是参数权限,也可以用数字
module_param(age,int,S_IRUGO);
module_param(name,charp,S_IRUGO);
// 使用外部文件函数
extern int add(int a,int b);
// 声明 外部内核符号 函数
extern int add_sum(int a,int b);
static int __init cite_init(void)
{
     // 多文件编译
    printk(KERN_EMERG"Test hi");
    int vle=add(3,3);
    printk(KERN_EMERG"add value:%d\n",vle);
    // 模块参数
     printk(KERN_EMERG" name : %s\n",name);
     printk(KERN_EMERG" age : %d\n",age);
    // 使用其他模块的函数 (内核符号导出)
    int adds=add_sum(4,4);
    printk(KERN_EMERG" add_sum : %d\n",adds);
    return 0;
}
static void __exit cite_exit(void)
{
    printk(KERN_EMERG"param exit!");
}
module_init(cite_init);
module_exit(cite_exit);
// 模块可选信息
MODULE_LICENSE("GPL");// 许可证声明
MODULE_AUTHOR("junwide");// 作者声明
MODULE_DESCRIPTION("This module is a param example.");// 模块描述
MODULE_VERSION("V1.0");// 模块别名
MODULE_ALIAS("a simple module");// 模块别名

# Build Code

我们这里主要关注被调用者符号这部分

Makefile

  • 编译:
    • KBUILD_EXTRA_SYMBOLS
      • 这是被调用者的 Symver 文件
      • 将文件路径导入系统变量

symvers contains a list of all exported symbols from a kernel build. During a kernel build the symvers or symbol versions file will be generated

构建文件
#第一次执行 KERNELRELEASE 是空的,所以执行 else 里面的
ifneq ($(KERNELRELEASE),)
name = _cite
$(name)-objs := cite_test.o add.o
obj-m :=$(name).o
else
name = _cite  #变量名称
KBUILD_EXTRA_SYMBOLS += /home/junwide/Kernel_Module/hello/Module.symvers #被调用者符号
export KBUILD_EXTRA_SYMBOLS  # 导入进编译环境
KDIR:= /lib/modules/$(shell uname -r)/build 
INSTALL_MOD_PATH = /lib/modules/$(shell uname -r)/
all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules
install:
	cp $(name).ko $(INSTALL_MOD_PATH)$(name).ko
	depmod -a
	modprobe $(name)
boot:
	echo $(name) >> /etc/modules-load.d/modules.conf 
rboot:
	sed -i "/${name}/d" /etc/modules-load.d/modules.conf  #删除有配置的那一行
clean:
	rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.order *.mod
	rm -f $(INSTALL_MOD_PATH)$(name).ko
	depmod -a
endif

# Usage

命令解释
make编译 Modules 工程
sudo make install将编译后的 Modules 放进系统目录,建立依赖关系,探测启动 modules
sudo make boot将这个 modules 设置为开机自动加载
sudo make rboot将这个 modules 的开机启动删除
sudo make clean清空这个 modules 编译出来的所有东西

# Test

命令行提示符
$ make #编译
$ sudo insmod _hello.ko
$ sudo insmod _cite.ko name=yuwei.shuai
$ dmesg   #查看消息
#退出顺序
$ sudo rmmod _cite
$ sudo rmmod _hello

image-20200925140412444


  1. 特权模式:指的是拥有最高的权限对系统资源进行调用 ↩︎

更新于

请我喝[茶]~( ̄▽ ̄)~*

Junwide Xiao 微信支付

微信支付

Junwide Xiao 支付宝

支付宝