写在最前面

前面我们介绍了CPU管理内存管理部分,通过其完成了操作系统中的多进程视图的构建。
后续我们将要进行IO设备驱动的相关知识的学习。涉及到的IO设备主要有键盘、显示器、磁盘这三个,其中这一节介绍键盘和显示器驱动管理,会通过具体的示例来介绍操作系统是如果管理这两个设备的;而磁盘这一设备又涉及到文件系统,所以后面单独开一节内容进行介绍。

设备驱动的基本原理

不论是磁盘是键盘、显示器,都是属于外部设备,而操作系统通过驱动管理这些硬件。所以在具体介绍这些设备的管理与实现流程前,先来介绍一些设备驱动的基本原理

外设的工作原理

如图所示,即为外设工作原理

  1. CPU发送命令给外设,最终归结到执行指令“out ax,端口号
  2. 命令执行完毕后控制器执行中断处理

文件视图

为了让外设的使用更加方便,要提供一种统一的视图,这个视图就是文件视图

:为什么要提供统一的视图?

:操作系统控制硬件执行就是向这些设备控制器的寄存器中写入指令,但是不同的外部设备往往其控制器也不同,写入前需要查寄存器地址内容格式和语义等,这对于用户来说太复杂了。所以要引入文件视图将这个过程简单化、标椎化

所以前面介绍的外设的工作原理还要加上一条建立文件视图

如上图所示,其中的代码就是操纵外设的程序:通过open()打开对应的设备文件。其具体的视图如下:

在使用了统一的文件视图后,用户对于外设的操作和对文件的操作是完全一样的,换句话说,用户可以忽略诸如端口号、指令格式等细节,因为操作系统会负责将这些设备文件展开成对设备的具体操作,形成一系列out语句。而完成这个展开工作就是外设管理的核心任务,也是我们下面要具体介绍的

显示器的驱动

下面我们会从printf出发,以具体的代码来分析显示器驱动执行的流程。

从printf开始

printf在执行时会先创建缓存buf并将格式化输出写到这个缓存,之后调用write(1,buf,...)

而根据第二节系统调用的学习,write最终会落到sys_write()上,前面我们介绍printf时到这里就停止了,因为后续内容就涉及到了硬件驱动所以就没有继续讲下去。下面我们就要由sys_write()继续探索printf的实现流程.

sys_write的代码1:寻找设备文件的FCB

sys_write第一部分代码就是要找到设备信息并将其保存起来,是通过上图所示的3条代码实现的。
这三句代码很简单,首先定义一个变量file,而后将其赋值为一个数组中元素的值,再之后取这个元素的某个值赋给inode变量。不过要想真正了解其含义,还需要很多相关知识,下面我们具体介绍.

  1. current—>filp[fd]中,filp中存放了当前进程打开的文件,而后可以通过文件句柄也就是fd获取这些文件。
  2. file->f_inode中,通过前面获取到的文件即可得到文件FCB,FCB中存储着设备信息

上两行代码实现了文件PCB的获取(到这里就完成了代码的介绍,下面内容为扩展知识),但是要想知道filp和inode的结构与值要从进程建立讲起.

扩展1:filp的结构

前面我们说过,filp存储着当前进程打开的文件,其中fd为1对应的文件是标准输出,这是所有进程都要打开的文件。而根据fork()建立进程的流程来看,子进程是通过复制父进程来建立的,所以最开始打开这个文件的就是1号进程,具体来说,是在init()程序中打开的这个文件。

如图所示,1号进程会打开tty0,之后两次dup(0)使fd为1、2的也指向tty0(dup指令用于将上一个fd的内容复制到下一个fd)。所以filp[1]就是获取到tty0这个设备文件也就是显示器(tty是终端设备)。

扩展2:inode的值

通过file->f_inode获取的值就是文件“/dev/tty0”的属性信息。

文件视图的大量分支

sys_write代码2:根据文件属性进行分支

当获取到文件属性后,就要根据此数据判断需要跳到的地方

sys_write函数接着向下执行,这一句为一个if判断语句。其作用是判断该文件是否为一个字符设备,如果是就跳转到rw_char函数执行。

显然,显示器也就是tyy0是一个字符设备,所以会跳转到rw_char()。我们需要关注的是此函数的前两个参数

  • WRITE用于标明write,便于后续调用
  • inode->i_zone[0]中存储着该设备的主设备号和次设备号

rw_char()函数与rw_ttyx()函数介绍

此函数是判断设备为字符设备时要跳转的函数,其具体代码如下:

上图中的两个函数是嵌套执行的,即:首先在rw_char()函数中通过使用主设备号查询crw_table获取到某函数地址(也就是rw_ttyx函数)

之后跳转到rw_ttyx函数执行,此函数会判断tty的类型READ还是WRITE,进而跳转到tty_read或者tty_write。其中tty_write就是操作显示器的真正函数


承上启下

前面我介绍了printf实现的部分流程:先使用write这一系统调用进入内核,而后通过中断、查表、跳转等操作执行sys_write函数。此函数会根据fd值获取设备文件与文件属性;而后通过此属性进行多次分支,最终到达tty_write
但是到达tty_write还不是最后的代码,只是此函数真正涉及到显示器的操作,其中还保护很多其他函数,我们来一一介绍

到达 mov ax,[pos]

tty_write函数介绍:将字符串放到缓存队列

tty_write函数的核心代码如下:

此函数主要完成以下三个功能

  1. 框1代码主要用于获取缓存队列并判断是否满队列(缓存队列为tty->write_q)
  2. 框2代码主要用于将内存中的字符串放到这个缓存队列
  3. 框3代码主要用于调用新的函数。其实tty->write(tty)调用的函数是con_write函数、

当printf的输出内容输出完毕或者对列已满时,调用con_write()

con_write函数介绍:将字符串真正打印到屏幕上

con_write函数的核心代码如下:

此函数用到了一段嵌入式汇编代码,如上图所示。其中c为要输出的字符串,是在缓存队列中取得的;attr为显示属性;最后将ax放到pos处完成打印。

其中pos为显存的当前光标位置,所以con_write函数的本质就是:mov ax,[pox]


printf使用总结

一个完整的文件视图路线printf->write->sys_write->rw_char->rw_ttyx->tty_write->write_q->con_write->mov ax,[pos]

键盘的驱动

前面我们介绍了显示器的打印在,是从上层到下层,由文件到驱动。而这里要介绍的键盘操作,是从下层到上层,从驱动到文件。

从键盘中断开始

键盘的故事应从键盘中断开始,0x21号中断就是键盘中断。

代码1:键盘中断初始化

对于上图代码,我们只需要知道框中的两行即可:

  • inb $0x60,%al:与out指令对应,inb指令用于从设备取出内容,这里是将键盘的0x60端口获取按键扫描码
  • call key_table(,%eax,4):根据按键扫描码调用不同的按时来处理各个按键。

key_table通过扫描码决定处理函数,绝大多数按键都是do_self()函数来进行处理。

代码2:do_self函数来处理按键

do_self函数主要完成以下三个工作:

  1. 根据按键扫描码获取其ASCII码
  2. 找到tty_table[0].read_q作为缓存队列
  3. 将ASCII码放到read_q

缓冲队列到scanf

将ASCII码放到缓冲队列后,就要返回文件视图

键盘中断初始代码在使用do_self处理完字符串后就要调用do_tty_interrupt返回文件视图;而后此函数返回需要调用copy_to_cooked()函数来处理缓存队列。这两个函数的代码如下:


其中copy_to_cooked函数的核心代码有三行:第一行从缓存队列取ASCII码;第二行将ASCII码放到tty->secondary队列;第三行代码会唤醒此队列上的进程。

而唤醒的这个进程就是用户发起的scanf程序,此程序会执行顺序read->sys_read->rw_char->rw_tyyx->tyy_read,而最后的tyy_read会使得此进程阻塞(如果secondary为空的话

所以:键盘中断scanf程序属于双向奔赴了,前者通过中断程序将键盘输入放到缓存队列并转移到secondary,后者则是从用户程序出发进入系统调用最后睡眠等待唤醒



写在最后:总结

本篇文章记录了IO设备驱动的管理,从文件视图出发,借助printfscanf实现显示器和键盘的操作。
后面将会介绍磁盘的相关知识,包括文件系统部分内容 。