3.2 Shell的消息处理过程
默认情况下,一个线程在创建的同时会创建一个消息队列,该消息队列与线程的控制结构(核心线程对象)进行绑定。其他的线程可以调用SendMessage函数向该线程发送消息,发送的消息类型是__KERNEL_THREAD_MESSAGE。线程可以调用GetMessage函数从自己的消息队列中获取消息,该函数以KernelThreadMessage的地址(指针)为参数。若该函数成功地从消息队列中获取了一个消息,则返回TRUE,KernelThreadMessage结构体里面存储了所获得的消息的内容;若当前线程消息队列为空,则该函数会阻塞当前线程。线程之间的消息有许多种类型,比如键盘消息、鼠标消息,以及用户自己定义的消息。在EntryPoint的实现中,首先打印出用户提示标识符,然后进入一个循环,循环调用GetMessage函数,从自己的线程队列中获得消息,若调用成功,则判断消息的类型是否是线程终止消息(一个线程可以给另外一个线程发送终止消息KTMSG_THREAD_TERMINAL,来终止此线程的运行)。若是,则跳出循环,从而导致线程运行结束,否则,调用DispatchMessage函数来分发消息(处理消息)。
GetMessage函数和SendMessage函数的实现在另外的章节中会有描述。Dispatch Message函数完成消息分发的功能。当前情况下,该函数的实现十分简单,只是把KernelThreadMessage中的参数分离出来,然后调用消息处理函数EventHandler。其中,EventHandler函数作为一个指针,传递给DispatchMessage函数,该函数的实现代码如下。
BOOL EventHandler(WORD wCommand,WORD wParam,DWORD dwParam) { WORD wr=0x0700; BYTE bt=0x00; BYTE Buffer[12]; switch(wCommand) { case MSG_KEY_DOWN: bt=LOBYTE(LOWORD(dwParam)); if(VK_RETURN==bt) { if(BufferPtr) DoCommand(); PrintPrompt(); break; } if(VK_BACKSPACE==bt) { if(0 !=BufferPtr) { GotoPrev(); BufferPtr--; } break; } else { if(MAX_BUFFER_LEN-1 > BufferPtr) { CmdBuffer[BufferPtr]=bt; BufferPtr++; wr+=LOBYTE(LOWORD(dwParam)); PrintCh(wr); } } break; default: break; } return 0L; }
在当前的实现中,EventHandler函数只处理键盘消息。其中,KernelThreadMessage的wCommand变量存储了具体的消息类型。目前情况下,定义了两种键盘消息:MSG_KEY_DOWN和MSG_KEY_UP,分别在用户按下键盘和放开按键的时候由键盘驱动程序发送。其中,EventHandler函数只处理键盘按下消息MSG_KEY_DOWN。WCommand变量标明了消息类型,而dwParam变量(KernelThreadMessage的另一个成员)则包含了对应于特定消息的相关参数。比如,在按键消息中,dwParam的最低一个字节存放了键盘被按下的ASCII码,这样EventHandler函数就可以根据dwParam的最低一个字节确定按下哪个键。根据所按下键的不同,分三种情况进行处理。
(1)若按下的键是回车键(VK_RETURN),则EventHandler函数判断当前键盘缓冲队列(CmdBuffer是一个键盘缓冲队列,BufferPtr则指明了当前队列中元素的数量)是否为空,若为空(BufferPtr为零),则只会换一行,重新打印出提示字符,然后返回。若不为空,则说明用户已经在提示符下输入了命令。这时候EventHandler函数会调用DoCommand函数来处理用户输入的命令。在目前的实现中,CmdBuffer是一个全局变量数组,因此DoCommand函数可直接访问该数组,不需要任何参数。DoCommand函数的实现在后面进行详细介绍。
(2)若按下的键是一个退格键(BACKSPACE),则判断当前命令缓冲区(CmdBuffer)是否为空,若为空,则不做任何处理;若不为空,则删除缓冲区的最后一个元素,并把显示器上的光标后移一格,最终的表现就是用户按BACKSPACE键,删除了输入的一个字符。
(3)若按下的键不是上述两者之一,则EventHandler函数会判断当前命令缓冲区是否满(长度是否达到了MAX_BUFFER_LEN,目前定义为512)。若已经满了,则不做任何处理,直接返回,否则,会把用户按下的键的ASCII字符存到CmdBuffer当中,并更新BufferPtr。
与DOS命令提示符类似,EventHandler函数实际上是实现了一个简单的行编辑器,用户可以输入字符,通过BACKSPACE键删除字符,并通过回车键确认输入的命令引发操作系统的执行。
上面讲到,若用户按下的键是回车键,且当前命令缓冲区不为空,则EventHandler函数会调用DoCommand函数,处理用户输入的命令。DoCommand函数会对CmdBuffer (全局的命令缓冲区)进行分析,然后根据命令的不同,调用合适的处理函数。该函数的实现代码如下。
VOID DoCommand() { DWORD wIndex=0x0000; BOOL bResult=FALSE; //If find the correct command object,then //This flag set to TRUE. BYTE tmpBuffer[36]; CmdBuffer[BufferPtr]=0x00; //Prepare the command string. BufferPtr=0; while((' ' !=CmdBuffer[wIndex]) && CmdBuffer[wIndex] && (wIndex < 32)) { tmpBuffer[wIndex]=CmdBuffer[wIndex]; wIndex++; } tmpBuffer[wIndex]=0; for(DWORD dwIndex=0;dwIndex < CMD_OBJ_NUM;dwIndex++) { if(StrCmp(&tmpBuffer[0],CmdObj[dwIndex].CmdStr)) { CmdObj[dwIndex].CmdHandler(&CmdBuffer[wIndex]); //Call the command handler. bResult=TRUE; //Set the flag. break; } } if(!bResult) { bResult=DoExternalCmd(&CmdBuffer[0]); //Call the external command parser. if(!bResult) { DefaultHandler(NULL); //Call default event handler. } } return; }
DoCommand函数是整个Shell命令处理的入口点。在目前的实现中,对于系统命令,分成两类。
(1)内部命令。操作系统自带的一些功能命令,比如设置时间、设置日期等。这些命令在运行的时候,直接在Shell线程的上下文中运行,不需要额外创建核心线程。这类命令的处理过程往往很短,且不会引起阻塞。
(2)外部命令。这类命令实际上是一些应用程序,由用户编写(也有几个是操作系统自带的),一般实现一些特定的功能。与内部命令不同的是,所有外部命令都需要额外创建一个线程,即外部命令在执行的时候,都有自己的上下文空间,且可以被阻塞。