linux驱动开发篇(一)—— Linux 内核模块介绍

linux系列目录:
linux基础篇(一)——GCC和Makefile编译过程
linux基础篇(二)——静态和动态链接
ARM裸机篇(一)——i.MX6ULL介绍
ARM裸机篇(二)——i.MX6ULL启动过程
ARM裸机篇(三)——i.MX6ULL第一个裸机程序
ARM裸机篇(四)——重定位和地址无关码
ARM裸机篇(五)——异常和中断
linux系统移植篇(一)—— linux系统组成
linux系统移植篇(二)—— Uboot使用介绍
linux系统移植篇(三)—— Linux 内核使用介绍
linux系统移植篇(四)—— 根文件系统使用介绍
linux驱动开发篇(一)—— Linux 内核模块介绍
linux驱动开发篇(二)—— 字符设备驱动框架
linux驱动开发篇(三)—— 总线设备驱动模型
linux驱动开发篇(四)—— platform平台设备驱动
linux驱动开发篇(五)—— linux设备驱动面向对象的编程思想
linux驱动开发篇(六)—— 设备树的引入


一、Linux 内核模块

本篇文章是介绍Linux 驱动开发承上启下的一个知识点。在 Linux 系统中,设备驱动会以内核模块的形式出现,学习 Linux 内核模块编程是驱动开发的先决条件。

1、内核

内核按照体系结构分为两类: 微内核(Micro Kernel) 和 宏内核(Monolithic Kernel)。在微内核架构中,内核只提供操作系统核心功能,如实现进程管理、存储器管理、进程间通信、 I/O 设备管理等,而其它的应用层 IPC、文件系统功能、设备驱动模块则不被包含到内核功能中,属于微内核之外的模块,所以针对这些模块的修改不会影响到微内核的核心功能。微内核具有动态扩展性强的优点。 Windows 操作系统、华为的鸿蒙操作系统就属于这类微内核架构。

而宏内核架构是将上述包括微内核以及微内核之外的应用层 IPC、文件系统功能、设备驱动模块都编译成一个整体。其优点是执行效率非常高,但缺点也是十分明显的,一旦我们想要修改、增加内核某个功能时(如增加设备驱动程序)都需要重新编译一遍内核。 Linux 操作系统正是采用了宏内核结构。为了解决这一缺点, linux 中引入了内核模块这一机制。

![在这里插入图片描述](https://img-blog.csdnimg.cn/c3dad8d874274cfea5236e574c67d5aa.png

2、内核模块引入原因

Linux 是一个跨平台的操作系统,支持众多的设备,在 Linux 内核源码中有超过 50% 的代码都与设备驱动相关。 Linux 为宏内核架构,如果开启所有的功能,内核就会变得十分臃肿。内核模块就是实现了某个功能的一段内核代码,在内核运行过程,可以加载这部分代码到内核中,从而动态地增加了内核的功能。基于这种特性,我们进行设备驱动开发时,以内核模块的形式编写设备驱动,只需要编译相关的驱动代码即可,无需对整个内核进行编译。

3、内核模块的定义和特点

核模块的具体的定义:内核模块全称 Loadable Kernel Module(LKM), 是一种在内核运行时加载一组目标代码来实现某个特定功能的机制。
模块是具有独立功能的程序,它可以被单独编译,但不能独立运行,在运行时它被链接到内核作为内核的一部分在内核空间运行,这与运行在用户空间的进程是不一样的。模块由一组函数和数据结构组成,用来实现一种文件系统、一个驱动程序和其他内核上层功能。因此内核模块具备如下特点:

  • 模块本身不被编译入内核映像,这控制了内核的大小。
  • 模块一旦被加载,它就和内核中的其它部分完全一样。

二、Linux 内核模块的工作机制

我们编写的内核模块,经过编译,最终形成.ko 为后缀的 ELF 文件。ko 文件在数据组织形式上是 ELF(Excutable And Linking Format) 格式,是一种普通的可重定位目标文件。这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类。在学习linux基础篇(二)——静态和动态链接时,有讲解过ELF文件的详细信息。内核就是利用ELF文件留在内核模块里的信息,对内核模块进行利用的。

1、内核模块组成部分

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

  • 模块加载函数 (必须): 当通过 insmod 或 modprobe 命令加载内核模块时,模块的加载函数就会自动被内核执行,完成本模块相关的初始化工作。
  • 模块卸载函数 (必须): 当执行 rmmod 命令卸载模块时,模块卸载函数就会自动被内核自动执行,完成相关清理工作。
  • 模块许可证声明 (必须): 许可证声明描述内核模块的许可权限,如果模块不声明,模块被加载时,将会有内核被污染的警告。
  • 模块参数: 模块参数是模块被加载时,可以传值给模块中的参数。
  • 模块导出符号: 模块可以导出准备好的变量或函数作为符号,以便其他内核模块调用。
  • 模块的其他相关信息: 可以声明模块作者等信息。

2、内核模块的加载过程

linux内核模块加载函数一般以__init标识声明,模块加载函数以module_init(函数名)的形式被指定。在编写时,若初始化成功应该返回0,若初始化失败应该返回错误编码,方便用户程序利用perror等方法将它们转换为有意义的错误信息字符串。

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello  Module Init\n");
    return 0;
}
module_init(hello_init);

__init 和module_init宏定义介绍:

#define __init __attribute__((__section__(".init.text")))

带有 __init 的修饰符,表示将该函数放到可执行文件的 __init 节区中,该节区的内容只能用于模块的初始化阶段,初始化阶段执行完毕之后,这部分的内容就会被释放掉。

#define module_init(x) __initcall(x);

宏定义 module_init 用于通知内核初始化模块的时候,要使用哪个函数进行初始化。它会将函数地址加入到相应的节区 section 中,这样的话,开机的时候就可以自动加载模块了。

3、内核模块的卸载过程

当模块从内核被卸载时,系统会调用模块的模块卸载函数,该函数使用__exit来标识。

static void __exit hello_exit(void)
{
    printk("KERN_INFO  Hello  Module Exit\n");
}
module_exit(hello_exit);

4、模块声明与描述

  • MODULE_AUTHOR:模块作者
  • MODULE_DESCRIPTION:模块描述
  • MODULE_VERSION:模块版本
  • MODULE_DEVICE_TABLE:设备表
  • MODULE_ALIAS:别名

完整代码的例子:

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

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello  Module Init\n");
    return 0;
}
module_init(hello_init);
static void __exit hello_exit(void)
{
    printk("KERN_INFO  Hello  Module Exit\n");
}
module_exit(hello_exit);

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("kk");
MODULE_DESCRIPTION("hello world module");
MODULE_ALIAS("test_module");

5、模块参数

Linux 内核提供一个宏来实现模块的参数传递

#define module_param(name, type, perm) \
 module_param_named(name, name, type, perm)

#define module_param_array(name, type, nump, perm) \
module_param_array_named(name, name, type, nump, perm)
  • name: 我们定义的变量名;
  • type: 参数的类型;
  • perm: 表示的是该文件的权限,具体参数值见下表。

6、导出符号

模块可以使用如下宏导出符号到内核符号表中

#define EXPORT_SYMBOL(符号名) 
#define EXPORT_SYMBOL_GPL(符号名) 

导出符号可以被其他模块使用,只需要使用前声明一下即可。EXPORT_SYMBOL_GPL()只适用于包含GPL许可权的模块,一般认为,保守做法是linux内核不能使用非GPL许可权。
完整例子:

#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");

三、Linux内核模块的编译

对于内核模块而言,它是属于内核的一段代码,只不过它并不在内核源码中。为此,我们在编译时需要到内核源码目录下进行编译。

Makefile 文件:

KERNEL_DIR=../../ebf-buster-linux

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
  • 第 1 行:该 Makefile 定义了变量 KERNEL_DIR,来保存内核源码的目录。
  • 第 3-5 行:指定了工具链并导出环境变量
  • 第 6 行:变量 obj-m 保存着需要编译成模块的目标文件名。
  • 第 8 行:’ $ (MAKE)modules ’实际上是执行 Linux 顶层 Makefile 的伪目标 modules。通过选项’ -C’,可以让 make 工具跳转到源码目录下读取顶层 Makefile。’ M=$(CURDIR)’表明返回到当前目录,读取并执行当前目录的 Makefile,开始编译内核模块。 CURDIR 是 make 的内嵌变量,自动设置为当前目录。

将编写的驱动程序代码放到内核代码同级目录,原因是编译内核驱动模块的时候,驱动程序需要依赖上一小节编译好的内核,KERNEL_DIR=../../ebf-buster-linux标识linux内核的目录。

编译:
在这里插入图片描述

四、Linux内核模块的使用

1. 加载模块与卸载模块

使用 insmod命令加载模块:
在这里插入图片描述

2. 使用 rmmod 命令卸载模块:

在这里插入图片描述

3.modprobe命令

modprobe 和 insmod 具备同样的功能,同样可以将模块加载到内核中,除此以外 modprobe 还能检查模块之间的依赖关系,并且按照顺序加载这些依赖,可以理解为按照顺序多次执行 insmod。
通过modprobe加载的模块,可以通过modprobe卸载。
模块之间的依赖关系存放在根文件系统的 /lib/modules//modules.dep 文件中,该文件是在整体编译内核时,由depmod工具生成的。

ARM 是一种广泛使用的 CPU 架构,而 Linux 内核是一个开放源代码的操作系统内核。在 ARM 平台上,我们可以通过内核模块编程的方式与内核进行交互,实现一些自定义的功能。 下面,我们将介绍如何在 ARM Linux 上编写内核模块,并输出一个简单的 "Hello World" 消息。 ## 1. 环境准备 在开始编写内核模块之前,需要先准备好开发环境。具体步骤如下: 1. 安装交叉编译工具链。ARM 平台上的应用程序和内核模块需要使用交叉编译工具链进行编译。可以从官网下载对应的交叉编译工具链,也可以使用已经编译好的交叉编译工具链。 2. 下载内核源代码。可以从官网下载对应版本的内核源代码,也可以使用已经编译好的内核源代码。 3. 配置内核源代码。需要在内核源代码根目录下运行配置命令 `make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- menuconfig` 进行配置,选择需要的模块和功能。 ## 2. 编写内核模块 在准备好开发环境之后,可以开始编写内核模块了。具体步骤如下: 1. 创建一个新的文件夹,用于存放内核模块代码。 2. 创建一个新的 C 文件,命名为 `hello.c`。 3. 在 `hello.c` 文件中编写以下代码: ```c #include <linux/init.h> #include <linux/module.h> static int __init hello_init(void) { printk(KERN_INFO "Hello, world!\n"); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO "Goodbye, world!\n"); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("A simple hello world module"); ``` 这段代码定义了一个简单的内核模块,当模块加载时会输出 "Hello, world!" 消息,当模块卸载时会输出 "Goodbye, world!" 消息。 4. 使用交叉编译工具链进行编译。在终端中进入 `hello.c` 文件所在的文件夹,运行以下命令进行编译: ```bash arm-linux-gnueabi-gcc -Wall -Werror -O2 -o hello.ko -c hello.c ``` 这个命令将生成一个名为 `hello.ko` 的内核模块文件。 ## 3. 加载和卸载内核模块 在编写好内核模块后,我们需要将它加载到内核中进行测试。具体步骤如下: 1. 将 `hello.ko` 文件复制到 ARM Linux 系统上。 2. 在终端中进入 `hello.ko` 文件所在的文件夹,运行以下命令以加载内核模块: ```bash insmod hello.ko ``` 这个命令将调用内核中的 `init_module` 函数,执行 `hello_init` 函数,输出 "Hello, world!" 消息。 3. 查看系统日志,可以看到 "Hello, world!" 消息。 ```bash dmesg ``` 4. 在终端中运行以下命令以卸载内核模块: ```bash rmmod hello ``` 这个命令将调用内核中的 `cleanup_module` 函数,执行 `hello_exit` 函数,输出 "Goodbye, world!" 消息。 5. 再次查看系统日志,可以看到 "Goodbye, world!" 消息。 至此,我们已经成功地在 ARM Linux 上编写了一个简单的内核模块,并输出了 "Hello, world!" 消息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WALI-KANG

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值