最近在搞一个linux的项目,其中主要是在编写一些应用模块,对内核及其驱动模块涉及很少,遇到了一些驱动模块的问题时,临时查了些资料,大致了解了一下驱动模块开发的基本步骤和常规步骤,并从网上也收集到了一些相关的资料,于是对其进行了一下简单的总结,记录于此,便于日后查阅,并与同道中人共享。

零、什么是linux内核驱动模块

Linux内核的整体结构已经非常庞大,而其包含的组件也非常多。我们怎样把需要的部分都包含在内核中呢?

一种方法是把所有需要的功能都编译到Linux内核。这会导致两个问题,一是生成的内核会很大,二是如果我们要在现有的内核中新增或删除功能,将不得不重新编译内核。

有没有一种机制使得编译出的内核本身并不需要包含所有功能,而在这些功能需要被使用的时候,其对应的代码被动态地加载到内核中呢?

答案是肯定的,Linux提供了这样的一种机制,这种机制被称为模块(Module)。模块具有这样的特点:

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

那么,问题来了。如何编写内核驱动模块呢?别急,我们一步一步来介绍。

一、先从一个最简单的例子入手

先来看一个最简单的内核模块“Hello World”。

#include <linux/init.h>
#include <linux/module.h>MODULE_LICENSE("Dual BSD/GPL");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_AUTHOR("Song Baohua");
MODULE_DESCRIPTION("A simple Hello World Module");
MODULE_ALIAS("a simplest module");

这个最简单的内核模块只包含内核模块加载函数、卸载函数和对Dual BSD/GPL许可权限的声明以及一些描述信息。编译它会产生hello.ko目标文件,通过“insmod ./hello.ko”命令可以加载它,通过“rmmod hello”命令可以卸载它,加载时输出“Hello World enter”,卸载时输出“Hello World exit”。

内核模块中用于输出的函数是内核空间的printk()而非用户空间的printf(),printk()作为一种最基本的内核调试手段,其用法和printf()基本相似,但可定义输出级别。

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

在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

lsmod命令实际上读取并分析“/proc/modules”文件,与上述lsmod命令结果对应的“/proc/modules”文件如下:

root@imx6:~$ cat /proc/modules
hello        1568        0     -          Live    0xc8859000
ohci1394     32716       0     -          Live    0xc88c8000
ieee1394     94420       1     ohci1394,  Live    0xc8840000
ide_scsi     16708       0     -          Live    0xc883a000
ide_cd       39392       0     -          Live    0xc882f000
cdrom        36960       1     ide_cd,    Live    0xc8876000

内核中已加载模块的信息也存在于/sys/module目录下,加载hello.ko后,内核中将包含/sys/module/hello目录,该目录下又包含一个refcnt文件和一个sections目录,在/sys/module/hello目录下运行tree –a得到如下目录树:

root@imx6:~$  tree -a
.
|-- refcnt
`-- sections|-- .bss|-- .data|-- .gnu.linkonce.this_module|-- .rodata|-- .rodata.str1.1|-- .strtab|-- .symtab|-- .text`-- __versions

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

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

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:   

二、模块程序的基本结构

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

  • 模块加载函数一般需要
    当通过insmod或modprobe命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。
  • 模块卸载函数一般需要
    当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块卸载函数相反的功能。
  • 模块许可证声明必须
    许可证(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。
  • 模块参数可选
    模块参数是模块被加载的时候可以被传递给它的值,它本身对应模块内部的全局变量。
  • 模块导出符号可选
    内核模块可以导出符号(symbol,对应于函数或变量),这样其它模块可以使用本模块中的变量或函数。
  • 模块作者等信息声明可选
    用于申明模块作者的相关信息,一般用于备注作者姓名、邮箱等。

1、内核模块的加载函数

模块加载函数Linux内核模块加载函数宜被以__init标识声明,典型的模块加载函数的形式如下所示:

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

模块加载函数必须以“module_init(函数名)”的形式被指定。它返回整型值,若初始化成功,应返回0。而在初始化失败时,应该返回错误编码errno【在Linux内核里,错误编码errno是一个负值,在头文件”linux/errno.h”中定义,包含-ENODEV、-ENOMEM之类的符号值】。总是返回相应的错误编码是种非常好的习惯,因为只有这样,用户程序才可以利用perror等方法把它们转换成有意义的错误信息字符串。

在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等)。

2、内核模块的卸载函数

Linux内核模块加载函数宜被以__exit标识声明,典型的模块卸载函数的形式如下所示:

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

模块卸载函数在模块卸载的时候执行,不返回任何值,必须以“module_exit(函数名)”的形式来指定。

通常来说,模块卸载函数要完成与模块加载函数相反的功能,例如:

  • 若模块加载函数注册了XXX,则模块卸载函数应该注销XXX;
  • 若模块加载函数动态申请了内存,则模块卸载函数应释放该内存;
  • 若模块加载函数申请了硬件资源(中断、DMA通道、I/O端口和I/O内存等)的占用,则模块卸载函数应释放这些硬件资源;
  • 若模块加载函数开启了硬件,则卸载函数中一般要关闭之;

和__init一样,__exit也可以使对应函数在运行完成后自动回收内存。实际上,__init和__exit都是宏,其定义分别为:

#define __init        __attribute__ ((__section__ (".init.text")))#ifdef MODULE
#define __exit        __attribute__ ((__section__(".exit.text")))
#else
#define __exit        __attribute_used__ __attribute__ ((__section__(".exit.text")))
#endif

数据也可以被定义为__initdata和__exitdata,这两个宏分别为:

#define __initdata   __attribute__ ((__section__ (".init.data")))
和
#define __exitdata  __attribute__ ((__section__(".exit.data")))

3、内核模块的参数传递

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

static char *book_name = "深入浅出Linux设备驱动";
static int num = 4000;
module_param(num, int, S_IRUGO);
module_param(book_name, charp, S_IRUGO);

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

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

模块被加载后,在/sys/module/目录下将出现以此模块名命名的目录。当“参数读/写权限”为0时,表示此参数不存在sysfs文件系统下对应的文件节点,如果此模块存在“参数读/写权限”不为0的命令行参数,在此模块的目录下还将出现parameters目录,包含一系列以参数名命名的文件节点,这些文件的权限值就是传入module_param()的“参数读/写权限”,而文件的内容为参数的值。

除此之外,模块也可以拥有参数数组,形式为“module_param_array(数组名,数组类型,数组长,参数读/写权限)”。从2.6.0至2.6.10 版本,须将数组长变量名赋给“数组长”,从2.6.10 版本开始,须将数组长变量的指针赋给“数组长”,当不需要保存实际输入的数组元素个数时,可以设置“数组长”为NULL。

运行insmod或modprobe命令时,应使用逗号分隔输入的数组元素。

4、内核模块的符号导出

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

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

导出的符号将可以被其它模块使用,使用前声明一下即可。EXPORT_SYMBOL_GPL()只适用于包含GPL许可权的模块。下面的代码给出了一个导出整数加、减运算函数符号的内核模块的例子(这些导出符号毫无实际意义,仅仅只是为了演示)。

#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");                                int add_integar(int a,int b)
{                                return a+b;
} int sub_integar(int a,int b)
{                                return a-b;
}                            EXPORT_SYMBOL(add_integar);
EXPORT_SYMBOL(sub_integar);

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。

6、内核模块的编译方法

我们可以为代码清单4.1的模板编写一个简单的Makefile:

obj-m := hello.o

并使用如下命令编译Hello World模块:

make -C /usr/src/linux-2.6.15.5/ M=/driver_study/ modules

如果当前处于模块所在的目录,则以下命令与上述命令等效:

make –C /usr/src/linux-2.6.15.5 M=$(pwd) modules

C后指定的是Linux内核源代码的目录,而M=后指定的是hello.c和Makefile所在的目录,编译结果如下:

root@imx6:~$ make -C /usr/src/linux-2.6.15.5/ M=/driver_study/ modules
make: Entering directory `/usr/src/linux-2.6.15.5'CC/driver_study/hello.o
/driver_study/hello.c:18:35: warning: no newline at end of fileBuilding modules, stage 2.MODPOSTCC      /driver_study/hello.mod.o
  LD
  /driver_study/hello.ko
make: Leaving directory `/usr/src/linux-2.6.15.5'

从中可以看出,编译过程中,经历了这样的步骤:先进入Linux内核所在的目录,并编译出hello.o文件,运行MODPOST会生成临时的hello.mod.c文件,而后根据此文件编译出hello.mod.o,之后连接hello.o和hello.mod.o文件得到模块目标文件hello.ko,最后离开Linux内核所在的目录。

7、实例解析

现在我们定义一个包含2个参数的模块,并观察模块加载时被传递参数和不传递参数时的输出。

#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");static char *book_name = "dissecting Linux Device Driver";
static int num = 4000;static int book_init(void)
{printk(KERN_INFO " book name:%s\n",book_name);printk(KERN_INFO " book num:%d\n",num);return 0;
}static void book_exit(void)
{printk(KERN_INFO " Book module exit\n ");
}module_init(book_init);
module_exit(book_exit);
module_param(num, int, S_IRUGO);
module_param(book_name, charp, S_IRUGO);MODULE_AUTHOR("Song Baohua, author@linuxdriver.cn");
MODULE_DESCRIPTION("A simple Module for testing module params");
MODULE_VERSION("V1.0");

对上述模块运行“insmod book.ko”命令加载,相应输出都为模块内的默认值,通过察看“/var/log/messages”日志文件可以看到内核的输出:

root@imx6:~$ tail -n 2 /var/log/messages
Jul  2 01:03:10 localhost kernel:  <6> book name:dissecting Linux Device Driver
Jul  2 01:03:10 localhost kernel:  book num:4000

当用户运行“insmod book.ko book_name=’GoodBook’ num=5000”命令时,输出的是用户传递的参数:

root@imx6:~$ tail -n 2 /var/log/messages
Jul  2 01:06:21 localhost kernel:  <6> book name:GoodBook
Jul  2 01:06:21 localhost kernel:  book num:5000

三、总结

本文主要讲解了Linux内核模块的概念和基本的编程方法。内核模块由加载/卸载函数、功能函数以及一系列声明组成,它可以被传入参数,也可以导出符号供其它模块使用。

由于Linux设备驱动以内核模块的形式而存在,因此,掌握上述内容是编写任何类型设备驱动的必须。在具体的设备驱动开发中,将驱动编译为模块也有很强的工程意义,因为如果将正在开发中的驱动直接编译入内核,而开发过程中会不断修改驱动的代码,则需要不断的编译内核并重启Linux,但是如果编译为模块,则只需要rmmod并insmod即可,开发效率为大为提高。

linux内核驱动模块开发步骤及实例入门介绍相关推荐

  1. linux内核驱动模块开发makefile实例解析

    昨天整理了一篇关于linux内核驱动模块的开发介绍入门,其中介绍了一些关于驱动模块的基本开发步骤,不过面广而不深,很多细节都没有涉及到,其中就包括如何编写驱动模块的makefile.那么,今天我们就来 ...

  2. 嵌入式 Linux 内核驱动开发【The first day: 36093万字】

    嵌入式 Linux 内核驱动开发[1] 嵌入式 Linux 内核驱动开发前言 第1章 Linux 内核裁剪和定制 [1]Linux 内核开发简介 [2] Linux 源码阅读工具 [1.2.1]Sou ...

  3. xmake v2.6.2 发布,新增 Linux 内核驱动模块构建支持

    Xmake 是一个基于 Lua 的轻量级跨平台构建工具. 它非常的轻量,没有任何依赖,因为它内置了 Lua 运行时. 它使用 xmake.lua 维护项目构建,相比 makefile/CMakeLis ...

  4. 《Linux内核驱动模块编程指南》

    Foreword Table of Contents 作者声明 版本和注意 感谢 译者注 作者声明 <Linux内核驱动模块编程指南>最初是由Ori Pomerantz为2.2版本的内核编 ...

  5. 树莓派基于Linux内核驱动开发详解

    一.驱动认知 首先理解Linux内核框图 文件系统认知,Linux内核框图 1.什么是驱动 linux内核驱动.软件层面上的驱动 广义上是指:这一段代码操作了硬件去动,所以这一段代码就叫硬件的驱动程序 ...

  6. linux 内核驱动开发

    一.为什么要学习内核? 有些人要学习内核,而有些人则可以不学习它.你如果以后要从事系统研发或驱动开发的话,就要学习内核. 刚刚接触内核,主要学习内核的接口函数.不要深入的去读内核,因为你读也读不懂,内 ...

  7. linux内核移植开发板,mini2440学习之(一)-------移植linux内核

    请 登录 后使用快捷导航 没有帐号?注册 阅 4622|回 2 最后登录2013-8-22 在线时间22 小时 威望2878分 芯积分608分(兑换) E金币好友 发表于2009-9-22 15:02 ...

  8. 树莓派基于Linux内核驱动开发

    一.驱动认知 1.1 为什么要学习写驱动 树莓派开发简单是因为有厂家提供的wiringPi库,实现超声波,实现继电器操作,做灯的点亮-都非常简单. 但未来做开发时,不一定都是用树莓派,则没有wirin ...

  9. Linux内核驱动开发(一)

    Linux内核初探 linux操作系统历史 开发模式 git 分布式管理 git clone 获取 git push 提交 git pull 更新 邮件组 mailing list patch 内核代 ...

最新文章

  1. 【Android】Android中判断后台服务是否正在运行
  2. 越秀人民币夹层二期完成首轮关账 首期募集近10亿元...
  3. 因滚动条出现而导致页面晃动的解决方案
  4. 10行Python代码自动清理电脑内重复文件,解放双手!
  5. IOI 2007 Sail (线段树+贪心)
  6. Zabbix如何监控Windows机器
  7. 膜拜大佬!不同层级的Android开发者的不同行为,社招面试心得
  8. Spring 容器:三种方式解决 Resource leak: ‘applicationContext‘ is never closed 问题
  9. 设计模式-UML关系基础
  10. 年度盘点 | 2016年中国云计算十大新闻
  11. Tomcat,Jboss,Weblogic通过jndi连接数据库
  12. 第一篇:手把手教你移植任天堂,没有声音、无需外置SD卡、可使用独立按键也可使用外置手柄,本人使用的芯片为ESP32,移植到STM32均可使用。(本篇主要介绍nes_main.h这个文件)
  13. mysql用户和权限管理总结_【转】mysql 用户及权限管理 小结
  14. Python实战 | 手拉手教你爬取贝壳房源数据
  15. IPv6规模部署及专项督查工作全国电视电话会议
  16. Windows10 Windows Store安装 应用商店重新安装
  17. 数据库相关基础知识总结
  18. 跨考电子信息408一点经验
  19. Unity 光照阴影烘焙
  20. HTML 页面 DIV 边框圆角

热门文章

  1. IOS工程自动打包并发布脚本实现
  2. php 调用 perl,perl中如何调用R语言
  3. (二)流--递归算法
  4. UWP 开发初阶 Chapter 6 - 简单介绍如何使用 C# 改变 XAML 控件的属性
  5. 如何通过组件化提高开发效率?
  6. 修改Linux系统日期与时间date clock
  7. MusicXML 3.0 (4) - 谱号
  8. ubuntu下安装mysql 杂记
  9. java如何获得当前文件路径
  10. git提取和拉取的区别_git fetch和git pull的区别