一个操作系统的实现(9):输入输出系统

I/O的全称是“Input/Output”,即“输入/输出”。其中“输入”指的是键盘,“输出”则指的是显示器。


键盘


在8259A的中断例程中有一个“键盘中断”。其实键盘中断不仅仅是个中断号,它还涉及到其它的一些信息,比如哪个键发生了什么操作,以及键盘上其它键的状态等等。这又涉及到两块芯片:8042和8048。


8048存在于键盘中,作用是将收到的键盘信号传给8042。8042接收到信息后,会产生一个扫描码,然后给8259A发送一个中断。此时如果8048再向8042发送键盘信号,则8042不予理会。只有将扫描码从8042读出来才能继续接收下一个扫描码。读取60端口得到的值即为扫描码。


按下键盘上任一一个键都会产生一个唯一的扫描码,叫做通码;而键弹起时则会产生断码。通码和断码是不同的,并且除了Pause键,所有的键都对应一个唯一的通码和断码。其实,按下Shift+a得到A完全是软件实现的,在硬件层面上得到的结果实际上就是:按下Shift、按下a、放开a、放开Shift。


从8042得到的键需要进行判断。如果是Shift、Ctrl、Alt这样的键,则修改将其对应的状态码以便支持大小写字符或各种其它操作。有一部分键是以“E0”开头的双字节扫描码,对于这样的键,读到E0后不返回,紧接着再等待一个字节即可。


我们可以为最终得到的字符序列提供一段内存区域来保存它,这个地方叫做键盘缓冲区。书中的缓冲区使用的是队列数据结构,即用head和tail来表示缓冲区中字符串的位置。这个队列是一个环形队列,当指针到达缓冲区末尾时要将指针移到开头。


显示器


显示器上显示的内容实际上是映射到一段内存上的,这段内存叫做显存。开机时,显存的模式为文本模式,高度为25,宽度为80。显存占用的内存固定为0xB8000到0xBFFFF,两个字节表示一个字符,低字节表示ASCII码,高字节表示字符的颜色等属性。但是80*25*2=4000字节,而显存有32K,这就意味着我们可以存放将近8个屏幕的信息。如果我们给每个屏幕分配10KB的空间,还可以实现滚屏的功能。


显卡也有自己的寄存器,要操作这些寄存器可以通过端口来实现。比如我们可以设置光标的位置,让它跟随我们的键盘输入;还可以重设显存的起始地址,实现屏幕滚动,或显示不同的屏幕。


TTY


tty这个词源于TeleTYpewriter,即电传打字机。在Unix的世界里,tty用于表示终端,按下Alt+F1、Alt+F2等键可以切换到不同的终端。现在我们就可以实现这样的功能。


切换屏幕很简单,只需要将显存的起始地址指向相应的位置就行了。问题在于键盘。由于键盘只有一个,所以只有当前tty才有权读取键盘缓冲区。每个tty在输出时,只能输出到属于自己的那部分内存区域中。


完善键盘处理


回车键:检测到回车符后,将光标移到下一行的开头,然后判断光标是否移出了屏幕。如果是,则滚动屏幕。


退格键:将光标回退一个字符,并在那里写一个空格以擦除字符。同样也要判断光标是否移出了屏幕。


CapsLock、NumLock、ScrollLock:设置三个全局变量以保存这三个键的状态。每当按下这个键时,切换状态,对8042的端口发送与LED指示灯有关的控制信号即可。


关于tty、进程和特权级


tty并不像进程一样可有可无,它肩负着I/O的重任。但是它的重要性又比不上系统内核,因此它叫做“任务”进程。任务进程一般放在特权级为1的位置。


创建进程时,由于进程只能在一个tty内工作,所以要在进程表中标记出它所拥有的tty。


可变参数


C语言中的printf函数的原型为


void printf(const char*, ...)
请注意结尾的省略号,它表示函数的参数个数不固定。不固定的意思就是,你不知道参数有多少,更不知道是什么。


C语言中的函数,如果不加声明,统一遵循C调用约定:参数从右向左入栈,并且由调用者清理堆栈。从右向左入栈使得第一个参数,即开头的格式字符串,能够第一个弹出栈,而printf就可以分析这个字符串,进而得知后面的参数会有多少个。而调用者清理堆栈的好处就是,即使你不小心将调用函数的语句写成了这样:




printf("%d",1,2);
也不用担心堆栈错误。而被调用者清理堆栈的缺点就是,当printf分析字符串后,会认为后面只有一个参数,清理堆栈时只清理了连同字符串的两个参数,还剩余一个“2”留在了堆栈里,于是不可预料的错误就发生了。

你可能感兴趣的:(一个操作系统的实现(9):输入输出系统)