1、Linux设备分类
计算机系统的硬件主要由 CPU、存储器和外设组成。驱动针对的对象是存储器和外设(包括CPU内部集成的存储器和外设),而不是针对CPU核。Linux将存储器和外设分为3个基础大类:字符设备;块设备;网络设备。如下图所示:
image.png
(1)字符设备
字符设备指那些必须以串行顺序依次进行访问的设备,是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。控制台( /dev/console )和串口( /dev/ttyS0)是字符设备的例子, 因为它们很好地展现了流的抽象。字符设备通过文件系统结点来存取, 例如 /dev/tty1 和 /dev/lp0。
在一个字符设备和一个普通文件之间唯一有关的不同就是, 你经常可以在普通文件指定位置读写数据, 但是大部分字符设备仅仅是数据通道, 你只能顺序存取。
(2)块设备
块设备可以用任意顺序进行访问,以块为单位进行操作,如硬盘、软驱等。字符设备不经过系统的快速缓冲,而块设备经过系统的快速缓冲。块设备包括硬盘、磁盘、U盘和SD卡等。Linux允许应用程序读写一个块设备象一个字符设备一样——它允许一次传送任意数目的字节。结果就是, 块和字符设备的区别仅仅在内核以及在内部管理数据的方式上, 并且因此在内核/驱动的软件接口上不同。
字符设备和块设备的驱动设计呈现出很大的差异,但是对于用户而言,他们都使用文件系统的操作接口open()、close()、read()、write()等函数进行访问。
(3)网络设备
在Linux系统中,内核与网络设备的通信和内核与字符设备、块设备的通信方式完全不同。网络设备面向数据包的接收和发送而设计,它并不对应于文件系统的节点。
一个网络接口负责发送和接收数据报文,在内核网络子系统的驱动下,不必知道单个事务是如何映射到实际的被发送的报文上的。
既然不是一个面向流的设备, 一个网络设备就不象/dev/tty1那么容易映射到文件系统的一个结点上。内核与网络设备驱动间的通讯与字符和块设备驱动所用的完全不同,不用read和write, 而是由内核调用和报文传递相关的套接字函数。

2、设备文件与设备号
(1)设备文件
从用户的角度出发,如果在使用不同设备时,希望能够用同样的应用程序接口和命令来访问设备和普通文件。Linux抽象了对硬件的处理,所有的硬件设备都可以作为普通文件一样来看待:它们可以使用和操作文件相同的、标准的系统调用接口来完成打开、关闭、读写和I/O控制操作,而驱动程序的主要任务也就是要实现这些系统调用函数。
对用户来说,设备文件和普通文件并无区别。用户可以打开和关闭设备文件,也可以通过设备文件对设备进行数据读写操作。例如,用同一write()系统调用既可以向普通文件写入数据,也可以通过向/dev/lp0设备文件写入数据,从而把数据发给打印机。如下图所示。

image.png
3、Linux设备驱动代码分布与特点
如下图所示:
image.png 介绍如下:
(1)Linux设备驱动代码分布
所有Linux的设备驱动源码都放在drivers目录中,分成以下几类:
1)block,块设备驱动包括IDE驱动。块设备包括IDE与SCSI 设备。
2)char,此目录包含字符设备的驱动,如ttys、串行口以及鼠标。
3)cdrom,包含了所有Linux CDROM代码。在这里可以找到某些特殊的CDROM设备。IDE接口的CD驱动位于drivers/block/ ide-cd. c中,而SCSI CD 驱动位于drivers/ scsi/ scsi. c中。
4)pci,包含了 PCI伪设备驱动源码。在这里可以找到关于PCI子系统映射与初始化的代码。
5)scsi,包含所有的SCSI 代码以及Linux支待的SCSI 设备的设备驱动。
6)net,包含了网络驱动源码,Chip21040 PCI 以太网驱动。
7)sound,包含了所有的声卡驱动源码。
(2) Linux 设备驱动程序的特点
1)内核代码,设备驱动是内核的一部分。
2)内核接口,设备驱动必须为Linux内核或者其从属子系统提供一个标准接口。比如一个终端驱动程序为内核提供了一个文件I/0接口,而一个SCSI设备驱动为SCSI子系统提供了一个SCSI设备接口,同时SCSI子系统也必须为内核提供文件I/0接口和buffer、cache接口。
3)内核机制与服务,设备驱动可以使用标准的内核服务,如内存分配、中断和等待队列等。
4)可加载,大多数Linux设备驱动可以在需要的时候加载到内核,同时在不再使用时被卸载。这样内核就能更有效地利用系统资源。
5)可配置,Linux设备驱动程序可以集成为内核的一部分。在编译内核的时候,可以选择把哪些驱动程序直接集成到内核里面。
6)动态性,当系统启动及设备驱动初始化后,驱动程序将维护其控制的设备。此时此设备驱动仅仅只是占用少量系统内存,不会对系统造成什么危害。

4、Linux内核设备模型
内核设备模型是 Linux 2.6之后引进的,是为了适应系统拓扑结构越来越复杂,对电源管理、热插拔支持要求越来越高等形势下开发的全新的设备模型。它采用sysfs文件系统,一个类似于/proc文件系统的特殊文件系统,作用是将系统中的设备组织成层次结构,然后向用户程序提供内核数据结构信息。
设备模型建立目的:

       为内核建立起一个统一的设备模型,从而有一个对系统结构的一般性抽象描述。设备模型设计的初衷是为了节能,有助于电源管理。
      基本原理是这样的,当系统想关闭某个设备节点的电源时,内核必须首先关闭该设备节点以下的设备电源。举个例子来说,内核需要在关闭USB鼠标之后,才能关闭USB控制器,再之后才能关闭PCI总线。
现在内核使用设备模型支持多种不同的任务:
电源管理和系统关机:这些需要对系统结构的理解,设备模型使OS能以正确顺序遍历系统硬件。
与用户空间的通讯:sysfs虚拟文件系统的实现与设备模型的紧密相关, 并向外界展示它所表述的结构。向用户空间提供系统信息、改变操作参数的接口正越来越多地通过sysfs,也就是设备模型来完成。
热插拔设备:越来越多的设备可以被动态的热插拔了,外围设备可根据用户需求来安装和卸载。
设备类型:设备模型包括了将设备分类的机制,在一个更高的功能层上描述这些设备,并使设备对用户空间可见。
对象生命周期:设备模型的实现需要创建一系列机制来处理对象的生命周期、对象间的关系和对象在用户空间的表示。


设备拓扑结构的sysfs文件系统:
        sysfs帮助用户以一个简单文件系统的方式来查看系统中各种设备的拓扑结构。sysfs代替的是/proc下的设备相关文件。
Block 目录:包含所有的块设备
Devices 目录:包含系统所有的设备,并根据设备挂接的总线类型组织成层次结构
Bus 目录:包含系统中所有的总线类型
Drivers 目录:包括内核中所有已注册的设备驱动程序
Class 目录:系统中的设备类型(如网卡设备,声卡设备等)
firmware目录:包含一些如ACPI, EDD, EFI等底层子系统的特殊树;
fs目录:存放的已挂载点,但目前只有fuse,gfs2等少数文件系统支持sysfs接口,传统的虚拟文件系统(VFS)层次控制参数仍然在sysctl (/ proc/ sys/ fs)接口中;
kernel目录:新式的 slab分配器等几项较新的设计在使用它,其他内核可调整参数仍然位于sysctl (/ proc/ sys/kernel)接口中;
module目录:系统中所有模块的信息,不管这些模块是以内联 (inlined)方式编译到内核映像文件(vmlinuz)还是编译到外部模块 (ko文件),都可能会出现在/sys/module中
power目录:包含系统范围的电源管理数据。

设备模型和sysfs:
Class(分类):在Linux设备模型中,Class的概念非常类似面向对象程序设计中的Class(类),它主要是集合具有相似功能或属性的设备,这样就可以抽象出一套可以在多个设备之间共用的数据结构和接口函数。
Device(设备):抽象系统中所有的硬件设备,描述它的名字、属性、从属的Bus、从属的Class等信息。
Device Driver(驱动):Linux设备模型用Driver抽象硬件设备的驱动程序,它包含设备初始化、电源管理相关的接口实现。而Linux内核中的驱动开发,基本都围绕该抽象进行(实现所规定的接口函数)。
     这与设备、驱动、总线和类的现实状况是直接对应的,如下图所示。
image.png

硬件拓扑描述Linux设备模型中四个重要概念:Bus、Class、Device、 Device Driver。
     Bus(总线):Linux认为总线是CPU和一个或多个设备之间信息交互的通道。而为了方便设备模型的抽象,所有的设备都应连接到总线上(无论是CPU内部总线、虚拟的总线还是“platform Bus”)。如下图所示。
image.png
设备kobject :
        kobject是基础的结构,它保持设备模型在一起。struct kobject所处理的任务和它的支持代码现在包括:
对象的引用计数:
       当一个内核对象被创建,需要知道它会存在多长时间。一种跟踪这种对象生命周期的方法是通过引用计数。当没有内核代码持有对给定对象的引用,那个对象就已经完成了它的有用寿命并且可以被删除。
sysfs表示:
        在sysfs中出现的每个对象在它的下面都有一个kobject,它和内核交互来创建它的可见表示。
数据结构粘和:
       设备模型从整体来看是一个极端复杂的由多级组成的数据结构,各级之间有许多连接。kobject实现这个结构并且保持它在一起。
热插拔事件处理:
         kobject子系统处理事件的产生,事件通知用户空间关于系统中硬件的来去。我们可能从前面的列表总结出kobject是一个复杂的结构。

5、Linux设备驱动程序分层思想

为了实现Linux一个内核映像适用于多个硬件的目标,Linux按照分层思想把总线、设备和驱动模型抽象出来,驱动只管驱动,设备只管设备,总线则负责匹配设备和驱动,而驱动则以标准途径拿到板级信息。
image.png
Linux的字符设备驱动需要编写file_operations 成员函数,并负责处理阻塞、非组塞、多路复用、SIGIO等复杂事务。如下图所示。
image.png
Linux的设备驱动程序与外界的接口可以分成三部分:
(l)驱动程序与操作系统内核的接口。这是通过include/linux/fs.h中的file_operations数据结构来完成的,后面将会介绍这个结构。
(2)驱动程序与系统引导的接口。这部分利用驱动程序对设备进行初始化。
(3)驱动程序与设备的接口。这部分描述了驱动程序如何与设备进行交互,这与具体设备密切相关。
        根据功能来划分,Linux设备驱动程序的代码结构大致可以分为如下几个部分:驱动程序的注册与注销、设备的打开与释放、设备的读写操作、设备的控制操作、设备的中断和轮询处理。

6、Linux总线设备驱动模型
在Linux中,所有的设备驱动程序都会有以下两行代码:
module_init(xxx_init_module);
module_exit(xxx_exit_module);
module_init/module_exit是两个宏。module_init是该驱动程序的入口,加载驱动模块时,驱动程序就从xxx_init_module函数开始执行。而当该驱动程序对应的设备被删除了,则会执行xxx_exit_module这个函数。
在Linux中,引入总线来对外设进行管理,设备与驱动采用分层结构,Linux内核中分别用struct bus_type,struct device和struct device_driver来描述总线、设备和驱动。
(1)总线
总线是处理器和设备之间的通道,在设备模型中,所有的设备都通过总线相连,以总线来管理设备和驱动函数。
struct bus_type {
const char      *name;
struct bus_attribute    *bus_attrs;
struct device_attribute *dev_attrs;
struct driver_attribute *drv_attrs;
int (*match)(struct device *dev, struct device_driver *drv);
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
int (*probe)(struct device *dev);
int (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
    ……
};
在总线数据结构中比较重要的三个成员是match、probe和remove函数。
1)match
配对函数(match)是总线结构体bus_type的其中一个成员:
          int (*match)(struct device *dev, struct device_driver *drv);
当总线上添加了新设备或者新驱动函数的时候,内核会调用一次或者多次这个函数。
match函数判断设备的结构体成员device->bus_id和驱动函数的结构体成员device_driver->name是否一致,如果一致,那就表明配对成功。
2)probe
第二个是探测函数(probe)是驱动函数结构体中的一个成员:
            int (*probe) (struct device *dev);
当配对(match)成功后,内核就会调用指定驱动中的probe函数来查询设备能否被该驱动操作,如果可以,驱动就会对该设备进行相应的操作,如初始化。所以说,真正的驱动函数入口是在probe函数中。
3)remove
卸载函数(remove)是驱动函数结构体中的一个成员:
            int (*remove) (struct device *dev);
当该驱动函数或者驱动函数正在操作的设备被移除时,内核会调用驱动函数中的remove函数调用,进行一些设备卸载相应的操作。
总线的注册有两个步骤:定义一个bus_type结构体,并设置好需要设置的结构体成员。调用函数bus_register注册总线。
函数原型如下:
         int bus_register(struct bus_type *bus)
       该调用有可能失败,所以必须检查它的返回值,如果注册成功,会在/sys/bus下看到指定名字的总线。
总线删除时调用:
         void bus_unregister(struct bus_type *bus)
(2)设备
在最底层,Linux系统中每个设备都用一个device结构的表示。
struct device {
    struct device    *parent; //指定该设备的父设备,如果不指定(NULL),注册后的设备目录在sys/device下
    struct device_private   *p;
    struct kobject    kobj;
    const char      *init_name;   //设置设备名称
    struct device_ type  *type;
    struct mutex     mutex;      
char bus_id[BUS_ID_SIZE];  //在总线生识别设备的字符串,同时也是设备注册后的目录名字。
struct bus_type *bus;   //指定该设备连接的总线
struct device_driver *driver;   //管理该设备的驱动函数

    void (*release)(struct device *dev); //当给设备的最后一个引用被删除时,调用该函数
};

在注册一个完整的device结构前,至少定义parrent、bus_id、bus和release成员。
  设备的注册与总线一样:
1)定义结构体device。
2)调用注册函数:
          int device_register(struct device *dev)
  函数失败返回非零,需要判断返回值来检查注册是否成功。
   设备注销函数:
void device_unregister(struct device *dev)

(3)驱动程序
       设备模型跟踪所有系统所知道的设备。进行跟踪的主要原因是让驱动程序协调与设备之间的关系。
struct device_driver {
     const char      *name; //驱动函数的名字,在对应总线的driver目录下显示
     struct bus_type  *bus; //指定该驱动程序所操作的总线类型,必须设置,不然会注册失败
     struct module    *owner;
     const char       *mod_name;  /* used for built-in modules */
     bool            suppress_bind_attrs;   /* disables bind/unbind via sysfs */
#if defined(CONFIG_OF)
     const           struct of_device_id   *of_match_table;
#endif
     int    (*probe) (struct device *dev); //探测函数
     int    (*remove) (struct device *dev); //卸载函数,当设备从系统中删除时调用
void   (*shutdown) (struct device *dev); //当系统关机是调用

};

驱动程序和设备不一样的是,在注册驱动函数是必须指定该驱动函数对应的总线,因为驱动函数注册成功后,会存放在对应总线的driver目录下,如果没有总线,注册当然会失败。
与总线的注册一样:
1)定义结构体device_driver。
2)调用注册函数:
       int driver_register(struct device_driver *drv)
函数失败返回非零,需要判断返回值来检查注册是否成功。
设备注销函数:
void driver_unregister(struct device_driver *drv)

总线、设备、驱动三者的关系:
在设备与驱动结构体中,都有一个总线的指针,用于关联设备的总线类型。在bus_type结构体的match函数,它的两个参数一个是驱动,另外一个则是设备。
当一个struct device诞生,总线就会去driver链表找设备对应的驱动程序。若是找到就执行设备的驱动程序,不然就等待。反之亦然。
device_driver中,当驱动匹配到了对应的设备以后,就会调用probe函数指针来关联驱动设备。因此能够说这个函数才是驱动程序真正的入口。
当驱动程序对应的设备被删除以后,使用remove函数来删除驱动程序。

驱动模块初始化:
当驱动程序开始执行时,首先会执行该驱动程序的初始化函数xxx_init_module,代码如下:
static int __init xxx_init_module(void)
{
     …
     return xxx _register_driver (&xxx_driver);
}
模块的初始化流程:module_init-->xxx_init_module-->xxx_register_driver。
驱动模块卸载:
当驱动卸载是卸载设备模块对应的驱动程序,设备对应的总线不卸载。
static void __exit xxx_exit_module (void)
{

xxx_unregister_driver (&xxx_driver);
}
模块的卸载过程和初始化类似,module_exit>xxx_exit_module-->xxx_unregister_driver。