生活百科
linux驱动开发教程(基本知识简介)
2022-11-06 20:51

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

什么是linux内核驱动模块

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

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

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

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

模块本身不被编译入内核映像,这控制了内核的大小。

模块一旦被加载,它就和内核中的其它部分完全一样。

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

文章福利】小编自己整理了一些个人觉得比较好的linux内核学习书籍、视频资料共享在群文件里面,有需要的可以私信【内核】自行添加免费领取哦!!!(含视频教程、电子书、实战项目及代码)

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

先来看一个最简单的内核模块“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 entern");
    return 0;
}

static void hello_exit(void)
{
    printk(KERN_INFO " Hello World exitn ");
}

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 file
  Building modules, stage 2.
  MODPOST
  CC      /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:%sn",book_name);
  printk(KERN_INFO " book num:%dn",num);
  return 0;
}

static void book_exit(void)
{
  printk(KERN_INFO " Book module exitn ");
}

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, [email protected]");
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即可,开发效率为大为提高。

发表评论
0评