(第七节)操作系统--设备驱动
写在最前面
前面我们介绍了CPU管理和内存管理部分,通过其完成了操作系统中的多进程视图的构建。
后续我们将要进行IO设备驱动的相关知识的学习。涉及到的IO设备主要有键盘、显示器、磁盘这三个,其中这一节介绍键盘和显示器驱动管理,会通过具体的示例来介绍操作系统是如果管理这两个设备的;而磁盘这一设备又涉及到文件系统,所以后面单独开一节内容进行介绍。
设备驱动的基本原理
不论是磁盘
还是键盘、显示器
,都是属于外部设备,而操作系统通过驱动管理这些硬件。所以在具体介绍这些设备的管理与实现流程前,先来介绍一些设备驱动的基本原理。
外设的工作原理
如图所示,即为外设的工作原理:
- CPU发送命令给外设,最终归结到执行指令“
out ax,端口号
” - 命令执行完毕后控制器执行中断处理
文件视图
为了让外设的使用更加方便,要提供一种统一的视图,这个视图就是
文件视图
。
问:为什么要提供统一的视图?
答:操作系统控制硬件执行就是向这些设备控制器的寄存器中写入指令,但是不同的外部设备往往其控制器也不同,写入前需要查寄存器地址、内容格式和语义等,这对于用户来说太复杂了。所以要引入文件视图将这个过程简单化、标椎化。
所以前面介绍的外设的工作原理还要
加上一条
:建立文件视图
如上图所示,其中的代码就是操纵外设的程序:通过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变量。不过要想真正了解其含义,还需要很多相关知识,下面我们具体介绍.
current—>filp[fd]
中,filp
中存放了当前进程打开的文件,而后可以通过文件句柄也就是fd获取这些文件。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代码主要用于获取缓存队列并判断是否满队列(缓存队列为
tty->write_q
) - 框2代码主要用于将内存中的字符串放到这个缓存队列里
- 框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函数主要完成以下三个工作:
- 根据按键扫描码获取其ASCII码
- 找到
tty_table[0].read_q
作为缓存队列 - 将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设备驱动的管理,从文件视图出发,借助
printf
和scanf
实现显示器和键盘的操作。
后面将会介绍磁盘的相关知识,包括文件系统部分内容 。