【驱动】linux设备驱动·入门


   驱动程序英文全称Device Driver,也称作设备驱动程序。

   在现代计算机体系结构中,操作系统并不直接于硬件打交道,而是通过驱动程序于硬件通信。

 


   在同一台计算机上,尽管设备是相同的,但是由于操作系统不同,驱动程序是有很大差别的。但是,无论什么都是相似的,可以归纳为下面三点:

    这是驱动程序最基本的功能,初始化通过总线识别设备,访问设备寄存器,按照需求配置设备地端口,设置中断等。

  • 设备驱动程序向操作系统提供了一类设备通用的软件接口,如硬盘设备向操作系统提供了读写磁盘块、寻址等接口,无论是哪种品牌的硬盘驱动向操作系统提供的接口都是一致的。

  • 现代计算机的处理能力越来越强,操作系统有一类虚拟设备驱动,可以模拟真实设备的操作,如虚拟打印机驱动向操作系统提供了打印机的接口,在系统没有打印机制情况下仍然可以执行打印操作。

 


   Linux内核模块是一种

   通过内核模块可以扩展内核的功能,通常内核模块被用于设备驱动、文件系统等。如果没有内核模块,需要向内核添加功能就需要修改代码、重新编译内核、安装新内核等步骤,不仅繁琐而且容易保出错,不易于调试。

 


   内核模块是一个应用程序,但是与普通应用程序有所不同,区别在于:

    内核模块运行在内核空间,可以访问系统的几乎所有的软硬件资源;普通应用程序运行在用户空间,可以访问的资源受到限制。这也是内核模块与普通应用程序最主要的区别。由于内核模块可以获得与操作系统内核相同的权限,因此在编程的时候应该格外注意,可能在用户空间看到的一点小错误在内核空间就会导致系统崩溃。

  • 普通应用程序为了完成某个特定的目标,功能定位明确;内核模块是为其他的内核模块以及应用程序服务的,通常提供的是通用的功能。

  • 内核模块只能调用内核提供的函数,访问其他的函数会导致运行异常;普通应用程序可能调用自身以外的函数,只要能正确连接就有运行。

     

 


程序的并发性。

由于代码的可重入特性,必须考虑到数据结构在多线程环境下不被其他线程破坏,对于共享数据更是应该采用加锁的方法保护。驱动程序员的通常错误是假定某段代码不会出现并发,导致数据被破坏而很难于调试。

应用程序使用虚拟内存,有一个巨大的地址空间,在应用程序中可以分配大块的内存。内核模块可以供使用的内存非常小,最小可能小到一个内存页面(4096字节)。在编写内核模块代码的时候要注意内存的分配和使用。

因此,一个内核模块至少包括加载和卸载两个函数。在linux 2.6系列内核中,通过module_init()宏可以在加载内核模块的时候调用内核模块的初始化函数,module_exit()宏可以在卸载内核模块的时候调用内核模块的卸载函数。

static int __init init_func(void);    //初始化函数
static void __exit exit_func(void);    //清除函数

   这两个函数的名称可以由用户自己定义,但是必须使用规定的返回值和参数格式。

 

 


   kmod模块与用户态的kmodule模块通信,获取内核模块的信息。

   通过insmod命令和modprobe命令都可以加载一个内核模块。

    命令加载内核模块的时候不检查内核模块的符号是否已经在内核中定义。

  • 不仅检查内核模块符号表,而且还会检查模块的依赖关系。

   另外,linux内核可以在需要加载某个模块的时候,通过kmod机制通知用户态的modprobe加载模块。

 

通常,内核输出符号被保存在内核模块列表第一个模块结构里。insmod命令把内核模块加载到虚拟内存,利用内核输出符号表来修改被加载模块中没有解析的内核函数的资源地址。

因为内核模块是工作在内核态的,访问用户态的资源需要做地址转换。申请好空间后,insmod把内核模块复制到新空间,然后把模块加入到内核模块列表的尾部,并且设置模块标志为UNINTIALIZED,表示模块还没有被引用。

 

 


   一个内核模块被其他模块引用的时候,自身的引用计数器会增加1.当卸载模块的时候,需要判断模块引用计数器值是否为0,如果为0才能卸载模块,否则只能把模块计数减1.

   超级用户使用rmmod命令可以卸载指定的模块。

   此外,内核kmod机制会定期检查每个模块的引用计数器,如果某个模块的引用计数器值为0,kmod会卸载该模块。

 


   还是以最经典的"Hello World !"为例子吧。

/* 内核模块: ModuleHelloWorld.c */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("GPL");        
MODULE_AUTHOR("Mystety");         
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
/* init function */
static int __init hello_init(void)        
{
    printk(KERN_ALERT "(init)Hello,World!\n");
    return 0;
}
/* exit function */
static void __exit hello_exit(void)       
{
    printk(KERN_ALERT "(exit)Bye-bye,Mystery!\n");
}
module_init(hello_init);                  
module_exit(hello_exit);

 


   编译内核模块需要建立一个Makefile,主要目的是使用内核头文件,因为内核模块对内核版本有很强的依赖关系。

   ❶我用的系统是Ubuntu的,

sudo apt-get install linux-source

   ❷安装内核代码完毕后,在ModuleHelloWorld.c同一目录下

ifneq ($(KERNELRELEASE),)
    obj-m := ModuleHelloWorld.o
else
    KERNELDIR := /lib/modules/$(shell uname -r)/build
    PWD := $(shell pwd)
default:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

 

   程序

,通过设置KERNELDIR和PWD环境变量,然后通过内核脚本编译当前文件,生成内核模块文件。

   ❸Makefile建立完毕后,在shell下输入""回车编译内核模块。

 

   ❹编译结束后,生成ModuleHelloWorld.ko内核模块,

   在加载过程中可以看到hello_init()函数的输出信息。

   ❺加载内核模块成功后,可以使用

   卸载模块的时候,内核会调用内核的卸载函数,输出hello_exit()函数的内容。

   模块卸载以后,使用,如果没有任何输出,表示HelloWorld内核模块已经被成功卸载。

lsmod | grep ModuleHelloWorld

 


   驱动程序常需要在加载的时候提供一个或者多个参数,内模块提供了设置参数的能力。

   通过odule_param()宏可以为内核模块设置一个参数。

   定义如下:

   其中,

#include <linux/init.h>                                                
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Mystety");
static int initValue = 0;   //模块参数 initValue = <int value>
static char *initName = NULL;   //模块参数 initName = <char*>
module_param(initValue, int, S_IRUGO);
module_param(initName, charp, S_IRUGO);
/* init function */
static int __init hello_init(void)
{
    printk(KERN_ALERT"initValue = %d initName = %s \n",initValue,initName); //打印参数值
    printk(KERN_ALERT "(init)Hello,World!\n");
    return 0;
}
/* exit function */
static void __exit hello_exit(void)
{
    printk(KERN_ALERT "(exit)Bye-bye,Mystery!\n");
}
                                                                       
module_init(hello_init);                                            
module_exit(hello_exit);

   在原来的代码中,重新编译,带参数加载模块。

   从输出结果可以看出,内核模块的参数被正确传递到了程序中。

 


   驱动其实也没有传说中的难,

 

 

本文出自 “成鹏致远” 博客,请务必保留此出处http://infohacker.blog.51cto.com/6751239/1218461

相关内容