详细的设备驱动程序实现机制可参考第10章,此处不做详细说明。下面以鼠标驱动程序为例,对驱动程序中与消息输入有关的部分进行解释,希望读者能够建立一个大概的逻辑概念。
鼠标驱动程序被加载后,操作系统核心会调用其DriverEntry函数,该函数主要完成注册鼠标中断、初始化鼠标硬件、创建鼠标设备对象等工作。其中最重要的是第一步:注册鼠标中断。下面是相关代码:
上面代码片段中,(1)处标注的是注册中断函数的代码。这个函数(ConnectInterrupt)的用途,是把一个中断处理函数与一个中断向量连接起来。这里的中断向量0x2C(MOUSE_INT_VECTOR)就是PC的缺省PS/2鼠标设备中断。需要注意的是,0x2C是调整后的鼠标中断向量号。在切换到保护模式时,向量表的前32个,是系统保留给异常使用的,硬件中断从32号开始。因此虽然按照IBM兼容机的标准,PS/2鼠标的中断是12,在切换到保护模式时,也要加上32,得到十六进制的0x2C。其他硬件的中断号,也是这样得到的。
上述函数执行完毕,一旦鼠标被移动或被按下,鼠标的硬件就会通过该中断向量,中断CPU请求服务。因此鼠标中断处理函数MouseIntHandler是整个鼠标驱动程序的核心,后面将以该函数的实现为主要内容,讲解鼠标驱动程序是如何识别用户按下的鼠标键,以及如何把鼠标消息发送给操作系统核心的。为了便于理解下列代码,首先简单介绍一下机械鼠标的工作原理。
当前一般存在两种类型的鼠标,一类就是所谓的2D(二维)鼠标,它就是我们平常用的那种没有滚轮的鼠标,由于这种鼠标在位移上只有X与Y两个方向,所以称之为2D(二维)鼠标;还有一类就是现在比较常见的3D(三维)鼠标,它们有一个滚轮,而这个滚轮会产生一个额外的Z位移量,因此,它在位移上有X、Y、Z三个方向,所以又称之为3D(三维)鼠标。为了简单,我们以二维鼠标为例,讲解其工作机理。一旦用户移动鼠标、按下鼠标、释放鼠标,不管是左键还是右键,鼠标控制器(在PC上是i8042芯片,该芯片同时还控制键盘)会发起三次中断,通过三次中断向主机发送三个字节的数据:第一个字节是控制字节,说明了鼠标当前的状态,比如鼠标左右键的状态(按下/释放),鼠标移动的偏移量的符号(正向移动还是负向移动),等等。接下来的两个字节,一个是X方向的偏移量,另外一个是Y方向的偏移量。注意这里是偏移量,即是针对鼠标最后一次的位置的位置变化,因此会有正负之分(正、负号在第一个字节里面)。如果X或Y是负数,则是以补码表示的,需要进行特殊处理。下面是第一个字节各比特的含义:
●位0:左键按下标志位,为1表示左键被按下。
●位1:右键按下标志位,为1表示右键被按下。
●位2:中键按下标志位,为1表示中键被按下。
●位3:保留位,总为1。
●位4:X符号标志位,为1表示X位移量为负。
●位5:Y符号标志位,为1表示Y位移量为负。
●位6:X溢出标志位,为1表示X位移量溢出了。
●位7:Y溢出标下位,为1表示Y位移量溢出了。
因此,针对任何一个鼠标消息,鼠标中断处理函数需要被调用三次:第一次输入控制字节,第二次输入X偏移分量,第三次输入Y偏移分量。好了,有了这些铺垫之后,我们正式进入鼠标中断处理程序。代码比较长,我们分段进行解释:
上面代码定义了中断函数要用到的一些变量,大部分是静态变量,这是因为一个鼠标消息,要至少调用三次中断处理函数,而且这三次之间还是有关联的(后面X/Y分量字节,受第一个控制字节的控制),因此上述静态变量用于记录之前的鼠标状态。
MsgCount记录了三次中断的次序,第一次中断发生时,该值为0,于是中断程序会认为是控制字节,会按照控制字节进行处理,然后增加MsgCount。第二次中断的时候,中断程序会认为是X分量的偏移,于是处理X分量,然后再增加MsgCount。第三次的时候自然是Y分量,处理完毕,重置MsgCount为0。这样就实现了三次中断处理一个鼠标消息的目的。x和y两个变量记录了鼠标的位置。(www.xing528.com)
中断函数第一次被调用,于是记录左键是否被按下、右键是否被按下,同时记录X/Y偏移的符号,然后结束中断处理函数,等待第二次中断。
第二次中断的时候,根据X分量的符号值,来对X分量分别进行处理。如果是正数,则在当前鼠标X分量(x值)基础上,增加相应的偏移。如果增加后超过了鼠标X分量的最大值(255),则会把鼠标的当前x坐标设置为最大。这时候鼠标在屏幕上的表现就是,鼠标到达屏幕的最右边,不能继续往右移动。如果X分量偏移是负数,则调整一下(原始数据是偏移的补码),在当前X分量上减去偏移。当然,X分量不能为负数,最小为0。如果为0,则鼠标在屏幕上表现为到达最左端,不能继续左移。
第三次中断的时候,首先处理Y分量,处理方式与X分量相同。
处理完毕,就形成一条完整的鼠标消息了。这时候首先要确定消息类型。XOR是异或操作,用于判断两个BOOL值是否相同。如果两个BOOL值的异或结果为TRUE,说明这两个值是不同的,否则相同。可以用XOR运算来确认前后两次鼠标消息的按键状态是否有变化,代码如下:
上面代码判断鼠标左键是否有变化。如果有变化,则进一步判断鼠标键是被按下,还是抬起。由于bLDPrev记录了先前一次鼠标消息的左键是否被按下,因此如果bLDPrev是TRUE,说明本次鼠标消息是按键抬起(本次消息和前一次消息一定相反)。这时候设置消息类型是KERNEL_MESSAGE_LBUTTONUP。如果bLDPrev是FALSE,则说明本次消息是按下鼠标键的消息,因此记录鼠标消息类型为LBUTTONDOWN。这里还有一种特殊情况,就是双击。鼠标双击本质上是两次间隔很短的单击消息,因此我们要进一步判断是不是双击消息。这里的判断方式是,使用另外一个变量,bHasLDown记录了先前一次消息是不是鼠标按下。如果是,且本次与前一次之间的时间间隔很小(小于3个系统时钟tick),则认为是一次双击鼠标消息,于是重新设置消息的类型为左键双击(LBUTTONDBCLK)。这时候还要把bHasLDown设置为FALSE,因为本次是一个双击消息。
好了,下面的代码处理鼠标右键消息,处理方法与左键相同,不做详细解释。
经过上面的处理之后,就确定了鼠标消息类型,共有七个:左键被按下、左键抬起、左键双击、右键按下、右键抬起、右键双击、鼠标按键状态不变的鼠标移动。在操作系统核心中,鼠标消息也只有这七个。
确定消息类型后,还需要确定鼠标的当前位置。这很简单,在前面已经说过了,鼠标的当前位置记录在x和y变量里。鼠标消息类型和鼠标位置确定后,鼠标消息就完整了,这时候只需要把这个消息发送给操作系统核心即可:
上述代码中,dmsg是核心消息对象,把鼠标消息的相关参数记录到里面,然后调用SendDeviceMessage函数,把消息递交给DIM(DeviceInputManager)对象即可。
SendDeviceMessage函数比较简单,只是把设备消息发送给DIM对象,由DIM对象再进一步放到当前输入焦点线程的消息队列。
鼠标中断处理函数的最后,一定要恢复MsgCount的值为0,这样可保证下一次鼠标消息(三个连续中断)被有效处理。
至此,我们以鼠标驱动程序为例,展示了如何把鼠标的硬件输入传递到操作系统内核(DIM对象)。需要注意的是,设备驱动程序只是把硬件输入消息传递到内核了事,不会进一步跟踪消息的去向。即这个消息最终会被传递到哪个线程(或进程),在设备驱动层面是不知道的,这是操作系统内核的工作,在后面的章节中会有详细介绍。
对鼠标硬件的具体操作方法,无非是使用in或out指令,对硬件端口进行操作,这非常简单,因此本书没有详细介绍,感兴趣的读者可以查看本书的参考文献,或者到互联网上去搜索。键盘、触摸屏等硬件设备的输入机制是相同的,不同的是硬件相关的操作。
这个过程理解了,GUI部分的消息传递机制就算是理解了至少三分之一,很简单,是不是?后面部分会更简单。同时,Windows等操作系统基本也是这样处理的,是不是感觉到Windows也不是那么神秘莫测了?如果读者有这种感觉,那么本章一半的目的就算达到了,但愿如此。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。