7.6 驱动框架

发布于:2022-11-05 ⋅ 阅读:(281) ⋅ 点赞:(0)

目录

一 驱动是什么玩意

二 我们讨论驱动的那些方面

三 目标的实现


一 驱动是什么玩意

   首先,就专业角度来讲,对计算机和电子专业的人,驱动在电子产品开发中,不是一个陌生的概念;其次,对非专业人士而言,也可能接触到驱动这个概念。比如,安装系统。因为系统安装过程中,通常都需要装显卡驱动、网卡驱动等。即使系统装完了,也不一定真正清楚驱动是干什么用的,或者驱动的工作过程。这就是封装与解耦思想的功劳。(会修电脑的人不一定会修电视,就是这个道理。)那么驱动到底是做什么的呢?

   驱动实际上是对设备的封装,是设备管理的第一级。对于计算机系统而言,一般来说,除了CPU和内存之外(这也不是绝对的,CPU的初始化也可以看做是驱动,内存的配置同样也可以看做是驱动,只是我们一般不这么讲),其他所有模块都是需要驱动才能正常工作的。像磁盘、显示、键盘、鼠标、网卡⋯等等,所有这些设备的正常工作,都需要驱动的配合。驱动将硬件的设备封装为软件的接囗,这样,操作系统本身或者其他软件模块,就可以通过驱动提供的接口,来操作和使用设备。这种层次关系如下图所示:

   如上图所示,不同设备,有其专门的驱动,且各自驱动的特点也不相同,有的多控制,有些多数据,这是由设备本身的功能来决定的。虽然设备五花八门,驱动多种多样,各有特点,独成一体,但是它们还是有一个共同点,也是相通的东西,这就是寄存器。外设+寄存器的组合,读者应该不会陌生,在第一部分的模型章节,我们对外设进行了抽象,就将其归纳总结为寄存器组。寄存器是设备硬件一级的封装,是软硬件交互的接口,而驱动是软件功能级别的封装,在寄存器这种近乎(虽然寄存器在其软硬件交互窗口这一基础责任属性上,自然的或者天然的附带了少量功能属性,但相对于大整体而言,我们还是认为其更多属于直接的转发接口,而非将其做为功能接囗)直接的转发接口基础上,驱动进行了第一次的重组,通过将不同寄存器的作用进行排列组合,从而实现更高层次的功能接口。系统软件和应用软件使用这些接口,来发挥外设本身的价值。这就好比搭建一座桥,有钢筋,水泥,沙石,这些基础物料,然后修建出桥墩,桥面,护栏,这些更高一级的组件,这些组件功能更为实用,最后使用桥墩桥面,搭建出最终实用的产品--桥。驱动就好比是桥墩,桥面这类更高一点的组件。可能这样打比方,读者还是有点云里雾里的感觉,下面我们举一个网卡驱动的例子,来进一步的说明。同时,这也为下一部分网络栈的讨论,做个引子。

   网卡是计算机通过有线方式连网的必备设备。我们上网传输的数据,通过OS的网络栈,在内存中一步步的封装,最后到达OS的网卡驱动,再通过网卡驱动,送给网卡,然后由网卡送到网线上,最终通过网线到达目的地,这个过程如下图所示:

   通过上图,我们了解了网卡驱动本身的作用。从最底层来讲,网卡做为一个硬件设备,一头连接到计算机的三总线上,实现同主机的连接;一头连接到网线上,检查网线的状态,并收发数据。再往上一层,CPU可以通过读写网卡的地址、数据和控制寄存器,实现软硬件的交互。比如,CPU可以通过控制寄存器,告诉网卡,将要把内存中的数据送给它。接着,CPU通过数据寄存器将数据送给网卡。完成这一步后,CPU再次通过控制寄存器,告诉网卡,将刚才写过来的数据送出去。网卡收到这一命令后,即检查网线的状态,并传送数据到网线上。这些不同寄存器的区分,则是通过地址寄存器找到的。地址寄存器本身的地址则是在硬件设计时就确定的。这后一步,就是具体送数据据的过程,不再需要CPU的参与,网卡自己就可以完成。而这自主的一步,恰恰是网卡自身硬件设计的重点,也是网卡区别于别的设备,叫网卡而不叫显卡或别的什么的卡的本质。

   其实,上面发送数据的逻辑描述,就是驱动层的工作。网卡本身有很多寄存器,这些寄存器都附带一个微小的功能,这个单一微小的功能是硬件可直接操作实现的,驱动就将这些寄存器的使用分类整理,然后封装出类似设备初始化,发送数据,接收数据,获取状态等等这类接囗。比如,就发送数据来讲,通过接口参数指定目的地址(这里所说的地址是网络数据要送达的地址)和数据缓冲地址。而在接口实现中,通过专门的寄存器将目的地址送给网卡,通过数据和控制寄存器将数据告诉网卡,然后通过专门的寄存器再通知网卡将数据送出去。这个过程与我们在上一部分的描述基本一致。在发送数据这一接口中,组合使用了网卡的多个寄存器,这就是驱动的工作。

   虽然说驱动是用来驾驭设备的,但是驱动的实现,并非是定死的,也非芯片厂家提供。对于一种芯片,厂家往往只针对个别流行OS,比如Windows、Linux等,提供参考驱动,但是用户完全可以根据自身需求,进行裁减或者独立实现。即使在同一个OS上,不同的人来实现,也是有一些差异的,无论从功能多寡上,还是性能强弱上。可能都可以工作,但是效果有差别。所以讲,驱动的实现也是有好有坏,十分讲究的。同一款芯片,不同的驱动,其所挖掘出的设备潜能是不一样的。而且不同的操作系统所能提供的舞台也是有差异的。总之,驱动的设计是一件精巧的工作。

二 我们讨论驱动的那些方面

   在这一部分,我们并不展开讨论各种各样设备驱动的具体实现,而是基于Linux的实现,简单介绍一些操作系统在驱动架构设计方面所采用的思想方法。

   驱动做为软件与多种多样设备之间的桥梁,在整个操作系统设计中,担负着十分重要的作用。如第一部分所述,同一芯片,在不同OS平台上,所能获得的舞台大小,是有差异的。所以,如何架构出可合理获得CPU资源,适应性强,稳定性好,并能灵活扩展的驱动框架,对OS设计者而言,是一大考题。而具有这些特性的驱动框架,也是操作系统强大的直接体现。因为一款强大的、流行的OS,很大程度上,也依赖其支持和兼容的设备类型和数量。试想一下,如果一款OS仅能支持个别数量十分有限的设备,那么要流行起来,是不太可能的,这个道理很好理解。不过要做到通用性好,还是需要下一番功夫的,毕竟设备是五花八门的。

  

三 目标的实现

   正如上一部分所述,我们的讨论更趋向于架构实现。也如上一部分所述,架构需要完成几点总体目标。而要实现这些目标,整个驱动架构的实现,就需要下一番功夫,精巧构思,巧妙实现。之所以选择Linux,是因为Linux在这一块的设计,确实是一个很好的可供学习的例子。

   到这里,笔者与读者之间,其实己达成了一种共性的认识:那就是驱动本身的实现非常灵活。这种灵活性带来了一种困难,那就是驱动的整体框架不好设计。不过,在技术发展过程中,人们通过不断的总结归类,还是摸索出来了一套驱动设计的模式,可以在不同OS,不同设备上应用。这倒㡳是怎样一种模式呢?

   驱动模式,说白了,就是编写驱动的一套行之有效的规范。这种模式把驱动的基本实现,大多归为下面几个接囗:

   1 Init接口。主要用于设备资源的准备以及设备的初始化。在启动驱动之前,需要先调用该接口,进行整个驱动运行环境的准备。

   2 Open接囗。打开设备,准备数据的收发。

   3 Start接口。启动设备运行。2和3两个接口并无明显的分界。有些设备并不需要单独的Open接口,但有些设备增加一个过渡接口,在整个逻辑设计上会更简洁,更加优雅。

   4 Interrupt接口。用于中断程序处理。这也是一个十分重要的接口。设备运行过程,有什么需要通知CPU进行处理的,就通过中断这个接口完成。比如,出现了异常情况,数据发送完成了,有新的数据到来了,等等。

   5 Read接口。用于读取数据。将设备中的数据读取到内存中。

   6 Write接囗。用于写数据。将内存中的数据写到设备缓冲中。

   7 Stop接口。3的反向接口,停止设备运转。

   8 Close接口。2的反向接口,关闭设备。

   9 Term接口。1的反向接口,释放1中为设备申请的资源。如果调用了Close接口,要再次启动设备运转,只需调用Open接口;如果调用了Term接口,那要再次启动设备,就必须重新调用Init接口。

   10 State接口。获取设备运行过程中的状态。

   通过上面10个接口,基本上就可以搭出一个驱动架子。设备的启动和关闭,数据的接收和发送,中断以及状态的处理,都就有据可循,有接口可调。并且,每一个设备的驱动都可以尽可能的实现这些接囗。同时,根据设备的不同,每一个接口的设计可能很简单,一两句代码就OK,也可能很复杂,需要考虑很多东西,实现的很庞大。不过,这都没关系,这些都是具体驱动实现时要考虑的,模式只要保证这套运行逻辑是合理完备的即可。这样,整体来看,大家的驱动,其实现样子都比较接近,但具体来看,毎一个设备的驱动又十分不同。A设备运行需要的环境同B设备需要的环境并不同,因而A设备驱动Init接口的实现同B设备,就完全可能不同,除了这些接口的名称和目的相同之外。这是不是有点面向对象的思想在里面了?框架接口就是抽象类,而具体驱动实现就是各种具体类对抽象类的实例化。

   那么框架设计如何从上述模式中获得突破口呢?直白的来说,就是具体如何操作实现的,这其中就涉及到许多细节的问题。对于这个问题,我们基于一幅图来说明。

   如下图所示。驱动是对设备的封装,然后暴露出几个逻辑接口,这是第一步的挂接。

   这幅图中,A与B是驱动实现,之外的部分,包括总线和设备,是框架的一部分。这样,这个框架的雏形就出来了。操作系统可以有一个类似虚拟设备的驱动,专门用于管理实际的设备和驱动。有了这个框架,操作系统加载一个设备后,就去查找该设备的驱动,找到后,按照逻辑顺序调用驱动暴露出来的接口。比如,先调用Init初始化环境,分配资源。至于如何初始化,如何分配,那是驱动自己的事。其他接口也一样。接着Open设备,按需求启动Start。仍然以网卡为例,Start之后,就可以配合中断,调用Read接口来不断的读取网络中的数据,这一般是被动的过程。当网络栈中有数据要发送时,则可以调用Write接口来主动发送数据。

   十个接口所构成的模式,只是一个简化的例子。鉴于设备的多样性,实际的模式,可能不只十个接口而己,而且面向的具体需求不同,也可以对模式裁减或增加(比如,功耗相关的接口,休眠和唤醒)。甚至呢,操作系统可以尽可能实现一个包括多种情况的大模式,如果驱动对某些接口不做实现,就说明该设备不需要这个接口,操作系统调用的效果跟完全跳过是一样的。

   下面,我们看看Linux如何完成这个框架的任务。

   Linux对参与驱动的软硬件各方都进行了抽象,因而,在整个驱动框架中,产生如下几个概念:总线,设备和驱动。设备和驱动都好说,并不陌生,而总线是一个新的内容。从硬件架构上来讲,设备都是连接到三总线上的,所以总线也属于整个驱动系统中重要的一员。

   设备的抽象,描述了设备的名称、地址、兼容性识别标识、关联的驱动和总线。

   驱动的抽象,除了上述的十多个接口外,还有匹配的设备和挂载的总线。

   总线的抽象,则描述的总线的名称、关联的设备和驱动等。

   灵活的驱动架构,上述三者的关系并不是绑定死的,而是可以动态的加载和卸载。另外,还支持总线架构的模拟,设备的动态识别等。简单举个例子来说明上述过程:

   首先,从硬件上构建上述框架的底座。这就是系统中必现包括CPU和内存。这里的CPU,是纯粹意义上的中央处理单元,不包括SOC等构成的所谓CPU封装。

   有了上述底座,就可以运行程序。

   根据配置和描述(比如现在Linux支持的设备树模型),系统可以搭建一个软件层面的设备关系图。

   然后,软件可以从总线开始,查找总线上的设备。找到一个设备后,就查找总线上该设备的驱动。如果找到驱动,就加载驱动,执行上述驱动抽象中设定的接口,包括初始化、打开、启动等。这样,该设备就进入工作状态。如果未找到设备的驱动,那么设备暂时就无法工作。

   当我们有新的设备进入系统,比如插入USB设备,那么USB总线就可以检测到设备的上线,从而触发系统查找设备的驱动。找到后,同上面所述一致,加载驱动,让设备工作。

   当我们安装新的驱动后,系统同样查找驱动关联总线上的设备,如果找到匹配的设备,则执行驱动的抽象接口,完成设备的配置,启动运行。

   当我们移除设备时,同样会被总线检测到,自然会查找总线上设备的驱动,执行驱动的关闭、释放资源接口,完成收尾工作。移除驱动也是类似的。停止设备运行后,驱动移除。

   上述流程中,我们主要介绍了驱动和设备的动态增加和移除过程。这其中并没有强调总线。一般而言,总线在整个硬件框架确定后,就是明确的。当然,从软件角度来讲,总线可以进一步划分为总线设备和总线驱动。自然,相关的处理流程可以类比到设备驱动和设备。

   上述过程中,设备、驱动和总线的匹配是如何完成的?这里面并没有什么魔法,设备所用总线在设备设计时,就是明确的。我们拿到设备后,这些信息就是已知的了。在系统中增加设备时,就需要提供获取这些信息的接口,这样系统就可以知道是什么设备,需要接到什么总线上。驱动也是一样。编写一个驱动软件时,需要明确该驱动是为那个设备或者哪类设备编写的,自然也会明确该驱动匹配的设备特点,系统也是根据这些提前预置的信息来查找设备的。

   从逻辑关系上来讲,总线就像是枝干,而设备和驱动就像是叶子,它们都挂到总线上。一个设备可能有多个驱动可以使用,而一个驱动,也可能适配多个同类型的设备。这样,在驱动和设备的匹配过程中,就存在一些优先级关系。更为亲密的关系,表明二者可以优先匹配。比如,通过完全的设备名称匹配的,是最优先的匹配关系,而较为远一些的关系,可以通过设备类型的名称来匹配。具体规则的设定,是由系统的设计者来确定的。

   至此,我们基本完成了驱动的介绍。

本文含有隐藏内容,请 开通VIP 后查看