linux设备驱动

驱动程序英文全称Device Driver,也称作设备驱动程序。驱动程序是用于计算机和外部设备通信的特殊程序,相当于软件和硬件的接口,通常只有操作系统能使用驱动程序。

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


设备驱动介绍

驱动程序是附加到操作系统的一段程序,通常用于硬件通信。

每种硬件都有自己的驱动程序,其中包含了硬件设备的信息。操作系统通过驱动程序提供的硬件信息与硬件设备通信。由于驱动设备的重要性,在安装操作系统后需要安装驱动程序,外部设备才能正常工作。

Linux内核自带了相当多的设备驱动程序,几乎可以驱动目前主流的各种硬件设备。

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

  • 初始化硬件设备。

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

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

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

  • 提供辅助功能。

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


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

【驱动】linux设备驱动·入门的更多相关文章

  1. linux设备驱动归纳总结

    前言: (总结已经基本写完,这段时间我会从新排版和修正.错误总会有的,望能指正!) 前段时间学习了嵌入式驱动,趁着没开始找工作,这段时间我会每天抽出时间来复习. 我的总结是根据学习时的笔记(李杨老师授 ...

  2. 【Linux】linux设备驱动归纳总结

    前言: (总结已经基本写完,这段时间我会从新排版和修正.错误总会有的,望能指正!) 前段时间学习了嵌入式驱动,趁着没开始找工作,这段时间我会每天抽出时间来复习. 我的总结是根据学习时的笔记(李杨老师授 ...

  3. 字符设备驱动、平台设备驱动、设备驱动模型、sysfs的比较和关联

    转载自:http://www.kancloud.cn/yueqian_scut/emlinux/106829 学习Linux设备驱动开发的过程中自然会遇到字符设备驱动.平台设备驱动.设备驱动模型和sy ...

  4. [kernel]字符设备驱动、平台设备驱动、设备驱动模型、sysfs几者之间的比较和关联

    转自:http://www.2cto.com/kf/201510/444943.html Linux驱动开发经验总结,绝对干货! 学习Linux设备驱动开发的过程中自然会遇到字符设备驱动.平台设备驱动 ...

  5. linux设备驱动编写入门

    linux设备驱动是什么,我个人的理解是liunx有用户态和内核态,用户空间中是不能直接对设备的外设进行使用而内核态中却可以,这时我们需要在内核空间中将需要的外设驱动起来供用户空间使用.linux的驱 ...

  6. 【驱动】linux设备驱动·字符设备驱动开发

    Preface 前面对linux设备驱动的相应知识点进行了总结,现在进入实践阶段! <linux设备驱动入门篇>:http://infohacker.blog.51cto.com/6751 ...

  7. 浅谈Android系统移植、Linux设备驱动

    一.Android系统架构 第一层:Linux内核 包括驱动程序,管理内存.进程.电源等资源的程序 第二层:C/C++代码库 包括Linux的.so文件以及嵌入到APK程序中的NDK代码 第三层:An ...

  8. linux设备驱动概述,王明学learn

    linux设备驱动学习-1 本章节主要学习有操作系统的设备驱动和无操作系统设备驱动的区别,以及对操作系统和设备驱动关系的认识. 一.设备驱动的作用 对设备驱动最通俗的解释就是“驱使硬件设备行动” .设 ...

  9. Linux设备驱动工程师之路——内核链表的使用【转】

    本文转载自:http://blog.csdn.net/forever_key/article/details/6798685 Linux设备驱动工程师之路——内核链表的使用 K-Style 转载请注明 ...

随机推荐

  1. EntityFramework 5.0 CodeFirst 教程01-搭建环境和快速上手

    ----------------------------目录------------------------------ EntityFramework 5.0 CodeFirst 教程03-数据结构 ...

  2. 使用btrace来找出执行慢的方法

    转载于:https://shaojun.name/2016/07/260 btrace script import static com.sun.btrace.BTraceUtils.name; im ...

  3. 一个MVC4 下的验证码用法

    先看一个核心验证码类(不用在意实现过程,直接copy就行),下面包含了两种验证码图片(原理一样),代码如下: using System; using System.Collections.Generi ...

  4. 【jsp】配置错误页面

    1,使用JSP方式 如果配置是Jsp时,需要把isErrorPage设置为true, 以及设置 <%@ page language="Java" contentType=&q ...

  5. 使用Nexus搭建Maven内部服务器

    概述         我们在使用maven时,一般通过网络上一些公共的maven仓库来获取jar包,但是有时候会碰到网速比较慢的情况就比较郁闷,Nexus是一个maven的服务器,可以让我们搭建一个本 ...

  6. Php廖雪峰教程学习与实战

    https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000 目录 Python教程 Pyth ...

  7. ASP.NET Core 不同操作系统环境安装之Hello World 教程

    Official Website:https://www.microsoft.com/net/learn/get-started-with-dotnet-tutorial#install Window ...

  8. 【转载】linux 测试机器端口连通性方法

    转载原文:http://blog.csdn.net/z1134145881/article/details/54706711 下面一一介绍: 1 telnet方法 2 wget方法 3 ssh方法 4 ...

  9. Sublime Text增加Build system类型,打造一个全能IDE

    Sublime text2是一款非常方便的文本编辑器,现在我基本上不用IDE去编写代码,一般都是在Sublime text2中编辑,当然,这里无法执行.debug是软肋,于是上网找了下资料,可以把添加 ...

  10. databus编译: Execution failed for task ':databus-core:databus-core-impl:compileJava'.

    在编译databus的过程中,出现了无法找到jdk的错误: 在/etc/.bashrc和/etc/profile中都配置了JAVA_HOME,依然报错,重启后还是报错,原因的是ubuntu中默认的jd ...