Linux内核模块学习

注:本文是《Linux设备驱动开发详解:基于最新的Linux 4.0内核 by 宋宝华 》和《Linux设备驱动程序(第三版)》学习的笔记,大部分内容为书籍中的内容。

书籍可直接在微信读书中查看:Linux设备驱动开发详解:基于最新的Linux4.0内核-宋宝华-微信读书 (qq.com)

1 简介

模块(Module)具有以下特点:

  • 模块本身不编译进内核映像
  • 内核加载之后,和其它内核中的部分完全一样。

一个简单的示例:

#include <linux/init.h>     /* 指定初始化和清除函数 */
#include <linux/module.h>   /* 可装载模块的大量符号和函数的定义 */
#include <linux/errno.h>    /* 内核错误编码 */
#include <linux/sched.h>    /* 驱动程序使用的大部分API定义,包括睡眠函数以及各种变量声明 */
#include <generated/utsrelease.h>

MODULE_LICENSE("Dual BSD/GPL");

static char *whom = "world";
static int howmany = 1;

/* 模块参数, 类型:
* bool/invbool: 布尔值, 关联变量应该是int型, invbool类型反转其值
* charp: 字符指针值, 内核分配字符串内存, 并设置指针
* int / long / short / uint / ulong / ushort: 不同长度的基本整数值
*/
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);

int test_func(void)
{
    printk(KERN_ALERT "test function\n");

    return 0;
}

/* 模块初始化函数: 注册模块所提供的任何功能 */
static int hello_init(void)
{
    printk(KERN_ALERT "Hello, world\n");

    /* 当前进程和进程ID */
    printk(KERN_INFO "The process is \"%s\" (pid %i)\n", current->comm, current->pid);

    /* 内核版本 */
    printk(KERN_INFO "Release veriosn:%s\n", UTS_RELEASE);

    printk(KERN_INFO "howmany: %d\n", howmany);
    printk(KERN_INFO "whom: %s\n", whom);

    return 0;
}

static void hello_exit(void)
{
    printk(KERN_ALERT "Goodbye, cruel world\n");
}

/* 将符号test_func导出到模块外部 */
EXPORT_SYMBOL(test_func);

module_init(hello_init);
module_exit(hello_exit);

/* 其它可选描述性定义 */
MODULE_AUTHOR("zhengzhiqianig");
MODULE_DESCRIPTION("Test module");
MODULE_VERSION("v1.0.0");
MODULE_ALIAS("hello-world");

Makefile:

# If KERNELRELEASE is defined, we've been invoked from the
# kernel build system and can use its language.
ifneq ($(KERNELRELEASE),)
	obj-m := hello.o    # 内核构造系统使用的makefile符号, 用来确定当前目录中应构造哪些模块

# Otherwise we were called directly from the command line; 
# invoke the kernel build system.
else
	KERNELDIR ?= /lib/modules/$(shell uname -r)/build
	PWD := $(shell pwd)

default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules clean
endif

编译,并且插入ko:

$ make 
$ sudo insmod hello.ko 
$ dmesg -c            
[  343.865334] Hello, world
[  343.865386] The process is "insmod" (pid 3584)
[  343.865388] Release veriosn:3.10.0-693.el7.x86_64
[  343.865390] howmany: 1
[  343.865391] whom: world

如果在运行于windows系统下的终端仿真器中运行insmod和rmmod,则不会在屏幕上看到任何输出,它可能输出到某个日志系统中,比如/var/log/messages。也可以使用dmesg命令查看。

可以通过lsmod命令查看当前系统插入了哪些ko,lsmod命令查看的结果对应/proc/modules文件,内核中已经加载模块信息也存在于/sys/module目录中:

$ lsmod | grep "hello"
hello                  12885  0 

$ cat /proc/modules | grep "hello"
hello 12885 0 - Live 0xffffffffc072c000 (OE)

$ ls /sys/module/hello/ 
coresize  initsize   notes/  rhelversion  srcversion  uevent
holders/  initstate  refcnt  sections/    taint

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

$ modinfo hello.ko
filename:       /home/grace/Linux_driver/00-modules/hello.ko
alias:          hello-world
version:        v1.0.0
description:    Test module
author:         mrlayfolk
license:        Dual BSD/GPL
rhelversion:    7.4
srcversion:     A55A69AA86AA6EA91186F90
depends:        
vermagic:       3.10.0-693.el7.x86_64 SMP mod_unload modversions 
parm:           howmany:int
parm:           whom:charp

卸载模块命令rmmod:

$ rmmod hello
$ dmesg -c
[  421.318122] Goodbye, cruel world

2 模块程序结构

一个Linux内核模块主要由以下几个部分组成:

(1)模块加载函数

通过insmod或者modprobe命令加载模块时,模块的加载函数会自动被内核执行,完成模块的初始化工作。

(2)模块卸载函数

通过rmmod命令卸载模块时,模块的卸载函数会自动被内核执行,完成模块相关的卸载功能。

(3)模块许可证声明

许可证(LICENSE)声明描述了内核模块的许可权限,如果不申明LICENSE,模块加载时会收到内核被污染(Kernel Tainted)的警告。

[10688.585888] hello: module license 'unspecified' taints kernel.

在Linux内核模块中可接受的LICENSE包括:GPL、GPL v2等。大多数情况下,内核模块应遵循GPL兼容许可证。

(4)模块参数(可选)

模块参数是模块被加载时可以传递给它的值,它本身对应模块内部的全局变量。

(5)模块导出符号(可选)

内核模块可以导出的符号(symbol,对应于函数或变量),若导出,其它模块可以使用本模块的变量或函数。

(6)模块作者等信息声明(可选)

3 模块加载函数

Linux模块加载函数一般以__init标识声明,典型的模块加载函数的形式如下:

static int __init init_func(void)
{
	/* 初始化代码 */
}
module_init(init_func);

模块加载函数module_init(函数名)的方式指定,返回整型值,若初始化成功,返回0;初始化失败,返回错误编码。

Linux内核中,错误码是一个接近0的负值,定义在<lnux/errno.h>中。

Linux内核中,可以使用request_module(const char *fmt, ...)函数加载内核模块,使用方式:

request_module(module_name);

初始化数据可以定义为__initdata,对于只在初始化阶段所需要的数据,内核在初始化完成之后会释放它们占用的内存。

static int hello_data __initdata = 1;
static int __init hello_init(void)
{
    printk(KERN_INFO "Hello world enter %d\n", hello_data);
    return 0;
}
module_init(hello_init);  //内核加载函数

4 模块卸载函数

模块卸载函数一般以__exit标识声明,常见的用法如下:

static void __exit clean_func(void)
{
    /* 释放代码 */
}
module_exit(clean_func);  //内核卸载函数

模块卸载函数在模块卸载的时候执行,不返回任何值,且必须以module_exit(函数名)的方式使用。

5 模块参数

可使用module_param(参数名, 参数类型, 参数读/写权限)为模块定义一个参数。

在加载模块时,用户可以向模块传递参数,形式为:

insmod 模块名 参数名=参数值

如果不传递,参数使用模块定义的缺省值。如果模块被内置无法insmod,在bootloader中可以通过bootargs设置"模块名.参数名=值"的形式给内核的模块传递参数。

参数的类型可以是:

byte short ushort int uint long ulong charp(字符指针) bool invbool(布尔的反)

在模块编译时会将module_param中的声明类型和变量定义的类型进行比较,判断是否一致。

模块也可以有参数数组,形式为:

module_param_array(数组名, 数组类型, 数组长, 参数读/写权限)

模块参数举例:

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

static char *book_name = "dissection Linux Device Driver";
module_param(book_name, charp, S_IRUGO);

static int book_num = 400;
module_param(book_num, int, S_IRUGO);

static int __init hello_init(void)
{
    printk(KERN_INFO "book name:%s\n", book_name);
    printk(KERN_INFO "book  num:%d\n", book_num);

    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("Test Hello");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("A simple Hello World Module");
MODULE_ALIAS("A simple module");

编译并且加载不带参数,可以看出输出的是默认的参数值:

$ make
$ insmod para.ko 
$ dmesg 
[13186.787598] book name:dissection Linux Device Driver
[13186.787601] book  num:400

加载时带参数,打印的是输入的参数:

$ insmod para.ko book_name="LDD3" book_num=500 
$ dmesg
[13423.036831] book name:LDD3
[13423.036835] book  num:500

在/sys/module/模块名/parameters目录下也可以看到模块的参数:

$ ls /sys/module/para/parameters/
book_name  book_num
$ cat /sys/module/para/parameters/book_num 
500
$ cat /sys/module/para/parameters/book_name 
LDD3

6 导出符号

Linux下内核符号表在/proc/kallsyms下,它记录了符号以及符号所在的内存地址:

$ more /proc/kallsyms
ffffffff8109f320 T sys_kill
ffffffff8109f330 T SyS_tgkill

模块可以使用以下方式导出符号到符号表中,导出的符号可以被其它模块使用,使用前需要进行声明:

EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名);  //只适用于包含GPL许可权的模块

测试用例:

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

int add_integer(int a, int b)
{
    return a + b;
}
EXPORT_SYMBOL_GPL(add_integer);

int sub_integer(int a, int b)
{
    return a - b;
}
EXPORT_SYMBOL_GPL(sub_integer);

MODULE_LICENSE("GPL v2");

加载并且查看相关信息:

$ insmod export_symb.ko 
ffffffffc0701000 t add_integer  [export_symb]
ffffffffc0701010 t sub_integer  [export_symb]

7 模块声明与描述

Linux内核模块中,使用以下函数进行声明:

MODULE_AUTHOR  //作者
MODULE_DESCRIPTION //描述
MODULE_VERSION  //版本
MODULE_DEVICE_TABLE  //设备表,对于USB、PCI驱动,表明该驱动模块所支持的设备
MODULE_ALIAS //别名

8 模块的使用计数

Linux2.6之后的模块计数管理接口为:try_module_get(&module)和module_put(&module)。模块的使用计数不用模块自己管理。

/* 用于增加模块使用计数;返回为0,表示调用失败,希望使用的模块没有加载或正在卸载 */
int try_module_get(struct module *module);

/* 用于减少模块的使用计数 */
void module_put(struct module *module);

9 模块的编译

简单的Makefile:

# If KERNELRELEASE is defined, we've been invoked from the
# kernel build system and can use its language.
ifneq ($(KERNELRELEASE),)
	obj-m := hello.o    # 内核构造系统使用的makefile符号, 用来确定当前目录中应构造哪些模块

# Otherwise we were called directly from the command line; 
# invoke the kernel build system.
else
	KERNELDIR ?= /lib/modules/$(shell uname -r)/build
	PWD := $(shell pwd)

default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules clean
endif

该makefile和源码hello.c在同一个目录,运行make命令得到模块hello.ko。

如果需要包括多个.c文件,需要更改:

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

在典型的构造过程中,该makefile将被读取两次。

当makefile从命令行调用时,它注意到KERNELRELEASE没有设置。已安装的模块目录存在一个符号链接,它指向内核的构造树,这样这个makefile就可以定位到内核的源代码目录。

再找到内核代码树之后,这个makefile会调用default目标,这个目标会使用先前描述过的方法第二次运行make命令,第二次读取该makefile文件时,它设置了obj-m,而内核的makefile负责真正构造模块。

快速参考

insmod		# 插入模块
modprobe	# 插入模块
rmmod		# 卸载模块

用户空间工具,加载模块到运行中的内核,以及去除它们.

#include <linux/init.h>
module_init(init_function);
module_exit(cleanup_function);

指定模块的初始化和清理函数的宏定义

__init
__initdata
__exit
__exitdata

函数( __init __exit)和数据 (__initdata __exitdata)的标记,只用在模块初始化或者清理时。为初始化所标识的项可能会在初始化完成后丢弃;退出的项可能被丢弃如果内核没有配置模块卸载。这些标记通过使相关的目标在可执行文件的特定的 ELF 节里被替换来工作。

#include <linux/sched.h>

最重要的头文件中的一个,这个文件包含很多驱动使用的内核 API 的定义, 包括睡眠函数和许多变量声明。

当前进程:

struct task_struct *current;

进程 ID 和 当前进程的命令名:

current->pid
current->comm
obj-m

一个 makefile 符号,内核建立系统用来决定当前目录下的哪个模块应当被建立。

#include <linux/module.h>

必需的头文件,它必须在一个模块源码中包含。

#include <linux/version.h>

头文件,包含在构建的内核版本信息。

LINUX_VERSION_CODE

整型宏定义,对 #ifdef 版本依赖有用

EXPORT_SYMBOL (symbol);
EXPORT_SYMBOL_GPL (symbol);

用来导出单个符号到内核的宏。第二个宏将导出符号的使用限于GPL许可证下的模块。

MODULE_AUTHOR(author);
MODULE_DESCRIPTION(description);
MODULE_VERSION(version_string);
MODULE_DEVICE_TABLE(table_info);
MODULE_ALIAS(alternate_name);

在目标文件中添加关于模块的文档信息

module_init(init_function);
module_exit(exit_function);

宏定义,声明一个模块的初始化和清理函数。

#include <linux/moduleparam.h>
module_param(variable, type, perm);

用来创建模块参数的宏,用户可以在装载模块时调整这些参数的值,其中的类型可以是 bool、charp、 int、invbool、long、short、ushort、uint、ulong、intarray

#include <linux/kernel.h>
int printk(const char * fmt, ...);

函数printf的内核代码

posted @ 2022-01-09 16:24  zhengcixi  阅读(554)  评论(0编辑  收藏  举报
回到顶部