本文引用代码及图片均来自 李治军: 操作系统32讲
IO与CPU
键盘、鼠标以及磁盘等都属于IO设备,在低层次的视野中CPU通过PCI(Peripheral Component Interconnect)总线向IO设备的控制器发出指令,控制器完成真正的工作并发出中断信号通知CPU,CPU再处理中断就完成了一次和IO设备的交互
因为IO设备多种多样,不同设备有不同的数据格式以及其他特性,让用户直接操作底层的设备控制器是很困难的。不过,好在Linux中一切皆文件,操作系统将各个设备信息封装成统一的文件形式,用户可通过操作文件来操作各种IO设备
操作系统提供统一的open,read,write,close等函数对文件进行操作,read,write函数会根据设备对应的文件中包含的信息对操作进行解释,翻译成操作特定设备控制器的指令
计算机的主板上提供了多个接口(设备控制器),控制器中有状态寄存器、命令寄存器以及数据寄存器。IO设备接入接口并提供驱动程序以供CPU操作控制器中的寄存器,CPU就是通过读写寄存器来给控制器下指令
为了读写控制器中的寄存器,系统需要给寄存器分配地址,这里有两种编址方式
内存映射编址:将寄存器映射到内存中,CPU像访问内存一样访问寄存器IO独立编址:系统内有一个和内存空间独立的地址空间,需要使用专用的IO指令对寄存器进行操作
有些设备自身带有内存,这些内存也可以被编址从而被CPU读写
输出
从printf入手探究系统如何控制显示器输出,在 操作系统(2) 系统调用 中介绍过printf先将内容写入缓冲区,然后调用write系统调用,调用形式为write(1, buf, …),write在内核中的实现为sys_write
write(1, fd, xxx)中1对应sys_write的参数fd,既然是在显示器输出那么fd应该是显示器文件对应的索引,current是当前进程的PCB,所以sys_write要在PCB中取出显示器对应的文件信息
为什么显示器对应的文件在PCB的文件数组中的索引是1呢?这个问题可以追溯到系统刚启动的时候,在 操作系统 (6) 线程与进程的创建 讲到进程的创建最终都需要调用copy_process,在copy_process中当前进程的PCB是直接使用的父进程PCB内容,自然也使用了父进程的文件数组。依此类推,我们需要找到最初的父进程看看它打开了什么文件
在 操作系统(1) 系统的启动 讲到Main是第一个执行的C函数,Main创建了进程0和进程1(init进程),init进程负责其他所有用户进程的创建,即其他进程往上追溯最终都是init进程创建的
可以发现,init进程中打开了dev/tty0这个文件,这正是终端设备,这里是作为系统的标准输入文件,后面两个dup(0)是将文件复制两份作为标准输出和标准错误输出
接下来看看open函数做了什么,open在内核的实现为sys_open,可以发现sys_open就是在PCB文件数组中放入了文件,文件中包含inode
简单讲sys_open就是建立了这样一个链接
现在回到sys_write,从前面的分析知道sys_write的参数fd = 1代表标准输出文件的索引。先从文件中取出inode,然后根据inode的信息判断设备类型(字符设备、块设备等,磁盘是典型块设备)
Linux中IO设备有两个设备号,主设备号用于区分设备类型,次设备号用于区分同一类型的不同设备,这里inode->i_zone[0]是4,主设备号,指定字符设备
进入rw_char可以发现它只是简单地根据主设备号从函数表中找出相应的函数并调用(找到设备对应的驱动)
查看crw_table可以发现索引4对应的函数是rw_ttyx,rw_ttyx只是简单的判断read write并调用相应tty函数
进入tty_write,channel是rw_ttyx传过来的次设备号,这里次设备号是0,所以tty_write先在tty_table中根据次设备号获取对应tty_struct,然后不断从缓冲区中获取字符放入tty_struct对应的写队列中,最后调用tty_struct中的write函数
查看tty_table发现0索引对应的tty_struct中write函数是con_write
进入con_write发现出现汇编代码了,如果你对汇编课的作业还有印象应该记得往显示器上输出字符要用到的命令mov pos c,pos指向显存的位置。这里,con_write就是真正写显示器的地方
最后,整个print流程可以总结为下图
输入
键盘是典型的输入设备,从中断入手探究输入
键盘中断号是21,按键被按下后产生中断信号,收到中断信号后CPU从0x60端口读取扫描码,每个按键对应不同的扫描码,然后根据扫描码调用key_table中的函数
显示字符通常对应do_self函数,其他的比如Fn键对应func等
do_self会查表获取扫描码对应的ascii码,按下了shift键则查找shift_map,否则查找key_map,然后还会判断是否大写,ctrl是否按下等并做相应处理,最后调用put_queue
put_queue将得到的ascii码放到读队列头中,上层应用要读取输入从读队列中取就可以
将队列放入读队列后_keyboard_interrupt还需要将读取的字符回显在屏幕上,这是_do_tty_interrupt的工作,其实就是上一节所讲的将数据放入写队列中
键盘输入流程可总结为下图
总结
输入输出流程可总结为下图