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 内核模块
    • 1、内核
    • 2、内核模块引入原因
    • 3、内核模块的定义和特点
  • 二、Linux 内核模块的工作机制
    • 1、内核模块组成部分
    • 2、内核模块的加载过程
    • 3、内核模块的卸载过程
    • 4、模块声明与描述
    • 5、模块参数
    • 6、导出符号
  • 三、Linux内核模块的编译
  • 四、Linux内核模块的使用
    • 1. 加载模块与卸载模块
    • 2. 使用 rmmod 命令卸载模块:
    • 3.modprobe命令


一、Linux 内核模块

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

1、内核

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

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

linux驱动开发篇(一)—— Linux 内核模块介绍_第1张图片

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 
#include 
#include 

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 
#include 
#include 

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驱动开发篇(一)—— Linux 内核模块介绍_第2张图片

四、Linux内核模块的使用

1. 加载模块与卸载模块

使用 insmod命令加载模块:
linux驱动开发篇(一)—— Linux 内核模块介绍_第3张图片

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

linux驱动开发篇(一)—— Linux 内核模块介绍_第4张图片

3.modprobe命令

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

你可能感兴趣的:(linux,linux,驱动开发,arm开发)