本文引用代码及图片均来自 李治军: 操作系统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
键对应fun
c等
do_self
会查表获取扫描码对应的ascii码
,按下了shift
键则查找shift_map
,否则查找key_map
,然后还会判断是否大写,ctrl
是否按下等并做相应处理,最后调用put_queue
put_queue
将得到的ascii码
放到读队列头
中,上层应用
要读取输入从读队列中取就可以
将队列放入读队列后_keyboard_interrupt
还需要将读取的字符回显
在屏幕上,这是_do_tty_interrupt
的工作,其实就是上一节所讲的将数据放入写队列
中
键盘输入流程可总结为下图
总结
输入输出流程可总结为下图