Linux内核驱动模块

Linux内核驱动模块

Linux 设备驱动会以内核模块的形式出现,因此,学会编写 Linux 内核模块编程是学习 Linux 设备驱动的先决条件。

1、Linux 内核模块简介

Linux 内核的整体结构已经非常庞大,而其包含的组件也非常多。这会导致两个问题,一是生成的内核会很大,二是如果我们要在现有的内核中新增或删除功能,将不得不重新编译内核。Linux 提供了这样的一种机制,这种机制被称为模块(Module)。使得编译出的内核本身并不需要包含所有功能,而在这些功能需要被使用的时候,其对应的代码被动态地加载到内核中。

2、初识Linux内核模块

先来看一个最简单的内核模块“Hello World”,代码如下:

#include <linux/init.h>
#include <linux/module.h>
​
static int hello_init(void)     /*初始化函数*/
{
    printk(KERN_INFO " Hello World enter\n");
    return 0;
}
​
static void hello_exit(void)        /*卸载函数*/
{
    printk(KERN_INFO " Hello World exit\n ");
}
​
module_init(hello_init);    /*模块初始化*/
module_exit(hello_exit);    /*卸载模块*/
​
MODULE_LICENSE("Dual BSD/GPL");     /*许可声明*/
​
MODULE_AUTHOR("Barry Song <21cnbao@gmail.com>");
MODULE_DESCRIPTION("A simple Hello World Module");
MODULE_ALIAS("a simplest module");

这个模块定义了两个函数, 一个在模块加载到内核时被调用( hello_init )以及一个在模块被去除时被调用( hello_exit ). moudle_init 和 module_exit 这几行使用了特别的内核宏来指出这两个函数的角色. 另一个特别的宏 (MODULE_LICENSE) 是用来告知内核, 该模块带有一个自由的许可证.

注:内核模块中用于输出的函数是内核空间的 printk()而非用户空间的 printf(),具体用法参考附件 printk函数介绍。

3、几个常用命令

3.1 加载模块

通过“insmod ./hello.ko”命令可以加载,加载时输出“Hello World enter”。

3.2 卸载模块

通过“rmmod hello”命令可以卸载,卸载时输出“Hello World exit”。

3.3 查看系统中已经加载的模块列表

在Linux中,使用lsmod命令可以获得系统中加载了的所有模块以及模块间的依赖关系,例如:

root@imx6:~$  lsmod
Module             Size         Used by
hello              1568         0 
ohci1394           32716        0 
ide_scsi           16708        0 
ide_cd             39392        0 
cdrom              36960        1 ide_cd

3.4 查看某个具体模块的详细信息

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

root@imx6:~$ modinfo hello.ko
filename:       hello.ko
license:        Dual BSD/GPL
author:         Song Baohua
description:    A simple Hello World Module
alias:          a simplest module
vermagic:       2.6.15.5 686 gcc-3.2
depends:   

 

4、Linux 内核模块程序的结构

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

1、模块加载函数(一般需要) 当通过insmod或modprobe命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。 2、模块卸载函数(一般需要) 当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块卸载函数相反的功能。 3、模块许可证声明(必须) 许可证(LICENSE)声明描述内核模块的许可权限,如果不声明LICENSE,模块被加载时,将收到内核被污染 (kernel tainted)的警告。在Linux 2.6内核中,可接受的LICENSE包括“GPL”、“GPL v2”、“GPL and additional rights”、“Dual BSD/GPL”、“Dual MPL/GPL”和“Proprietary”。大多数情况下,内核模块应遵循GPL兼容许可权。Linux 2.6内核模块最常见的是以MODULE_LICENSE( “Dual BSD/GPL” )语句声明模块采BSD/GPL双LICENSE。 4、模块参数(可选) 模块参数是模块被加载的时候可以被传递给它的值,它本身对应模块内部的全局变量。 5、模块导出符号(可选) 内核模块可以导出符号(symbol,对应于函数或变量),这样其它模块可以使用本模块中的变量或函数。 6、模块作者等信息声明(可选) 用于申明模块作者的相关信息,一般用于备注作者姓名、邮箱等。

4.1 模块加载函数

Linux 内核模块加载函数一般以_ _init 标识声明,典型的模块加载函数如下:

static int _ _init initialization_function(void)
{
/* 初始化代码 */
}
module_init(initialization_function);

模块加载函数必须以“module_init(函数名)”的形式被指定。它返回整型值,若初始化成功,应返回 0。而在初始化失败时,应该返回错误编码。在 Linux 内核里,错误编码是一个负值。

在 Linux 2.6 内核中,可以使用 request_module(const char *fmt, ...)函数加载内核模块,驱动开发人员可以通过调用。

request_module(module_name);
/**** 或者 ****/
request_module("char-major-%d-%d", MAJOR(dev), MINOR(dev));

注意:在 Linux 中,所有标识为_ init 的函数在连接的时候都放在.init.text 这个区段内,此外,所有的 init 函数在区段.initcall.init 中还保存了一份函数指针,在初始化时内核会通过这些函数指针调用这些 _init 函数,并在初始化完成后,释放 init 区段(包括.init.text、.initcall.init 等)。

4.2 模块卸载函数

static void _ _exit cleanup_function(void)
{
/* 释放代码 */
}
module_exit(cleanup_function);

模块卸载函数在模块卸载的时候执行,不返回任何值,必须以“module_exit(函数名)”的形式来指定。通常来说,模块卸载函数要完成与模块加载函数相反的功能,如下所示。

  • 若模块加载函数注册了 XXX,则模块卸载函数应该注销 XXX。

  • 若模块加载函数动态申请了内存,则模块卸载函数应释放该内存。

  • 若模块加载函数申请了硬件资源(中断、DMA 通道、I/O 端口和 I/O 内存等)的占用,则模块卸载函数应释放这些硬件资源。

  • 若模块加载函数开启了硬件,则卸载函数中一般要关闭之。

4.3 模块参数

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

static char *book_name = " dissecting Linux Device Driver ";
static int num = 4 000;
module_param(num, int, S_IRUGO);
module_param(book_name, charp, S_IRUGO);

参数类型可以是 byte、short、ushort、int、uint、long、ulong、charp(字符指针)、bool 或 invbool(布尔的反),在模块被编译时会将 module_param 中声明的类型与变量定义的类型进行比较,判断是否一致。

在装载内核模块时,用户可以向模块传递参数,形式为“insmode(或 modprobe)模块名 参数名=参数值”,如果不传递,参数将使用模块内定义的缺省值。

4.4 内核模块的符号导出

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

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

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

4.5 模块声明与描述

在Linux内核模块中,我们可以用MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION、MODULE_DEVICE_TABLE、MODULE_ALIAS分别声明模块的作者、描述、版本、设备表和别名,例如:

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

对于USB、PCI等设备驱动,通常会创建一个MODULE_DEVICE_TABLE。

 

5、Linux 内核模块的编译

5.1 makefile

Linux 内核模块的编译需要编写makefile文件,具体的makefile文件编写介绍详见附件。

5.2 内核模块的makefile文件

编写一个简单的 Makefile:

KVERS = $(shell uname -r) #显示内核版本号
​
# Kernel modules
obj-m += hello.o
​
# Specify flags for the module compilation.
#EXTRA_CFLAGS=-g -O0
​
build: kernel_modules
kernel_modules:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules #modules表示编译成模块的意思
#CURDIR是make的内嵌变量,自动设置为当前目录
​
clean:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean

该 Makefile 文件应该与源代码 hello.c 位于同一目录,开启其中的 EXTRA_CFLAGS=-g -O0可以得到包含调试信息的 hello.ko 模块。运行 make 命令得到的模块可直接在 PC 上运行。

注:uname 的更多用法详见附件

如果一个模块包括多个.c 文件(如 file1.c、file2.c),则应该以如下方式编写 Makefile:

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

obj-m是个makefile变量,它的值可以是一串.o文件的表列。

 

 

 

 

 

附件

1、printk函数介绍

printk的用法

内核通过 printk() 输出的信息具有日志级别,日志级别是通过在 printk() 输出的字符串前加一个带尖括号的整数来控制的,如 printk("<6>Hello, world!\n");。内核中共提供了八种不同的日志级别,在 linux/kernel.h 中有相应的宏对应。

#define KERN_EMERG   "<0>"   /* system is unusable */
#define KERN_ALERT   "<1>"   /* action must be taken immediately */
#define KERN_CRIT     "<2>"   /* critical conditions */
#define KERN_ERR      "<3>"   /* error conditions */
#define KERN_WARNING "<4>"   /* warning conditions */
#define KERN_NOTICE   "<5>"   /* normal but significant */
#define KERN_INFO     "<6>"   /* informational */
#define KERN_DEBUG    "<7>"   /* debug-level messages */

所以 printk() 可以这样用:printk(KERN_INFO "Hello, world!\n");。未指定日志级别的 printk() 采用的默认级别是 DEFAULT_MESSAGE_LOGLEVEL,这个宏在 kernel/printk.c 中被定义为整数 4,即对应KERN_WARNING。

在头文件中共定义了八个可用的记录级;我们下面按其严重性倒序列出: KERN_EMERG Used for emergency messages, usually those that precede a crash. 用于突发性事件的消息,通常在系统崩溃之前报告此类消息。 KERN_ALERT A situation requiring immediate action. 在需要立即操作的情况下使用此消息。 KERN_CRIT Critical conditions, often related to serious hardware or software failures. 用于临界条件下,通常遇到严重的硬软件错误时使用此消息。 KERN_ERR Used to report error conditions; device drivers often use KERN_ERR to report hardware difficulties. 用于报告错误条件;设备驱动经常使用KERN_ERR报告硬件难题。 KERN_WARNING Warnings about problematic situations that do not, in themselves, create serious problems with the system. 是关于问题状况的警告,一般这些状况不会引起系统的严重问题。 KERN_NOTICE Situations that are normal, but still worthy of note. A number of security-related conditions are reported at this level. 该级别较为普通,但仍然值得注意。许多与安全性相关的情况会在这个级别被报告。 KERN_INFO Informational messages. Many drivers print information about the hardware they find at startup time at this level. 信息消息。许多驱动程序在启动时刻用它来输出获得的硬件信息。

KERN_DEBUG

Used for debugging messages.

用于输出调试信息

每一个字符串(由宏扩展而成)表示了尖括号内的一个整数。数值范围从0到7,数值越小,优先级越高。

2、makefile介绍

2.1 简介

一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为 makefile就像一个Shell脚本一样,也可以执行操作系统的命令

编写Makefile 的好处是能够使用一行命令来完成“自动化编译”,一旦提供一个(通常对于一个工程来说会是多个)正确的 Makefile。编译整个工程你所要做的事就是在shell 提示符下输入make命令。整个工程完全自动编译,极大提高了效率。

2.2 具体操作

1.makefile的命名(两种)

  1. makefile

  2. Makefile

    注:在同一个工程文件夹下建立名为makefile的txt文件。

2. makefile的规则

规则的三个要素:目标、依赖、命令

  格式:   目标:依赖   tab 命令

3. 多文件的makefile的编写

makefile可以有多个规则,当第一个规则的的命令在执行的时候发现没有相应的依赖,就在下面的规则中找。最上面的规则的目标是终极目标一定写在最上面,也就是最后要生成的文件。

4.makefile中的变量

  1. 自定义变量 obj=main.o add.o sub.o 引用的时候直接使用 $(obj)

  2. 自动变量(只能在规则的命令中使用(第二行)) $< : 规则中的第一个依赖 $@:规则中的目标 $^: 规则中所有的依赖

  3. 模式自动匹配 子规则中: 目标:依赖   %.o:%.c 自动匹配终极目标的依赖 :                    main.o:main.c                    add.o:add.c                     sub.o:sub.c

  4. makefile维护的变量(通常大写,自己可以修改) CC:cc(即gcc) APPFLAGS:预处理使用的选项 CFLAGS:编译的时候使用的选项 LDFLAGS:链接库使用的选项

5.makefile中的函数(都是有返回值)

  1. wildcard 查找当前目录下所有.c文件,返回值给src src=$(wildcard ./*.c)

  2. patsubst 替换所有.c文件为.o文件 obj=$(patsubst ./%.c, ./%.o, $(src))

6.make clean

在makefile最后加入clean的目标,为了重新编译所有文件得删除原来生成的文件

7.最终的简单的makefile

objs = hello1.o hello2.o  
#src=$(wildcard ./*.c)  #查找该目录下所有的.c文件
#objs=$(patsubst ./%.c, ./%.o, $(src))  #将.o改为.c
​
target = hello  
#CC=gcc
​
$(target) : $(objs)     #链接,将hello1.o hello2.o 链接为hello可执行程序
    gcc $(objs) -o $(target)
​
%.o : %.c           #编译,将.c文件编译为.o文件
    gcc -c $< -o $@
​
.PHONY : clean
clean :
    rm $(objs) $(target) -f

 

3、uname

常用命令uname -v # uname -i #uname -a

    dream361@master:~$ uname -n    #主机名称
    master
    dream361@master:~$ uname -v    #操作系统版本
    #-Ubuntu SMP Fri Mar  :: UTC
    dream361@master:~$ uname -r    #内核版本号
    --generic
    dream361@master:~$ uname -s    #内核名称
    Linux
    dream361@master:~$ uname -p    #CPU类型
    x86_64
    dream361@master:~$ uname -i    #硬件平台
    x86_64
    dream361@master:~$ uname -o    #操作系统名
    GNU/Linux
    dream361@master:~$ uname -m    #主机的硬件名
    x86_64
    dream361@master:~$ uname -a    #显示所有信息
    Linux master --generic #-Ubuntu SMP Fri Mar  :: UTC  x86_64 x86_64 x86_64 GNU/Linux
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值