应用程序是如何去访问所需的硬件资源的?今天我们来聊聊这个话题!
大家都知道应用程序需要访问数据或者运行的时候都是通过CPU去访问内存地址空间,应用程序的数据通过存储在内存空间中以便于CPU进行快速的读写,CPU不会去硬盘读取数据也不会去U盘读取数据。那么当我们的应用程序需要访问硬件的时候,是怎么去访问的呢?
在X86的体系架构的当中,存在着IO空间这样一个概念。我们在编辑程序的时候所有对程序的调用以及程序的数据读写都是存储到内存空间中的。但是在我们的X86里面还存在着一个IO空间,IO空间实际上是X86硬件自己的一套独立的地址线,主要是作用就是标明我的这个硬件所处的地址位置,以便于CPU能够直接去访问硬件设备。这些硬件各自都拥有彼此独立的地址空间。在32为处理器的X86体系架构下,IO空间的大小为64K,内存空间大小为4GB。那么这IO空间是放在内存中的吗?
IO空间中这部分空间其实是指CPU里面的,CPU其实是有一个概念叫地址空间的东西,这个东西存储着两部分的数据:第一是内存地址空间,第二是IO地址空间。看下图:
根据上图我们可以看见,在左边是CPU的内存地址空间,右边是IO地址空间。在这里我要提醒大家,内存地址空间和内存并不是一个概念,内存是指物理内存,是实实在在的一个硬件。那么可能有人就不解了?那内存地址空间和内存是啥关系呢?顾名思义,内存地址空间就类似于物理内存的地址表,上面写明了内存地址是哪儿到哪儿,类似于硬盘的分区表。
那么上面我们说了,32位系统下内存大小为4GB,是因为在内存地址空间中,有4GB的地址代码,可以编4GB个地址。这4GB个地址码正好可以分配给4GB内存。也就是说在计算机中 CPU的地址总线数目决定了CPU 的寻址范围。32位系统下寻址能力就是4GB。但是后来的出现了一个技术PAE,PAE允许操作系统在32位模式下使用大于4G的物理内存。PAE的优势是可以让不同的进程(在不同的地址空间里)累计使用大于4G的内存,因此而达到使用超过4G内存的目的。但是不管是否使用PAE,对于单个进程而言,32位系统下可见的地址空间最大只有4G。换句话说就是,在32为系统下,使用了PAE安装了8GB的内存,但是当我们插入U盘拷贝一个大于4GB的文件到系统中时,是无法拷贝进来的。因为单个进程无法支持超过4GB。对于PAE,在这里举个例子:在我国当前电话系统中,一部分电话号码是保留号码(110、119什么的,专用),大部分开放给用户,还有一部分就是总机号码,接总机里面有很多分机号码,使得8位电话号码所代表的电话用户数量远超8位数。这就是32位系统实际还是可以通过“总机”(即所谓的PAE)来使用超过4GB寻址的能力。
回归正题,上述我们好像有点跑题了!右边的IO地址空间存放了写什么东西呢?上面我们也说了, IO地址空间实际上是X86硬件自己的一套独立的地址线,主要是作用就是标明我的这个硬件所处的地址位置,以便于CPU能够直接去访问硬件设备。因此,当应用程序需要去访问哪个硬件的时候,就在IO地址空间里面查,比如并口,哪个并口在什么地址线里面,CPU通过这个表查询到以后,就通过地址总线发送指令到指定的地址和硬件联系就好了。
关于IO地址空间的说明我觉得还值得大家知道的是,IO地址空间只有X86架构有IO地址空间。其他的处理器架构是什么IO地址空间这么一说的,比如ARM、MIPS、PowerPC等等处理器都只支持内存地址空间。而且,这个CPU的地址空间是谁来决定的呢?是CPU决定的?如果是CPU决定的,那么我的CPU怎么知道将来在其上运行的操作系统是什么操作系统?而且怎么保证我的CPU安装在不同的服务器上,如何知道主板上的其他硬件设备的地址线?如果通过CPU来控制或者说提前写入这个地址空间的表,或者说提前规定好,那么CPU就没有那么好的兼容性,只能是定制化的CPU!那么通过操作系统来规定?每一种操作各自规定自己的CPU地址空间?操作系统也不知道服务器硬件的具体设计和规划啊!经过这么一分析,我们就可以找到,谁开决定这个表最合适呢?是服务器,具体来说是服务器的BIOS,只有服务器的BIOS才会知道我服务器的主板上那些硬件设备是在哪儿以及地址线的信息。因此,CPU中的内存地址空间和IO地址空间中由BIOS写入的。服务器在开启的时候,BIOS会加点自检硬件设备,同时根据自身硬件设备的信息,决定这个地址空间的表是怎么样的以及怎么分布的。
那么CPU有了IO地址空间和内存地址空间之后呢,我们继续详细往下走,就需要继续为你们介绍俩个与此相关的概念:IO端口和IO内存。IO端口是指当一个寄存器或内存位于IO空间的时候,我们就称他为IO端口。那这里可能有提到了一个寄存器的概念,这个寄存器我们在接下来来的段落里面介绍。相应地,当有一个寄存器位于内存空间的时候,我们就称其为IO内存。具体IO端口和IO内存有啥用请您往下看!
在上文中我们屡次提及寄存器,我在这里需要做出说明:这里的寄存器并不是指CPU的寄存器,而是:在每一个硬件设备中,都存在着一个小的寄存器设备,该寄存器设备存储了中央处理器和硬件设备的交互信息,同时这个寄存器内部还存储着操作这个硬件设备的二进制代码。因此我们可以这样理解,我们对于硬件的操作其实就是操作硬件设备的寄存器,所以在上文才说,如果在IO地址空间当中,如果存在一个寄存器,那我们就称其为IO端口的原因。
如上所说,计算机在访问硬件的时候就是访问硬件的寄存器。所以我们需要去了解硬件设备的寄存器和计算机的内存有何区别?在《Linux设备驱动程序》第三本的书里面有关于寄存器和内存的说明,书中是这么说的:寄存器和内存的区别在于寄存器操作有副作用,在英文书中写明的是Side effect,翻译过来的时候作者翻译为边际效果。但是在之前的第二版书中并不是这么翻译的,如果我们按照中文的字面意思很难理解这个到底是干嘛的!所以在这里写明是有副作用可能更便于理解。那么都有啥副作用呢?就是当我们去读取寄存器的时候,我们可能会改变这个寄存器的值。注意这里是指可能,并不是说一定要改变这个值。这个可能改变的值就是副作用。比如很多设备的中断状态寄存器只要一读取,便自动清零。这就是寄存器和内存的区别。
那我们如何来操作IO端口(寄存器)呢?操作IO端口主要有三个步骤:
1、 申请
2、 访问
3、 释放
首先来看如果申请!我们说一个应用程序在需要调用硬件设备的时候,应用程序首先需要把请求发给硬件设备的驱动程序,驱动程序告诉内核我需要操作某某IO端口了。同时在内核中提供了一套函数来允许驱动申请她需要的IO端口,核心函数里面规定了:驱动程序需要操作的IO端口的地址、需要操作多少个IO端口以及操作IO端口的驱动程序是谁。申请了之后,哪一个IO端口正在被哪一个设备所使用,内核就有记录信息存在。比如说1、2、3这3个端口,现在正在被串口使用,内核就会记录下来,下一次别人再申请的时候,内核会直接告诉他,你申请的这个硬件设备已经有人在使用了,现在无法提供给你。这个记录信息在Linux的内核中存在于/proc/ioports中,我们可以通过命令cat /proc/ioports机进行查看。通过这样的申请流程,完成申请步骤之后,接下来就是第二步访问。
访问IO端口的时候,也是使用专门的函数in/out,该函数规定了我们需要访问的位宽。IO端口可分为8位、16位和32位端口。Linux内核头文件定义了下列内联函数来访问IO端口。比如我们都字节端口(8位宽),那么就是inb,写字节端口(8位宽)就是outb等等。
当用完一组IO端口之后,需要释放Io端口。通常情况下是驱动程序卸载时才会被释放。释放也有相应的函数来进行释放。
上面我们说的是如何访问IO端口,那么IO内存又是怎么样访问的呢?对于IO内存来说,我们要对其进行操作需要进行四个步骤:
1、 申请
2、 映射
3、 访问
4、 释放
相比较访问IO端口而言,多了一步映射。下面我们解析下相似的过程,第一步还是申请IO内存,内核提供了一套函数来允许驱动程序申请她所需要的IO内存,函数也规定了:所需IO内存的起始地址,IO内存的长度以及设备的名称。申请成功,则会返回非NULL;否则返回NULL,在Linux中所有已经在使用的IO内存在 /proc/iomem中列出。
当我们成功申请了这个IO内存之后,这个IO内存地址会是一点真实的物理地址。而在操作系统当中,不能够直接使用这个物理地址,因此在访问IO之前,必须进行物理地址到虚拟地址的映射,这一步就是映射的步骤,那么怎么来进行映射呢?ioremap函数就是具有此功能的函数,该函数会将物理地址转化成虚拟地址。该函数规定了你需要转化的物理地址是那些、长度是多少;有了这些参数之后,该函数会找到一段相同大小的虚拟地址,然后把这个物理地址和虚拟地址关联起来。硬件的各种寄存器会被映射到某一块物理内存中,这种方式称为MMIO,在Windows的设备管理器里,右键点设备,看属性->资源里,不少硬件设备都有“内存范围”的参数,这里的内存范围就表示这个硬件的资源可以通过访问这一段内存来控制它。
转换完成之后我们就可以访问IO内存了,访问IO内存的正确方法是通过一系列内核提供的函数,这些函数提供了读写IO内存的相关操作。当我们不在使用IO内存的时候也需要释放IO内存的占用,在进行Io内存的释放的时候有两个环节需要我们特别注意:第一个是解除映射关系,我们上面说了,访问的时候不是已经把物理地址转化虚拟地址了吗,现在我们在释放的时候,就需要先解除这种绑定或者说映射关系才行。第二个才是解除对IO内存的使用申请。
那么上面我们讲了IO端口和IO内存,这个怎么去访问硬件有什么关系呢?在这里我首先的说明白硬件的内部结构:在计算机内部,我们的硬件都集成有各自的芯片,看起来像是一些集成电路板。我们也可以称之为硬件的主控芯片。在这块主控芯片里面有什么东西呢?这款芯片里面有CPU, 内存,寄存器。大家不要觉得,哎呀,计算机里面一个普通的硬件怎么会有CPU、内核和寄存器呢?对的,你不用惊讶,每一块计算机的硬件解剖开来就是一台小的迷你计算机,相当于我们的计算机结构了。在这里我们需要知道的是:芯片的引脚跟寄存器是相对应的,寄存器是8位的内存单元,这存在于内存上面。当我们往这个硬件发送指令时,CPU执行这段代码,使芯片的引脚电平(电压)发生变化。而CPU执行的这段代码就去外面所说的物理硬件驱动程序。于是这里的驱动程序,指的是对硬件设备所支持操作的程序表示。做个简单的比喻:比如说我们需要控制显示器的显示操作,譬如清除显示,我们可以编写一个clear()函数,光标移动,我们编写一个move_cursor()函数,读取数据和写数据分别为read()和write(),然后分别实现就可以了(通过向寄存器里写数据的形式,进而控制引脚的电平变化,再而控制显示器,这个过程就是驱动程序的运行机制)。而这些函数也就是所谓的设备驱动程序了!
于是乎,控制访问硬件的流程就变成了这样:
1、 应用程序向内核请求硬件资源;
2、 内核首先去IO端口的表里面查找应用程序所请求的硬件资源发IO端口是否可以使用;
3、 如果可以使用,内核就将该设备的IO内存信息反馈给该设备的驱动程序(IO内存信息是指寄存器映射导虚拟内存的地址段,内存里有一段是跟寄存器相对应的空间);
4、 应用程序请求设备做什么,需要有什么动作,这些指令信息都会发送到设备的驱动程序,设备的驱动程序会生成相应的操作函数;
5、 驱动程序将操作函数代码段加载到内存里跟寄存器相对应的内存空间中,也就是将在物理空间内的代码段通过数据传输的手段传输到硬件设备的寄存器里面。
6、 而寄存器是跟芯片的引脚相对应的,于是操作该段内存就能控制芯片引脚的电压变化。也就能够控制硬件执行相应的动作了。
在这里的流程中,我们还得说明最后一公里的东西,即要读写的数据如何到达硬件呢?在CPU层面上来说,有两种处理方法:
1、我们上面说了,内存中有一段所谓的IO空间是和硬件的寄存器相对应的,当我们将驱动程序需要执行的代码段放入到该IO空间之后,CPU和内核会使用IN/OUT函数或者指令将数据传输到真实的物理设备的寄存器的内存当中。如果数据量比较大,那么就会占用我们CPU的大量的时间,因为我们的CPU会不断的进行内存的搬运,直到数据被搬完为止。
2、采用DMA技术,将存放数据的内存地址通过IN/OUT指令通知硬件,硬件在传入的内存读取/写完数据后,再以中断的方式通知CPU。DMA技术是Direct Memory Access的缩写。其意思是“存储器直接访问”。它是指一种高速的数据传输操作,允许在外部设备和存储器之间直接读写数据,既不通过CPU,也不需要CPU干预。这种方式通过DMA芯片执行数据传输操作,占用CPU时间较短,效率较高。
目前这两种技术,前一种对CPU资源的消耗太高,目前一般都采用后来,即DMA的方式,所以现在的X86计算机体系架构中,内存和硬件设备通信中间还隔着一个DMA控制器。