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

linux设备驱动linux

   驱动程序英文全称Device Driver,也称做设备驱动程序。驱动程序是用于计算机和外部设备通讯的特殊程序,至关于软件和硬件的接口,一般只有操做系统能使用驱动程序。程序员

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

 


设备驱动介绍编程

   驱动程序是附加到操做系统的一段程序,一般用于硬件通讯。bash

   每种硬件都有本身的驱动程序,其中包含了硬件设备的信息。操做系统经过驱动程序提供的硬件信息与硬件设备通讯。因为驱动设备的重要性,在安装操做系统后须要安装驱动程序,外部设备才能正常工做。数据结构

   Linux内核自带了至关多的设备驱动程序,几乎能够驱动目前主流的各类硬件设备。多线程

   在同一台计算机上,尽管设备是相同的,可是因为操做系统不一样,驱动程序是有很大差异的。可是,不管什么系统驱动程序的功能都是类似的,能够概括为下面三点:并发

  • 初始化硬件设备。函数

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

  • 向操做系统提供统一的软件接口。

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

  • 提供辅助功能。

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

 


Linux内核模块

   Linux内核模块是一种能够被内核动态加载和卸载的可执行程序。

   经过内核模块能够扩展内核的功能,一般内核模块被用于设备驱动、文件系统等。若是没有内核模块,须要向内核添加功能就须要修改代码、从新编译内核、安装新内核等步骤,不只繁琐并且容易保出错,不易于调试。

 


内核模块简介

   Linux内核是一个总体结构,能够把内核想象成一个巨大的程序,各类功能结合在一块儿。当修改和添加新功能的时候,须要从新生成内核,效率较低。

   为了弥补总体式内核的缺点,Linux内核的开发者设计了内核模块机制。

   从代码的角度看,内核模块是一组能够完成某种功能的函数集合。

   从执行的角度看,内核模块能够看作是一个已经编译可是没有链接的程序。

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

  • 运行环境不一样。

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

  • 功能定位不一样。

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

  • 函数调用方式不一样。

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

     

 


内核模块的结构

   内核编程与用户空间编程最大的区别就是程序的并发性

   在用户空间,除多线程应用程序外,大部分应用程序的运行是顺序执行的,在程序执行过程当中没必要担忧被其余程序改变执行的环境。而内核的程序执行环境要复杂的多,即时最简单的内核模块也要考虑到并发执行的问题。

   设计内核模块的数据结构要十分当心。因为代码的可重入特性,必须考虑到数据结构在多线程环境下不被其余线程破坏,对于共享数据更是应该采用加锁的方法保护。驱动程序员的一般错误是假定某段代码不会出现并发,致使数据被破坏而很难于调试。

   linux内核模块使用物理内存,这点与应用程序不一样。应用程序使用虚拟内存,有一个巨大的地址空间,在应用程序中能够分配大块的内存。内核模块能够供使用的内存很是小,最小可能小到一个内存页面(4096字节)。在编写内核模块代码的时候要注意内存的分配和使用。

   内核模块至少支持加载和卸载两种操做。所以,一个内核模块至少包括加载和卸载两个函数。在linux 2.6系列内核中,经过module_init()宏能够在加载内核模块的时候调用内核模块的初始化函数,module_exit()宏能够在卸载内核模块的时候调用内核模块的卸载函数。

   内核模块的初始化和卸载函数是有固定格式的。

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

   这两个函数的名称能够由用户本身定义,可是必须使用规定的返回值和参数格式。

    • static修饰符的做用是函数仅在当前文件有效,外部不可见;

    • __init关键字告诉编译器,该函数代码在初始化完毕后被忽略;

    • __exit关键字告诉编译器,该代码仅在卸载模块的时候被调用;

 

 


内核模块的加载

   linux内核提供了一个kmod的模块用来管理内核模块。

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

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

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

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

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

 

   使用insmod加载内核模块的时候,首先使用特权级系统调用查找内核输出的符号。一般,内核输出符号被保存在内核模块列表第一个模块结构里。insmod命令把内核模块加载到虚拟内存,利用内核输出符号表来修改被加载模块中没有解析的内核函数的资源地址。

   修改完内核模块中的函数和资源地址后,insmod使用特权指令申请存放内核模块的空间。由于内核模块是工做在内核态的,访问用户态的资源须要作地址转换。申请好空间后,insmod把内核模块复制到新空间,而后把模块加入到内核模块列表的尾部,而且设置模块标志为UNINTIALIZED,表示模块尚未被引用。insmod使用特权指令告诉内核新增长的模块初始化和清除函数的地址,供内核调用。

 

 


内核模块的卸载

   卸载的过程相对于加载要简单,主要问题是对模块引用计数的判断。

   一个内核模块被其余模块引用的时候,自身的引用计数器会增长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的,首先在系统命令行shell下安装当前版本的linux内核源代码

sudo apt-get install linux-source

   编译内核模块不须要从新编译内核代码,但前提是须要使用当前内核版本相同的代码。

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

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

 

   程序第1行检查是否认义了KERNELRELEASE环境变量,若是定义则表示该模块是内核代码的一部分,直接把模块名称添加到 obj-m环境变量便可;

   若是未定义环境变量,表示在内核代码之外编译,经过设置KERNELDIR和PWD环境变量,而后经过内核脚本编译当前文件,生成内核模块文件。

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

 

   ❹编译结束后,生成ModuleHelloWorld.ko内核模块,经过modprobe或者insmod加载内核模块。

   在加载过程当中能够看到hello_init()函数的输出信息。

   ❺加载内核模块成功后,可使用rmmod命令卸载内核模块。

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

   模块卸载之后,使用lsmod命令查看模块列表,若是没有任何输出,表示HelloWorld内核模块已经被成功卸载。

lsmod | grep ModuleHelloWorld

 


为内核模块添加参数

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

   经过module_param()宏能够为内核模块设置一个参数。

   定义以下:module_param(参数名称,类型,属性)

   其中,参数名称是加载内核模块时使用的参数名称,在内核模块中须要有一个同名的变量与之对应;类型是参数的类型,内核支持C语言经常使用的基本类型属性是参数的访问权限。

#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);

   在原来的代码中,增长了两个变量initValue和initName,分别是int类型和char*类型;而后在第8行设置initValue为int类型的参数,第9行设置initName为char*类型的参数。从新编译,带参数加载模块。

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

 


总结    

   驱动其实也没有传说中的难,关键是须要动手去实践,相信本身,什么均可以!

 

 

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