哈工大os学习笔记十三(IO/显示器/键盘)
一、 I/O与显示器
1.让外设工作起来
使用外设 (1)向外设对应的端口地址发送 CPU 命令; (2)CPU 通过端口地址发送对外设的工作要求,通常就是命令“out ax, 端口号”,其中 AX 寄存器存放的就是让外设工作的具体内容,有时候不止一条指令; (3)外设开始工作,工作完成以后使用中断机制告诉 CPU,CPU 会在中断处理程序中处理外设的工作结果。
向设备控制器的寄存器写不就可以了吗? 因为直接对不同的设备进行操作很麻烦,需要查寄存器地址、内容的格式和语义······ 所以操作系统要给用户提供一个简单 视图—文件视图,这样更方便高效。
2.文件视图
文件视图 操作系统将所有外设都统一抽象成一个文件,程序员通过文件接口 open、read、write 来使用这些外设。
int fd = open(“/dev/xxx”);
for (int i = 0; i < 10; i++) {
write(fd,i,sizeof(int));
}
close(fd);
(1)不论什么设备,操作系统操作外设使用的都是open, read, write, close 。不同的只是调用的名称不一样,例如int fd = open(“dev/***”) 操作系统为用户提供统一的接口! (2)不同的设备对应不同的设备文件(/dev/xxx),根据设备文件找到控制器的地址、内容格式等等!
3.中断处理
中断处理:当CPU(中央处理器)执行一条现行指令的时候,如果外设向CPU发出中断请求,那么CPU在满足响应的情况下,将发出中断响应信号,与此同时关闭中断,表示CPU不在受理另外一个设备的中断。这时,CPU将寻找中断请求源是哪一个设备,并保存CPU自己的程序计数器(PC)的内容。然后,他将转移到处理该中断源的中断服务程序。CPU在保存现场信息,设备服务(如交换数据)以后,将恢复现场信息。在这些动作完成以后,开放中断,并返回到原来被中断的主程序的下一条指令。
4.小结
总的来说外设驱动的3件事: 1. 发出out指令(最核心的指令)
- 形成文件视图
- 形成中断处理
整理 操作系统为了让外设工作起来,首先用CPU向控制器中的寄存器发出指令;然后因为各个外设实现比较复杂,所以引入文件视图的概念,将每个外设都定义为一个文件,采用文件的形式进行封装;最后设备完成了读写操作,进行中断操作。
以上就是外设驱动的概念,接下来看具体的代码加深体会理解
5.具体实现
5.1 切入点
-
触发这一切的点——调用printf()函数,想在显示器上打印东西。 -
我们已经知道,printf库展开的部分——先创建缓存buf将要打印的东西都存到那里,然后调用write(1,buf,…) -
系统调用 write(1, buf, count),write 的内核实现是 sys_write,sys_write 首先要做的事就是找到所写文件的属性,即到底是普通文件还是设备文件。 -
如果是设备文件,sys_write 要根据设备文件中存放的设备属性信息具体分支到相应的操作命令中。 -
file的目的是得到inode,显示器信息应该就在这里
5.2 current->filp的解释
- fd=1的filp指针从哪里来?
- 因为是被current指向,所以是从fork中来
- 显然是拷贝来的,那么是谁一开始打开的?
- shell进程启动了whoami命令,shell是其父进程,所有进程都是0号进程创建1号进程做出shell,在做的。
- 分析shell,在系统初始化的时候,果然打开了一个文件,并且拷贝了两份。对应的为0,1,2。1也是打开一个文件。
- 所以write里面的1是对应的设备文件是dev/tty0,而tty就是终端设备
- sys_dup()的主要工作就是用来“复制”一个打开的文件号,使两个文件号都指向同一个文件。
- dup参考:https://www.cnblogs.com/pengdonglin137/p/3286627.html
- current->filp 数据中存放当前进程打开的文件,如果一个文件不是当前进程打开的,那么就一定是其父进程打开后再由子进程继承来的。
- 在 init 函数中我们调用 open 打开一个名为“/dev/tty0”的文件,由于这是该进程打开的第一个文件,所以对应的文件句柄 fd = 0,接下来使用了两次 dup,使得 fd = 1,fd = 2 也都指向了“/dev/tty0” 的 PCB(文件控制块)。
5.3 open系统调用
- 一步一步向下跟踪,慢慢细化到具体的设备
- 首先根据 inode 中的信息判断该文件对应的设备是否是一个字符设备,显示器就是一个字符设备
- 如果是字符设备,sys_write 就要分支到函数 rw_char() 中去执行,其中 inode->i_zone[0]
中存放的就是该设备的主设备号和次设备号。 - rw_char() 中以主设备号(MAJOR(dev))为索引从一个函数表 crw_table 中要找到和终端设备对应的读写函数 rw_ttyx,然后调用这个函数。
-
函数 rw_ttyx 中根据是设备读操作还是设备写操作继续分支。 -
显示器和键盘合在一起构成了终端设备 tty,显示器只写,键盘只读。此处是显示器,所以对应写操作,将调用函数 tty_write()。 -
tty_write()是实现输出的核心函数 -
因为CPU速度快,往显示器上写速度很慢,所以先将内容写到缓冲区里,即一个队列中,等到合适的时候,由操作系统统一将队列中的内容输出到显示器上。 -
如果缓冲区已满,就睡眠等待。这里就生产者消费者联系起来。
5.4 tty_write()函数
-
如果队列没有满,那么就从用户缓存区读出字符 c。 -
进行一些判断和操作后,将字符 c放入队列tty->write_q 中。 -
输出完事(读出的字符是 \r )或 写队列满后,跳出循环。 -
调用 tty->write()。 -
在 tty 结构体中可以看到 write 函数。 -
根据对 tty 结构体的初始化可以看出,tty->write 调用的函数是 con_write。(con - console) -
在 con_write 中,先从缓冲区中取出字符,然后将字符 out 到显示器上。 -
内嵌汇编部分: ah( ax 的高 8 位)里放属性(颜色,闪烁),al(ax 的低 8 位)里放字符,然后将ax里的内容out就行。 -
这里用的是mov ax,pos ,将字符打印在了显示器上(将 printf 要显示的字符放在显存的当前光标位置处)。 -
pos是显卡的寄存器,某些外设的控制器中的东西可以统一和内存编址,这时候寻址用mov,如果是独立编址的时候,用out。 -
初始化以后 pos 就是开机以后当前光标所在的显存位置。 -
con_write 中每输出一个 ax 都让 pos 加 2,是因为 ax 就是两个字节。 -
只有一句话:mov pos -
完成显示中最核心的秘密就是 mov pos, c -
pos的修改: pos+=2 为什么加2? 屏幕上的一个字符在显存中除了字符本身还应该有字符的属性(如颜色等)
printf的整个过程: 将mov pos, c 指令包装成 统一的文件view 并利用了缓冲技术和 消费者和生产者同步的机制
二、 键盘
终端设备包括键盘显示器
- out指令 发 命令
- 形成统一的文件视图
- 进行中断处理
CPU 向设备的写的最终命令是 out,那么读设备的第一步就应该是 in,即从设备上取出内容交给 CPU。 小常识:按下键盘会产生 0x21 号中断。
1.切入点—键盘
对于使用者来说,要敲键盘,看到结果 对于操作系统来说,要等待敲键盘,敲了键盘,就发生中断,所以应该从中断开始跟踪分析。
- 中断处理函数中,先从键盘的 0x60 端口上获得按键扫描码,然后要根据这个扫描码调用不同的处理函数 key_table()来处理各个按键。
2.处理扫描码
-
从key_map中取出ASCII码 -
put_queue将ASCII码放到? con.read_q -
处理扫描码key_table+eax*4 -
绝大多数按键(如字母键、数字键等)都用 do_self 函数来处理。(其他特殊按键由func 等其他函数来处理) -
函数 do_self 要做的第一件事就是从键盘对应的 ASCII 码表(key_map)中以当前按键的扫描码(存在寄存器 EAX 中)为索引找到当前按键的 ASCII 码。 -
第二件事就是找到 tty 结构体中的 read_q 队列。键盘和显示器使用了同一个 tty 结构体 tty_table[0],只是键盘使用的读队列,而显示器使用的写队列。 -
得到了 ASCII 码并找到键盘对应的缓冲队列read_q 以后 -
接下来就是第三件事了,即将 ASCII 码放到缓冲队列 read_q 中。 3.处理ASCII码
- 将 ASCII 码放到缓冲队列 read_q 中后,可显示的字符要回显,先放在缓冲队列 write_q 中,再显示到屏幕上。
实验七:按下F12,输出变成 *
写myfunc函数 { if Flag ==0 } tty_write { if Flag ==1 c = ‘*’; }
参考博客: [https://blog.csdn.net/tfnmdmx/article/details/119650951] [https://blog.csdn.net/qq_53111905/article/details/119737978]
|