实现OS记录:输入输出
0x00
看似最简单的输入输出其实要到这里才实现
0x01 输出
其实在之前就实现了输出如下,就是与 I/O 接口交互以及写显存的过程
1 | TI_GDT equ 0 |
从中导出的几个函数就是最基本的输出函数底层实现如下
1 |
|
_Generic 宏意思是在编译器根据类型分发编译,毕竟 c 语言没有原生的重载机制。为了方便我们使用这样一个宏。(之前的出现的 print 函数也是用类似的宏实现的).
虽然这些输出函数之前就实现了,但是并不线程安全,所以在实现线程调度和锁机制后,专门再写了线程安全的版本:
1 |
|
很简单的实现,就是输出前上锁输出后释放锁保证输出的原子性。这样就无需进行大开销的开关中断了。为了方便同样用_Generic来封装这四个函数。
0x02 输入
既然实现了输出到终端,自然还要实现一下从键盘输入。
当我们按下键盘上某个按键,本质只是向计算机主板某个键盘相关的处理硬件发送扫描码,然后会转发到 I/O 接口,然后 cpu 触发键盘中断,由键盘中断处理程序来处理我们的输入。
扫描码就是给每个按键的按下与弹起编码,分为通码(按下)与断码(弹起)。在常见编码方式中,断码 = 通码 + 0x80。
所以我们写一个键盘驱动,要做的事就几件:
- 给扫描码和对应数据建立一个映射表
- 键盘中断处理程序
- 输入的缓冲区的维护
第一点直接打表
1 | // 用转义字符定义一部分控制字符 |
第二点,键盘中断处理程序的工作就是处理接受到的扫描码处理各种情况:比如按下 ctrl+a时,是一串 ctrl 的通码加上 a 的通码,然后是 ctrl 的断码和 a 的断码。等等情况做出处理。
1 | // 定义以下变量来记录相应键是否按下的状态, |
第三点,使用一个环形队列来作为数据缓冲区,并且其中需要线程处理:
- 缓冲区为空,有线程想读取数据,则先标记并阻塞这个线程等待数据输入
- 缓冲区满了,有线程想输入数据,则先标记并阻塞这个线程等待缓冲区空间…吗?
注意这里往键盘数据缓冲区写数据是键盘中断处理程序做的,键盘中断的时候阻塞线程,还记得thread_block会调用task_schedule马上切换任务,这时候的上下文并不是线程上下文而是出于键盘中断,会导致错误!!
所以缓冲区满了的时候只能丢弃数据而非阻塞线程,根本原因就是输入时键盘中断做的,读取是线程自己读取的(后续用软中断实现系统调用,上下文环境是进程/线程的上下文,和键盘中断这种硬件中断的中断上下文不同)。
所以键盘中断在使用从数据缓冲区读取数据的函数前一定要判断缓冲区没有满,如果不判断一直输入的话,就会发生线程调度的错误,如下

数据缓冲区的io队列如下
1 | // 初始化io队列ioq |
0x03 测试
main.c
1 |
|
运行后按住 r 键不松效果如下

两个线程交替读取到输入的数据,符合预期


