对于输入输出系统的理解
目录
键盘的输入如何被系统识别
键盘中断
接收队列
终端的概念
多终端如何实现
TTY私有接收队列
控制字符的处理
将TTY纳入文件系统
进程和TTY绑定
读写对象
字符设备文件
TASK_TTY
键盘的输入如何被系统识别
键盘的按键信号被8048键盘编码器处理后发送给8042键盘控制器,8042接收和解码8048传递过来的数据。然后8048给8259A一个中断信号,表明用户有键盘上的输入动作了。8259A再给cpu一个信号,cpu响应后会调用键盘中断处理程序。至于为什么cpu能找到键盘中断而不会错误地调用硬盘中断。
- IDT表中有各序号对应的处理函数,只要序号正确cpu就能找到正确的处理函数
- 序号是8259A给的,8259A通过初始化时候的设置,确定好每个中断引脚对应的序号,发生中断的时候就可以得到正确的序号了。
键盘中断
键盘中断用来做什么事呢?很简单,就是从0x60端口读数据就可以了。另外如果不把0x60端口的数据读出来,那么下一次敲击键盘的信号就不能被8042接收了。
接收队列
键盘中断程序把接收到的数据放到哪里呢?应该放到一个全局变量里面,因为中断处理程序读到内存的数据是要给其他进程用的。这样的话,自然是用一个循环队列来表示比较好了。也就是常说的接收队列。
终端的概念
我不知道其他人是如何理解终端的。我的理解就是有输入的设备和输出的设备就算终端了。为什么叫TTY,历史遗留的问题吧,据说是电传打印机的名字。
多终端如何实现
其实多终端只是对于使用者来说的,按下Alt+F2就换到一个全新的输入窗口了,然后再按Alt+F1又变回来了,似乎真的是两个不相关的显示器一样。
以前我也好奇这是怎么实现的。但是那个时候学习重心都放在文件系统上了,精力有限,就没有再查阅资料了。正好在实践过程中看到多终端的实现,作者一讲原理,恍然大悟。
因为向显示器写数据的函数还是我们自己实现,只要我们组织好逻辑,给每个终端规划不同的显存,在调用写函数的时候写到各个终端自己的显存范围内,写满了就回滚,这样就实现了多终端了。
像书中的例子,32K的显存,给每个终端分10K的显存还有剩余的。第一个终端写0-10K的空间;第二个终端写10-20K的空间。那我们在设计TTY结构体的时候,只需要多设计一个CONSOLE结构体用来描述每个终端显存的信息,例如本终端显存起始地址、当前光标位置。
TTY私有接收队列
多个终端了,那么似乎一个键盘缓冲区不够用啊,比如说这个换行符该让哪个控制台换行呢?所以TTY结构体中又需要加上各自终端的接收队列,从键盘接收队列中读取数据然后放到自己的接收队列中,交给输出函数来输出就可以了。
在处理TTY的函数中,我们需要将键盘接收队列中的数据取出来放到当前控制台对应的TTY中的接收队列中就可以了。
控制字符的处理
但是键盘的按键不仅仅是英文字母和数字这么简单,还有很多控制键,例如左右Shift、Ctrl、Alt,CapsLock、Backspace、回车。都需要单独处理。需要一点点耐心逐步实现它们。
将TTY纳入文件系统
前面我们了解输入输出,但是仅仅做到自顾自的接收键盘输入然后打印到不同的显存区域中。似乎和我们常用的linux不太一样。常用的好像是通过文件描述符来操作终端呢。这就需要将TTY纳入文件系统了。
将TTY纳入文件系统后,TTY就不能像开始那样自己一个人玩那样,碰见键盘中断就接收然后打印出来。就要受制于文件系统提供的接口了。
进程和TTY绑定
这个时候TTY就不是独立的了,它必须被显式的调用才能进行终端的读写操作。这样一来,TTY就必然要和进程产生联系了,在TTY的结构体中也就要加入引起读写操作的进程号,以便在操作结束后能通知进程。
读写对象
此时TTY的读写函数要处理的不仅仅是键盘接收队列和显存了。它还要面对进程的需求。
对于用户的读请求,TTY任务需要不断的循环读键盘接收队列,然后回显到控制台,还要把有效字符放到用户提供的缓冲区中,读到“\n”字符则向发送请求的进程发送读取完成的消息。
对于用户的写情趣,TTY任务需要读取用户提供的缓冲区中的字符读取出来打印到控制台上。
字符设备文件
字符设备文件到底是什么呢?
假设没有这些设备文件,试想一下要怎么读写像终端这样的硬件设备呢?可能内核要单独提供接口,类似于read_TTY(int TTY_num,void *read_buffer,int size)。仔细看和普通的读写接口有什么不同。第一个参数是TTY的序号,而不是通常所说的文件描述符了。这意味着什么?意味开发人员必须要记住0号是哪个终端,1号是哪个终端,这样真的很头疼。如果再多几个硬件设备岂不是很郁闷了?除了极少数的一撮人有强大的记忆力外,一般人还是只擅长处理有意义的字符的。那么文件名是个极好的索引了,不会重复表示多个设备,也具有辨识意义。那就把设备也纳入文件系统吧,这样用来表示设备的文件就是设备文件了啊。
设备文件如何配合文件系统来工作呢?现代的文件系统自然是极其复杂的,已经不是一个人能面面俱到的掌握了。但是简单的文件系统的实现流程还是可以梳理顺畅的。我们想想文件系统里面inode结构体里面有个i_mode元素,这个就是用来分用文件处理流程的。如果这个文件的mode表明是个设备文件,那么其实这个文件在硬盘上没有真实的数据的,我们需要让文件处理流程走入和mode表明的设备处理流程中了。比如说像字符设备,mode== I_CHAR_SPECIAL这个时候我们就可以调用字符设备的处理函数了,那处理哪个TTY呢?上面我们看到读TTY设备的时候要提供TTY的序号,这个序号就是我们TTY结构体数组的下标,用来标示我们操作的是哪个TTY。那这个号存放在哪里?不得不说以前的操作系统设计师真是无所不用其极,还记得inode中有个成员i_start_sect吗?原本它是用来表示普通文件的数据存储的起始扇区的,现在用来存放设备号了!其实仔细一想,真是合情合理,既然这个文件在硬盘上没有真正的数据了,那这个字段不就是没有用了,挪作他用岂不是很好。
TASK_TTY
上面总结了TTY要处理的任务,以及如何和进程、文件系统关联起来。到这个时候,其实TASK_TTY的整个循环流程已经出来了。
- 首先是依次调用终端,如果是当前终端那么读键盘接收队列到TTY的私有缓冲区
- 将1中读取到的字符回显到控制台上,并且将字符都存放一份到进程提供的缓冲区中
- 等待其他进程发送读写请求
- 对于读请求,将希望读的进程号记录到TTY结构体中,以便第2步用
- 对于写请求,将进程提供的缓冲区中的字符依次打印到控制台