Linux驱动开发之内核模块

 

一、简介

Linux系统中,内核模块是指一段可加载的代码,可以动态地插入(移除)到内核中,以扩展或修改内核的功能。内核模块是Linux内核的一个重要组成部分,它们允许用户或开发人员在不重新编译整个内核的情况下修改或扩展内核功能。内核模块可以实现许多功能,例如添加文件系统、设备驱动程序、网络协议栈等。它们通常编写在C语言中,并使用内核AP进行偏程。内核模块的代码必须遵循内核的编程规范,并且必须经过内核源代码的编译和链接才能成为可加载的内核模块。内核模块可以通过命令行或在系统启动时自动加载。一旦内核模块被加载,它会在内核中创建个新的模块对象,并将其添加到内核模块列表中。内核模块可以通过内核模块接口与内核进行通信,例如获取内核参数、注册设备驱动程序等。总之,内核模块是Linux系统中一个非常重要的组成部分,它们允许用户或开发人员动态地扩展和修改内核功能,从而使Linux系统更加灵活和可定制。

二、方法

  1. 把所有需要的功能都编译到Linux内核中。但这会导致两个问题:
    • 一是生成的内核会很大(根据实际情况而定)
    • 二是如果我们要在现有的内核中新增或删除功能,将不得不重新编译内核。
  2. 另一种机制可使得编译出的内核本身并不需要包含所有功能,而在这些功能需要被使用的时候,其对应的代码被动态地加载到内核中。Linux提供了这样的机制,这种机制被称为模块(Module)。模块具有这样的特点:
    • 模块本身不被编译入内核映像,从而控制了内核的大小。
    • 模块一旦被加载,它就和内核中的其他部分完全一样。

三、简单示例

code

  /*
  * a simple kernel module: hello
  *
  * Copyright (C) 2023 wtp
  *
  * Licensed under GPLv2 or later.
  */

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

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

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

  MODULE_AUTHOR("wtp");
  MODULE_LICENSE("GPL v2");
  MODULE_DESCRIPTION("A simple Hello World Module");
  MODULE_ALIAS("a simplest module");

Makefile

  KERNELDIR := <Linux源码路径>
  CURRENT_PATH := $(shell pwd)
  obj-m := hello.o

  build: kernel_modules

  kernel_modules:
  $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
  clean:
  $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

这个最简单的内核模块只包含内核模块加载函数、卸载函数和对GPL v2许可权限的声明以及一些描述信息。编译它会产生hello.ko目标文件,通过“insmod./hello.ko”命令可以加载它,通过“rmmod hello”命令可以卸载它,加载时输出“Hello World enter”,卸载时输出“Hello World exit”。内核模块中用于输出的函数是内核空间的printk()而不是用户空间的printf(),printk()的用法和printf()基本相似,但前者可定义输出级别。printk()可作为一种最基本的内核调试手段,在Linux驱动的调试章节中将会详细讲解。

四、模块动态加载相关命令

查看以加载的模块

Linux中,使用lsmod命令可以获得系统中已加载的所有模块以及模块间的依赖关系。lsmod命令实际上是读取并分析"/proc/modules"文件,与上述lsmod命令结果对应的cat /proc/modules

加载

  • insmod hello.ko命令可以加载hello模块,加载时输出“Hello World enter”。
  • modprobe hello.ko命令比insmod命令要强大,它在加载某模块时,会同时加载该模块所依赖的其他模块。使用modprobe命令加载的模块若以modprobe-r hello.ko的方式卸载,将同时卸载其依赖的模块。

卸载

  • rmmod hello.ko命令可以卸载模块,卸载时输出“Hello World exit”。
  • modprobe-r hello.ko命令也可以卸载模块,优点如上。

查看具体模块

modinfo hello.ko命令可以获得模块的信息,包括模块作者、模块的说明、模块所支持的参数以及vermagic.

五、其它

模块参数

我们可以用“module_param(参数名,参数类型,参数读/写权限)”为模块定义一个参数,例如下列代码定义了1个整型参数和1个字符指针参数:

  static char *book_name = "dissecting Linux Device Driver";
  module_param(book_name, charp, S_IRUGO);
  static int book_num = 4000;
  module_param(book_num, int, S_IRUGO);

在装载内核模块时,用户可以向模块传递参数,形式为“insmode(或modprobe)模块名参数名=参数值”,如果不传递,参数将使用模块内定义的缺省值。如果模块被内置,就无法insmod了,但是bootloader可以通过在bootargs里设置“模块名.参数名=值”的形式给该内置的模块传递参数。参数类型可以是byte、short、ushort、int、uint、long、ulong、charp(字符指针)、bool或invbool(布尔的反),在模块被编译时会将module_param中声明的类型与变量定义的类型进行比较,判断是否一致。除此之外,模块也可以拥有参数数组,形式为“module_param_array(数组名,数组类型,数组长,参数读/写权限)”。 模块被加载后,在/sys/module/目录下将出现以此模块名命名的目录。当“参数读/写权限”为0时,表示此参数不存在sysfs文件系统下对应的文件节点,如果此模块存在“参数读/写权限”不为0的命令行参数,在此模块的目录下还将出现parameters目录,其中包含一系列以参数名命名的文件节点,这些文件的权限值就是传入module_param()的“参数读/写权限”,而文件的内容为参数的值。运行insmod或modprobe命令时,应使用逗号分隔输入的数组元素。

导出符号

Linux/proc/kallsyms文件对应着内核符号表,它记录了符号以及符号所在的内存地址。 模块可以使用如下宏导出符号到内核符号表中:

  EXPORT_SYMBOL(符号名);
  EXPORT_SYMBOL_GPL(符号名);

导出的符号可以被其他模块使用,只需使用前声明一下即可。 EXPORT_SYMBOL_GPL()只适用于包含GPL许可权的模块。

模块的多文件编译

  obj-m := modulename.o
  modulename-objs := file1.o file2.o