Linux教程

Linux内核与驱动学习记录-最简单的内核模块-Hello内核模块

本文主要是介绍Linux内核与驱动学习记录-最简单的内核模块-Hello内核模块,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

By: Ailson Jack
Date: 2021.05.09
个人博客:http://www.only2fire.com/
本文在我博客的地址是:http://www.only2fire.com/archives/134.html,排版更好,便于学习,也可以去我博客逛逛,兴许有你想要的内容呢。

1.内核模块的概念

因为Linux 操作系统采用了宏内核结构,宏内核的优点是执行效率非常高,但缺点也是十分明显的,一旦我们想要修改、增加内核某个功能时(如增加设备驱动程序)都需要重新编译一遍内核。为了解决这一缺点,Linux 中引入了内核模块这一机制。

内核模块就是实现了某个功能的一段内核代码,在内核运行过程,可以加载这部分代码到内核中,从而动态地增加了内核的功能。基于这种特性,我们进行设备驱动开发时,以内核模块的形式编写设备驱动,只需要编译相关的驱动代码即可,无需对整个内核进行编译。内核模块的引入不仅提高了系统的灵活性,对于开发人员来说更是提供了极大的方便。

内核模块定义:内核模块全称 Loadable Kernel Module(LKM),是一种在内核运行时加载一组目标代码来实现某个特定功能的机制。

内核模块特点:

  • 模块本身不被编译入内核映像,这控制了内核的大小;

  • 模块一旦被加载,它就和内核中的其它部分完全一样。

我们编写的内核模块,经过编译,最终形成以.ko为后缀的文件。ko 文件在数据组织形式上是 ELF(Excutable And Linking Format) 格式,是一种普通的可重定位目标文件。

2.编写Hello内核模块

对于程序入门学习来说,Hello World程序是经典的例子,这里我们也实现一个简单的Hello内核模块用于了解内核模块编程的基本框架。

hello_module.c文件的内容如下所示:

/**
 * @file hello_module.c
 * @author Ailson Jack (jackailson@foxmail.com)
 * @brief
 * @version 1.0
 * @date 2021-05-08
 *
 * @copyright Copyright (c) 2021
 *
 * @note blog:www.only2fire.com
 *
 */

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

/* 内核模块加载函数 */
static int __init hello_module_init(void)
{
    printk(KERN_EMERG "[KERN_EMERG] Hello Module init!\r\n");
    printk("[default] Hello Module init!\r\n");

    return 0;
}

/* 内核模块卸载函数 */
static void __exit hello_module_exit(void)
{
    printk(KERN_EMERG "[KERN_EMERG] Hello Module exit!\r\n");
    printk("[default] Hello Module exit!\r\n");
}

module_init(hello_module_init);
module_exit(hello_module_exit);

MODULE_LICENSE("GPL v2"); //表示模块代码接受的软件许可协议
MODULE_AUTHOR("Ailson Jack"); //描述模块的作者信息
MODULE_DESCRIPTION("hello module"); //对模块的简单介绍
MODULE_ALIAS("test_module"); //给模块设置一个别名

2.1.Hello内核模块代码框架分析

Linux 内核模块的代码框架通常由下面几个部分组成:

  • 模块加载函数 (必须):当通过 insmod 或 modprobe 命令加载内核模块时,模块的加载函数就会自动被内核执行,完成本模块相关的初始化工作。

  • 模块卸载函数 (必须):当执行 rmmod 命令卸载模块时,模块卸载函数就会自动被内核自动执行,完成相关清理工作。

  • 模块许可证声明 (必须):许可证声明描述内核模块的许可权限,如果模块不声明,模块被加载时,将会有内核被污染的警告。

  • 模块参数:模块参数是模块被加载时,可以传值给模块中的参数。

  • 模块导出符号:模块可以导出准备好的变量或函数作为符号,以便其他内核模块调用。

  • 模块的其他相关信息:可以声明模块作者等信息。

2.2.内核模块头文件

Hello内核模块中,使用3个头文件,下面说说这3个头文件具体提供的信息:

  • #include <linux/module.h>:包含内核模块信息声明的相关函数;

  • #include <linux/init.h>:包含了 module_init() 和 module_exit() 函数的声明;

  • #include <linux/kernel.h>: 包含内核提供的各种函数,如 printk。

2.3.内核模块加载/卸载函数

module_init():声明内核模块加载函数,加载内核模块的时候会调用声明的内核模块加载函数,模块加载成功,会在**/sys/module**下新建一个以模块名为名的目录。

module_exit():声明内核模块卸载函数,卸载内核模块的时候会调用声明的内核模块卸载函数。

__init 用于修饰函数, __initdata 用于修饰变量。带有 __init 的修饰符,表示将该函数放到可执行文件的 __init 节区中,该节区的内容只能用于模块的初始化阶段,初始化阶段执行完毕之后,这部分的内容就会被释放掉,真可谓是“针尖也要削点铁”。

__exit 用于修饰函数,__exitdata 用于修饰变量。带有__exit的修饰符,表示将该函数放到可执行文件的__exit节区,当执行完模块卸载阶段之后,就会自动释放该区域的空间。

注意:hello_module_init()函数的返回值是int,hello_module_exit()的返回值是void,并且这两个函数都使用static进行修饰,表示函数只能在本文件进行调用,不能被其他文件调用。

2.4.内核打印函数-printk

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>" //调试信息

printk函数可以指定打印等级,当不指定打印等级的时候,会使用默认的打印等级。

查看当前系统 printk 打印等级: cat /proc/sys/kernel/printk,从左到右依次对应控制台日志级别、默认消息日志级别、最小的控制台日志级别、默认控制台日志级别。

在这里插入图片描述

控制台日志级别:优先级高于该值得消息将被打印到到控制台;

默认消息日志级别:将用该优先级来打印没有指定优先级的消息;

最小的控制台日志级别:控制台日志级别可被设置的最小值(最高优先级);

默认控制台日志级别:控制台日志级别的缺省值。

以上的数值设置,数值越小,优先级越高。

假设你想让hello_module_init()或者hello_module_exit()函数中,没有指定打印等级的printk的内容输出到控制台,那么你可以将"默认消息日志级别"设置为小于4,可以设置为3(只需要数值小于控制台日志级别即可),执行的命令如下:

sudo sh -c "echo '4 3 1 7' > /proc/sys/kernel/printk"

然后执行加载或者卸载模块,就可以看到未指定打印等级的消息输出到控制台了。

查看内核所有打印信息: dmesg,注意内核 log 缓冲区大小有限制,缓冲区数据可能被覆盖掉。

3.内核模块的makefile

对于内核模块而言,它是属于内核的一段代码,只不过它并不在内核源码中。为此,我们在编译时需要到内核源码目录下进行编译。编译内核模块使用的 Makefile 文件,和我们前面编译 C 代码使用的 Makefile 大致相同,这得益于编译 Linux 内核所采用的 Kbuild 系统,因此在编译内核模块时,我们也需要指定环境变量 ARCH 和CROSS_COMPILE 的值。

编译Hello内核模块使用的Makefile文件内容如下:

# 指向编译出来的 linux 内核具体路径
KERNEL_DIR = ../kernel/ebf-buster-linux/build_image/build

# 定义变量,并且导出变量给子 Makefile 使用
ARCH = arm
CROSS_COMPILE = arm-linux-gnueabihf-
export ARCH CROSS_COMPILE

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

# 选项 "-C":让 make 工具跳转到 linux 内核目录下读取顶层 Makefile
# "M=" 表示内核模块源码目录
# $(CURDIR): Makefile 默认变量,值为当前目录所在路径
# make modules: 执行 Linux 顶层 Makefile 的伪目标,它实现内核模块的源码读取并编译为.ko文件
all:
	$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules

.PHONY:clean copy

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

copy:
	cp *.ko /home/ailsonjack/share/nfs/temp

在内核模块的目录中,执行make命令编译内核模块,生成hello_module.ko文件,将hello_module.ko文件通过nfs或者scp拷贝到开发板,即可加载该内核模块。

4.内核模块常用命令

4.1.lsmod

lsmod 列出当前内核中的所有模块,格式化显示在终端,其原理就是将/proc/module 中的信息调整一下格式输出。 lsmod 输出列表有一列 Used by,它表明此模块正在被其他模块使用,显示了模块之间的依赖关系。

4.2.insmod

如果要将一个模块加载到内核中, insmod 是最简单的办法, insmod+模块完整路径就能达到目的,前提是你的模块不依赖其他模块,还要注意需要 sudo 权限。如果你不确定是否使用到其他模块的符号,你也可以尝试modprobe,后面会有它的详细用法。

4.3.rmmod

rmod 工具仅仅是将内核中运行的模块删除,只需要传给它路径就能实现。

rmmod 不会卸载一个模块所依赖的模块,需要依次卸载,当然用 modprobe -r 可以一键卸载。

4.4.modprobe

modprobe 和 insmod 具备同样的功能,同样可以将模块加载到内核中,除此以外 modprobe 还能检查模块之间的依赖关系,并且按照顺序加载这些依赖,可以理解为按照顺序多次执行 insmod。

4.5.depmod

modprobe 是怎么知道一个给定模块所依赖的其他的模块呢?在这个过程中, depend 起到了决定性作用,当执行 modprobe 时,它会在模块的安装目录下搜索 module.dep 文件,这是 depmod 创建的模块依赖关系的文件。

4.6.modinfo

modinfo 用来显示内核模块一些信息。比如:modinfo hello_module.ko

5.系统自动加载内核模块

我们自己编写了一个模块,或者说怎样让它在板子开机自动加载呢?这里就需要用到上述的 depmod 和 modprobe 工具了。

首先需要将我们想要自动加载的模块统一放到”/lib/modules/内核版本”目录下,内核版本使用 'uname -r’查询;其次使用 depmod 建立模块之间的依赖关系,命令’ depmod -a’;这个时候我们就可以在 modules.dep 中看到模块依赖关系。

最后在/etc/modules 加上我们自己的模块,注意在该配置文件中,模块不写成.ko 形式代表该模块与内核紧耦合,有些是系统必须要跟内核紧耦合,比如 mm 子系统,一般写成.ko 形式比较好,如果出现错误不会导致内核出现 panic 错误,如果集成到内核,出错了就会出现panic。

欢迎关注博主的公众号呀:

在这里插入图片描述

如果文中有什么问题欢迎指正,毕竟博主的水平有限。

如果这篇文章对你有帮助,记得点赞和关注博主就行了^_^。

排版更好的内容见我博客的地址:http://www.only2fire.com/archives/134.html

注:转载请注明出处,谢谢!^_^

这篇关于Linux内核与驱动学习记录-最简单的内核模块-Hello内核模块的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!