4. 输入输出系统(键盘和显示器交互)

输入/输出系统(包含键盘和显示器与用户交互)

经过这一章,操作系统才拥有和用户交互的接口,用户才能通过键盘操作它,并在显示器获取结果。

先是键盘

键盘中断对应的是8259A的IRQ1,外部硬件中断处理的框架已经搭好,现在需要做的只是写好中断处理程序并把它的地址填进函数指针数组即可。

键盘初体验

先写键盘中断处理函数(新建keyboard.c)

PUBLIC void keyboard_handler(int irq)
{
    disp_str("*");
}

然后设置函数指针数组(keyboard.c)

PUBLIC void init_keyboard()
{
    put_irq_handler(KEYBOARD_IRD,keyboard_handler);/*设定键盘中断程序*/
    enable_irq(KEYBOARD_IRQ);/*开键盘中断*/
}

就是简单的接收键盘有输入就显示一个*.而且你会发现只能显示一次,后面就会显示键盘缓冲器满。

键盘敲击过程

现在主流的键盘都是USB的了,曾经还有AT和PS/2键盘。

graph LR
8048-->8042
8042-->8259A
  • 8048键盘编码器:获取键盘输入,并传适当的数据给计算机
  • 8042键盘控制器:接收和解码来自键盘的数据,并与8259A和软件通信

键盘敲击:动作+内容,所以8048不仅要反映是哪个键,还要反映是什么动作

  • 动作:按下、保持按下、放开
  • 内容:即键盘上不同的键

敲击键盘产生的编码:扫描码Scan code,分成两类

  • Make Code:按下或者保持住时发送
  • Break Code:弹起时发送

有三套:Scan code set 1、set 2、set 3
现在主流的是set2,比AT更老的XT键盘则是用set1

整个过程:

8048检测到一个键的动作->把相应扫描码发给8042->8042把它转成相应的Scan code set1扫描码->将其放到输入缓冲期中->8042告诉8259A产生中断(IRQ1)

此时,如果有新的按键被按下,8042将不再接收,直到缓冲区被清空(所以这个需要我们在中断处理程序中读取清空)

  • 那么如何从缓冲区读取扫描码?看8042
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pHsPnFDP-1575882874068)(http://yeholdon.top/wp-content/uploads/2019/12/IMG_20191206_165240-scaled.jpg)]

也就是说也是用in指令来读取。in_byte(0x60)

  • 值得注意的是:在keyboard_handler里in_byte后,可以持续读取键盘输入了,而且按一次出现两个星号(keyboard_handler被调用两次,两次中断)因为一次敲击包括按下Make Code和弹起Break Code两个动作

  • 打印读取的扫描码:获取in_byte(0x60)的返回值(u8)
    4. 输入输出系统(键盘和显示器交互)_第1张图片

  • 然后将得到的扫描码对照scan code set 1就能得到按键内容。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w2HbRHEA-1575882874071)(http://yeholdon.top/wp-content/uploads/2019/12/IMG_20191206_170835-scaled.jpg)]

值得注意的是,虽然键盘支持的是Scan code set2,但是最后传入计算机的又是scan code set1.这时基于为XT键盘写的程序兼容性而考虑的。

用数组来存scan code set 1表

简单来说就是扫描码和具体键盘字符的映射表。
写在keymap.h头文件中。其中每3个值是一组(MAP_COLS=3),分别为单独按、Shift+某键和有0xE0前缀的扫描码对应的字符。Esc,Enter等被定义成了宏,宏的具体数值无所谓,只要不造成冲突混淆即可,让OS认识即可。注意0xE0和0xE1开头的扫描码要区别对待。

  • 这样问题就出现了,用户输入一个想要的字符,可能会是2个扫描码,也可能是4个扫描码,而8042的缓冲区大小只有一个字节,所以实际输入一个字符可能会产生2次4次等不同的中断次数,另外,还有可能用户先按了shift又松开了,我们必须将整个连续合法的码序列作为整体来处理。
  • 为此,需要将收到的扫描码保存起来,然后结合后面的输入一起解析用户的意图。
  • 因为处理起来比较复杂,所以如果我们把处理扫描码的部分都放在keyboard_handler里的话,这个函数会变得很大,结构不清晰。为此,参考Minix,建立一个缓冲区,让keyboard_handler每次收到的扫描码都放入这个缓冲区,然后建立一个新的任务专门用来解析它们并做相应处理。

键盘输入缓冲区

缓冲区用结构体s_kb来定义:

/* Keyboard structure, 1 per console. */
typedef struct s_kb {
	char*	p_head;			/* 指向缓冲区中下一个空闲位置 */
	char*	p_tail;			/* 指向键盘任务应处理的字节 */
	int	count;			/* 缓冲区中共有多少字节 */
	char	buf[KB_IN_BYTES];	/* 缓冲区 */
}KB_INPUT;

结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4HPKpKjP-1575882874072)(http://yeholdon.top/wp-content/uploads/2019/12/IMG_20191206_190617-scaled.jpg)]
注意这个是循环的,p_tail到达末尾后指针移到开头

  • 有了缓冲区,那就是将传入的扫描码添加到队列中。这部分是在keyboard_handler里实现的。如果缓冲区已满,则直接丢弃新的码。

用新加的任务处理键盘操作

前面设立了键盘缓冲区,就是为了方便后面针对一串扫描码串来统一处理。而且,这样就可以使键盘中断和码处理不是严格的同时进行的,这样做的好处就是可以用一个单独的任务(进程)来处理键盘操作,而无需全部都写在键盘中断处理程序里。而且,虽然不是有中断马上就处理,但是因为进程的切换很快,这个时间差可以忽略。

  • 新的任务task_tty()就是循环地调用keyboard.c里定义的keyboard_read()函数。
  • keyboard_read()函数里要注意读取的整个过程是要连贯的,不应该受到打扰,所以在读取前后分别打开和关闭中断。

解析扫描码

前面只是搭好了框架,可以正常获取扫描码了,下面开始真正的解析算法。较为复杂。。。具体工作就是扩充keyboard_read()

显示简单的字符(无shift等)

由简到难,先处理小写字母,再处理组合输入等情况

break code 是make code和0x80 或or的结果;make Code 是break code 和0x7F and与的结果

处理shift、alt、ctrl等组合键

这三个每个都在键盘上有两个键,而且有的时候左右shift是不同的,所以用6个int来表示它们的状态。再加上caps lock ,num lo还有scroll lock等表示状态的键。

  • 如果一个完整的操作还没结束(比如一个2字节的扫描码还未完全读入,则key赋值为0,等到下一次keyboard_read()被执行时再继续处理。也就是说,目前的情况是,一个完整的操作需要在keyboard_read()多次调用时完成。
处理剩下的所有按键

目前已经可以处理大部分按键了,但是还存在两个问题:

  1. 更复杂的扫描码,比如超过3个字符
  2. F1等功能键,系统把它们当成可打印字符处理,打印的还是奇怪的符号。
  • 先解决第一个问题

    • 目前的实现是,一个完整的读取操作要调用多次keyboard_read()完成,因为它一次只读取一个字符。这样还得加全局变量,记录上一次读取的结果,逻辑上比较难以理解。
    • 符合逻辑的做法是,既然按下一个键会产生一个或者多个自己的扫描码,那就应该在一个处理过程中把它们都读出来。
    • 实现也不难,只要把从kb_in读取一个字符的代码单独用一个函数实现即可。get_byte_from_kbbuf()。
    • 另外,以0xE1开头的PAUSE键和以0xE0开头的PrintScreen键都和其他普通的0xE1或者0xE0开头的键不同,因为它们一个make code有6个字符,另一个有4个字符,而普通的只有2个字符。所以需要特殊处理。
  • 下面解决第二个问题,不可打印的控制字符怎么处理

    • 为了增加程序的通用性,keyboard_read()就只负责读取和匹配得到标准扫描码,至于如何处理,应该交给上层软件来处理。
    • 所以,我们将前面打印得到的扫描码的部分也去掉。然后,新建一个in_process函数来处理。
    • 在定义各个非可打印字符的宏的时候,就在第9位增加了一个FLAG_EXT标志位了,所以在in_process中,只要将key和FLAG_EXT&一下就能够区分是可打印字符还是普通的非可打印字符。
    • 对于这些非可打印字符,暂时还不处理,也就是没有反应,后面可以在in_process中轻松添加。

另外,很值得注意的一点就是,对于shift这些按键,它们的作用主要和和别的按键组合使用,所以很多时候只需要将它们是否按下的状态添加到正常的按键码上即可。而且这里无论单键还是组合键都是用的是32位的key表示,除去表示可打印字符和非可打印字符的第9位,还剩下32-9=23位来表示shift、ctrl、alt等键的状态,这足够链路。

接下来就是显示器(终端)

熟悉的终端终于要出场了。因为随着键盘模块的完善,需要考虑它和屏幕输出的关系了。IO是密不可分的。

先了解一下终端和显示器的驱动方式

TTY终端

tty也称为终端,直观的认识是,通过按alt+f1等f功能键,可以切换到不同的屏幕界面,不同屏幕中分别有各自的IO,相互不受影响。但这只是表象,实际并没有那么简单

TTY的结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VlhhpIVd-1575882874073)(http://yeholdon.top/wp-content/uploads/2019/12/IMG_20191207_145538-scaled.jpg)]
可以看到,三个tty共用一个keyboard,而且实际也公用了同一块显存,这就 需要在切换tty的时候让屏幕显示显存中某个位置的内容。这可以通过端口操作简单地做到。

显示器的基本概念

实际,对于显示器的操作,我们可能操作的是显卡,也可能只是显存。显示器实际有很多种模式,现在我们接触到的只是默认的80*25文本模式,另外还有很多更复杂和强大的模式,用来显示更强大的色彩等。

  • 80*25文本模式下,显存大小为32KB,地址范围为0xB8000~0xBFFFF.每两个字节代表一个字符,低字节是ASCII码,高字节是属性。
  • 一个屏幕映射所占空间就好计算了,80252=4000 Byte,而显存为32K,可见,显存最多可以存放8个屏幕映射的数据。如果这时只用3个,那每个就可以用10多KB空间,这时还可以实现简单的滚屏功能。
  • 那么,在切换tty的时候,如何让系统界面(显示器显示指定位置的内容呢?其实和简单,通过端口操作设置相应寄存器即可

VGA视频子系统寄存器操作

  • 对寄存器的访问通过读写端口实现
  • 但是如果多个寄存器只有一个端口,就要通过地址寄存器,先向地址寄存器写需要的寄存器的索引号,然后再通过那个唯一的端口号读写。
  • 让光标随着输入字符位置移动:通过设置Cursor Location High/Low Register两个寄存器来实现,将光标位置设为disp_pos/2即可。
  • 实现滚屏功能:设置Start Address High/Low Register,设置当前屏幕开始显示的位置,实现整体滚屏。

TTY 任务

TTY任务,也就是前面的task_tty(),在其中执行了一个循环,这个循环将轮询每一个TTY,然后处理轮询到的TTY的事件,包括从键盘缓冲区读取数据、显示字符等。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7ZJoV1Fn-1575882874074)(http://yeholdon.top/wp-content/uploads/2019/12/IMG_20191207_162330-scaled.jpg)]

  • 并非每次轮询到某个TTY时,箭头所对应的全部事件都会发生,只有当某个TTY对应的控制台是当前控制台时,它才能够读取键盘缓冲区(虚线表示)
    • 这里多出了一个控制台的概念。从前面可以看到,控制台是包含在TTY里的,或者说每个TTY对应有一个console控制台。
    • 简单来说,TTY里设置了一个缓冲区,用来存放当前TTY从键盘缓冲区读入并处理后待显示的字符。而这个字符的显示此时不是直接由in_process这个处理模块来直接显示了,而是建立一个新的模块console来处理TTY的待显示内容的显示。
    • 这样高度模块化是有好处的,可以将读取、处理和显示三个步骤分开来,一定程度上可以不用完全同步,这和缓冲区的设置是一致的,因为要实现这样的模块化就需要缓冲区来暂存。

TTY任务框架

新建两个结构体,分别为s_tty和s_console,定义在tty.h和console.h中。

typedef struct s_console
{
	unsigned int	current_start_addr; 	/* 当前显示到了什么位置	  */
	unsigned int	original_addr;		           /* 当前控制台对应显存位置 */
	unsigned int	v_mem_limit;		         /* 当前控制台占的显存大小 */
	unsigned int	cursor;			                       /* 当前光标位置 */  
} CONSOLE;

可以看到,其实控制台就是TTY中,负责操作显存显示的部分。规定了该TTY当前显示的情况,比如占据的显存的位置,大小,以及当前显示到的位置。控制台是直接与用户接触的,当我们通过alt+fn切换终端时,其实就是在切换控制台。而控制台对应的TTY则是由TTY任务按照一定的频率轮询的。可以看成是一个程序的界面和后台的关系,后台进程被不断地调度,而前端的用户接口则保持同一个,除非用户自己切换。

typedef struct s_tty
{
	u32	in_buf[TTY_IN_BYTES];	/* TTY 输入缓冲区 */
	u32*	p_inbuf_head;		        /* 指向缓冲区中下一个空闲位置 */
	u32*	p_inbuf_tail;		            /* 指向键盘任务应处理的键值 */
	int	inbuf_count;		                  /* 缓冲区中已经填充了多少 */

	struct s_console *	p_console;      // 指向对应console的指针
}TTY;

  • 只有当某个tty对应的console是当前console的时候,它才可以读取键盘缓冲区。 这个理解起来也简单,就像现在我们在OS中打开多个程序窗口,但是我们开始打字的话,字也只会被“当前”窗口获取,如果当前窗口不是我们想要输入的窗口,我们就需要先通过点击或者键盘快捷键切换到目标窗口再输入。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uMYQpTP2-1575882874075)(http://yeholdon.top/wp-content/uploads/2019/12/IMG_20191207_171439-scaled.jpg)]

几点开发和调试的总结教训:

  • 添加新的源文件就马上在makefile中加上
  • 写一个新的函数的时候,就先考虑它是否要在别的源文件里调用,以此来判断是否加static也就是PUBLIC和PRIVATE
  • 当写的函数里要调用别的函数而别的函数又还没实现的时候,就会先去写别的函数,但这时候最好标记一下前面的那个函数,不然很可能写完就忘了原来在哪里调用它了。
  • PRIVATE的函数就在本文件前面声明即可,PUBLIC的则要在proto.h这个专门放函数声明的头文件里声明。别的头文件就可以不放函数声明了,也尽量不包含别的头文件,除非里面的某些结构体定义中用到了对应的结构,这样才包含对应头文件。

总结:TTY任务开始运行时,所有TTY被初始化,全局变量nr_current_console被赋默认初值0.然后轮询开始并一直进行下去。对于每一个TTY,先执行tty_do_read(),它将调用keyboard_read()并将读入的字符交给函数in_process()处理,如果是需要输出的字符,会被in_process放入当前TTY的缓冲区。然后tty_do_write()接着执行,如果缓冲区有数据,就会被送到out_char显示出来。

目前因为没有切换过current console所以,其他tty轮询到时,都被is_current_console()忽略掉了。

现在框架已经搭好,下面进行多个console的切换。

多控制台

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a3L3Ufl4-1575882874076)(http://yeholdon.top/wp-content/uploads/2019/12/IMG_20191207_205530-scaled.jpg)]
可以看到,一个console的总大小是大于80* 25 的,所以可以做到滚屏,相当于将80 *25的窗口整体下移(或者上移)直到到达console的边界,就做到了滚屏。

图上标注了s_console结构里的成员,之前其实都还没有用到。

  • original_addr和v_mem_limit用作定义控制台所占显存总体情况,它们是静态的,一经初始化就不再改变
  • current_start_addr将随着屏幕卷动而变化
  • cursor每输出一次字符就更新一次
先初始化控制台

init_screen()函数主要是初始化CONSOLE结构的那4个成员,并显示标识console的序号和命令提示符$,并在init_tty()里调用

需要注意的是,结构CONSOLE的成员都是以Word计的,这符合VGA寄存器操作的使用习惯。

修改out_char以适应多控制台情况

主要是传入CONSOLE,,然后根据该CONSOLE来显示,而非disp_pos

切换控制台
  • 这一步是很关键的,其实说到底切换控制台就是通过设置VGA系统的Start Address High/Low Register来让当前这个只有80*25的显示窗口移到目的控制台所对应的那段显存空间。

  • 所以切换控制台的操作是通过前面用过的滚屏的方法操作对应的CRT寄存器来实现的。

这里要注意!
我的电脑当前是Ubuntu18.04系统,第一次测试的时候,发现只有F3能够正确切换到console2,F1和F2貌似都不行,而且alt+F2貌似被系统占用了,按下触发的是系统的功能。因此我怀疑这里的很多组合键是被系统一直占用的,并没有映射到bochs虚拟机里。

经过测试,F5也是可以用的,但是F6和F7又不行。为了能够三个连续,最后找到了F9~F11都是可以的,为此,这个问题算是解决了。

  • 然后是添加滚屏代码scroll_screen.
    简单起见,当屏幕滚到最下端或者最上端后,再按shift+down和shift+up就不再响应。

  • 但是这样还有一个问题,就是当写满当前屏幕后再往下写,按照习惯应该自动往下滚动一行,现在来自行添加这个功能。主要是在out_char里添加判断和滚屏。

  • 再进一步,如果写满屏幕后手动向上滚动了几行,这时候又继续写,按照常规用户习惯,应该自动滚动到当前光标所在行继续写,这个其实也简单,就是在前面自动向下滚动一行的基础上先计算出当前屏幕显示的结尾和当前光标所在行差的行数,然后循环向下滚动这个差的行数即可。

完善键盘处理

到目前为止,只剩下键盘上的某些键的处理程序没有写了,现在补上。

  • 回车和退格:通过向tty缓冲区添加\n和\b实现,同时记得修改out_char
  • Caps Lock\Num Lock\Scroll Lock:键盘上这三个键都有相应的状态指示灯,可以通过写入8042的输入缓冲区来控制它们。输入缓冲区和控制寄存器都是可写的,只不过写入缓冲区是用来给8048(在键盘上编码器)发送命令。而写入控制寄存器是往8042本身发送命令。
    • 我们要操作的显然是键盘本身,所以应该往8048发命令,使用0x60端口。设置LED的命令为0xED,键盘收到后回一个ACK(0xFA),然后等待从0x60写入的LED参数字节,表示LED状态,收到后再回一个ACK。
    • 另外往8042缓冲区写数据前要通过读0x64端口判断是否缓冲区为空,为空才能写数据。

TTY任务总结

运行在ring0的keyboard_handler主要是负责获得读入扫描码,存入kb_in缓冲区。而终端任务task_tty()则是运行在ring1下的一个任务(进程,至少目前这俩是等价的),它负责读取kb_in里的扫描码,处理得到具体的key后写入当前tty的缓冲区,再由console输出到屏幕显示。所以,比较特殊的是,这里的kb_in是在ring0下写,ring1下读的。这也是Minix的做法。

区分任务和用户进程

现在,我们有了四个进程(任务)分别是TTY、A、B、C。ABC对于我们的OS来说是可有可无的,不运行也不会有什么影响,而TTY则是必须的,没有它,我们无法使用键盘进行用户交互。因此,TTY应该是操作系统的一部分,我们有必要区分这两种进程。TTY称为任务,A、B、C则称为用户进程。实现上也做一些改变,让用户进程运行在ring3上,任务继续是ring1,总结来说,就是将系统任务用户进程区分开来。这样将特权级进一步层次化,有利于管理和保护操作系统。

具体来讲就是将原来的NR_TASKS分成NR_TASKS和NR_PRCS用到的地方和描述符特权级也做相应修改,另外还有进程调度应该还是一起调,没有区别。

自己的printf

现在我们的TTY已经有了雏形,可以写一个用来在控制台输出的C库函数printf()了。这里要注意printf()这种函数并不是系统调用,而是C语言提供的库函数,但是printf也是通过调用系统提供的系统调用实现的,比如write()这个系统调用,因此,我们在实现自己的printf的时候还要先实现write系统调用。

为进程指定TTY

调用printf的是进程,而输出的对象的TTY的控制台,printf调用系统调用的时候,从ring3转到了ring0,系统只能知道当前系统调用是由哪个进程触发的,因此为了让printf知道要输出到哪个控制台,就要让用户进程有从属于的TTY.具体实现可以在进程表中增加一个nr_tty成员。

printf()的实现

printf的实现其实并不简单,因为它的参数个数和类型都是可变的,而且其中表示格式的参数形式很多样(%d,%x等等)。所以,由浅入深,先实现一个简单的——只支持"%x"一种格式的printf()

我们知道,printf的结构是这样的

printf("以%开头的格式串",和格式串里的格式对应的数量不定的输出对象);

所以除了第一个格式字符串,后面的参数个数是不定的,为此,C语言给出的是用…表示的可变参数。可变参数实现的原理也值得一说:

  • 调用函数的过程是调用者将参数传入堆栈,被调用的函数获取堆栈里的参数,最后完成调用过程后需要有人清理堆栈,恢复调用前的状态。

  • 这里就涉及两个问题:

    • 一是参数压栈的顺序如何
    • 二是由谁来清理堆栈,是调用者还是被调用者
  • 规定这两个问题的叫做调用约定calling conventions

  • 对于C语言来说,使用的是C调用约定:后面的参数先入栈,由调用者清理堆栈。

  • 这种方式的好处:在支持可变参数时得到了充分体现,因为只有调用者才知道这次调用包含了几个参数,清理堆栈的时候就很方便。而被调用者虽然通过一定的方式(比如用一定有的第一个参数来间接地告知被调用者后面参数的情况)得知参数的情况,但是这显然有一定限制,也不够方便。

  • 但是可变参数的情况下,被调用者也需要知道参数的具体情况才能区分和使用这些参数。所以,printf()的实现还是用到了第一个一定有的参数作为参考,传达给被调用者传入参数的个数和类型格式等。

  • 具体实现:

    • 定义一个buf缓冲区,用来暂存按照第一个参数指向的格式符来转换输出格式后的待输出内容,由于第一个参数是char*也就是一个指针变量,占4个字节,至于如何获取第一个参数这个指针fmt指向的格式字符串的边界,可以通过循环的终止条件来解决,不断地让指针自增,直到它指向的内容为空。
    • 后面的参数就可以根据缓冲区里分离 出来的格式符的个数以及类型来获取后面的可变参数
    • 至于后面可变参数列表的首地址,则可以通过第一个参数的地址加上4的偏移得到。
    • 得到了待输出的格式化字符流buf后,就要调用系统调用来输出到显示屏了。
    • 这个系统调用,就是write(),用在很多地方,比如网络编程里也有使用。前面已经实现过一个系统调用,所以这里也比较容易实现了。
    • 系统调用名称为write(),对应的内核部分为sys_write()
  • 增加一个系统调用的过程
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uVx6IIK1-1575882874077)(http://yeholdon.top/wp-content/uploads/2019/12/IMG_20191208_211643-scaled.jpg)]

  • 一个使用了系统调用的C库函数的调用过程(printf):
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aiU9aMv7-1575882874078)(http://yeholdon.top/wp-content/uploads/2019/12/IMG_20191209_103726-scaled-e1575859118288.jpg )]

最后再记录一下这次开发调试时遇到的问题和教训
这一节在完成开发后运行,发现虽然各个console里能够正确输出绑定在其上的用户进程的printf()输出。但是控制台却也随着输出在两个输出的console之间快速跳动。其实通过上面的分析很容易知道问题出在调用过程的最后一环也就是tty_write()里的out_char()函数,因为只有这个函数才会直接操作显存切换。

但是让我恼火的是,我第一次逐字核对了out_char函数,却没有问题,然后我就懵逼了。开始回溯整个过程,虽然发现了几个遗漏的小问题,但是都不是造成这个console快速切换跳动的原,有些强迫症的我还为此纠结了一晚上。结果第二天早上,我又从理性出发,觉得问题一定出在out_char,就又再核对了一遍,原来并不是out_char本身有问题,而是里面调用的flush()这个用来更新重新设置的当前console到显示器的函数的问题,因为flush应该是要判断当前TTY是对应当前console后才能刷新的,而我遗漏掉了这个,直接导致只要输出字符调用out_char就切换到了调用out_char的进程对应的console。

  • 最后总结一下经验教训:
    对于问题要理性分析,缩小排查范围,然后再仔仔细细地在出问题的范围内排查,就像这次,排查到out_char函数,不应该只看它的函数体有没有错误,还要查看衍生的函数调用链上的每一个函数直到调用链的最后一个函数为止。也就是缩小范围,从“深度”着手 ,否则盲目地乱查效率很低…真的…很低…

结语

到这里,一个OS的雏形已经基本实现,我们可以在多个控制台之间切换,各个控制台下运行着各自的用户进程,彼此互不干扰,呈现出了一个多任务多控制台操作系统雏形。接下来将进入一个操作系统的其他一些基本组成模块的编写,包括进程间通信、文件系统、内存管理。

你可能感兴趣的:(一个操作系统的实现)