L26 I/O与显示器
使用外设: (1)向外设对应的端口地址发送 CPU 命令; (2)CPU 通过端口地址发送对外设的工作要求,通常就是命令“out ax, 端口号”,其中 AX 寄存器存放的就是让外设工作的具体内容; (3)外设开始工作,工作完成以后使用中断机制告诉 CPU,CPU 会在中断处理程序中处理外设的工作结果。
向设备控制器的寄存器写不就可以了吗? 因为直接对不同的设备进行操作很麻烦,需要查寄存器地址、内容的格式和语义, 所以操作系统要给用户提供一个简单视图—文件视图,这样更方便高效。
文件视图,操作系统将所有外设都统一抽象成一个文件,程序员通过文件接口 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 - 操作系统为用户提供统一的接口! (2)不同的设备对应不同的设备文件(/dev/xxx),根据设备文件找到控制器的地址、内容格式等等!
系统调用 write(1, buf, count),write 的内核实现是 sys_write,sys_write 首先要做的事就是找到所写文件的属性,即到底是普通文件还是设备文件。 如果是设备文件,sys_write 要根据设备文件中存放的设备属性信息具体分支到相应的操作命令中。
sys_dup()的主要工作就是用来“复制”一个打开的文件号,使两个文件号都指向同一个文件。 参考https://www.cnblogs.com/pengdonglin137/p/3286627.html
current->filp 数据中存放当前进程打开的文件,如果一个文件不是当前进程打开的,那么就一定是其父进程打开后再由子进程继承来的。 在 init 函数中我们调用 open 打开一个名为“/dev/tty0”的文件,由于这是该进程打开的第一个文件,所以对应的文件句柄 fd = 0,接下来使用了两次 dup,使得 fd = 1,fd = 2 也都指向了“/dev/tty0” 的 FCB(文件控制块)。 首先根据 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速度快,往显示器上写速度很慢,所以先将内容写到缓冲区里,即一个队列中,等到合适的时候,由操作系统统一将队列中的内容输出到显示器上。 如果缓冲区已满,就睡眠等待。
如果队列没有满,那么就从用户缓存区读出字符 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 就是两个字节。
L27 键盘
CPU 向设备的写的最终命令是 out,那么读设备的第一步就应该是 in,即从设备上取出内容交给 CPU。 小常识:按下键盘会产生 0x21 号中断。 中断处理函数中,先从键盘的 0x60 端口上获得按键扫描码,然后要根据这个扫描码调用不同的处理函数 key_table()来处理各个按键。 绝大多数按键(如字母键、数字键等)都用 do_self 函数来处理。(其他特殊按键由func 等其他函数来处理) 函数 do_self 要做的第一件事就是从键盘对应的 ASCII 码表(key_map)中以当前按键的扫描码(存在寄存器 EAX 中)为索引找到当前按键的 ASCII 码。 第二件事就是找到 tty 结构体中的 read_q 队列。键盘和显示器使用了同一个 tty 结构体 tty_table[0],只是键盘使用的读队列,而显示器使用的写队列。
得到了 ASCII 码并找到键盘对应的缓冲队列read_q 以后, 接下来就是第三件事了,即将 ASCII 码放到缓冲队列 read_q 中。 将 ASCII 码放到缓冲队列 read_q 中后,可显示的字符要回显,先放在缓冲队列 write_q 中,再显示到屏幕上。
实验七:按下F12,输出变成 *
写myfunc函数 { if Flag ==0 } tty_write { if Flag ==1 c = ‘*’; }
|