操作系统实验lab3
本实验为南京大学软件学院操作系统课程第三次实验,本文是实验完成后的回顾总结,也可作为本实验的指南。
实验要求 在nasm + bochs 平台上完成一个接受键盘输入,回显到屏幕上 的程序
功能要求 基本功能
从屏幕左上角开始,以白色显示键盘输入的字符。可以输入并显示 a-z,A-Z 和
0-9 字符。
大小写切换 包括 Shift 组合键以及大写锁定两种方式。大写锁定后再用 Shift组合键将会输入小写字母
支持回车键换行 。
支持用退格键删除输入内容 。
支持空格键 和 Tab 键 (4 个空格,可以被统一的删除)
每隔 20 秒左右, 清空屏幕 。输入的字符重新从屏幕左上角开始显示。
要求有光标显示 , 闪烁与否均可, 但⼀定要跟随输入字符的位置变化。
不要求支持屏幕滚动翻页,但输入字符数不应有上限。
不要求支持方向键移动光标。
查找功能
按 Esc 键进入查找模式 ,在查找模式中不会清空屏幕。
查找模式输入关键字 ,被输入的关键字以红色显示
按回车后,所有匹配的文本 (区分大小写) 以红色显示,并屏蔽除 Esc 之外
任何输入。4. 再按Esc
键,之前输入的关键字被自动删除 ,所有文本恢复白颜色, 光标回到正确位置。参见示例。
附加题 按下 Ctrl + z 组合键可以撤回操作 (包含回车和 Tab 和删除),直到初始状态。
要求
具体实验 运行书上框架代码 本次实验可以使用《orange’s 一个操作系统的实现》书附录光盘代码,在其基础上修改实现,所以第一步是将光盘中的代码跑起来,光盘中的代码可以从Github中下载,运行chapter7/n
中的代码:
1 2 make image bochs -f bochsrc
运行程序时可能会出现一些错误,需要根据bochs版本进行调整 ,或是要安装图形库 。
为了实现支持make run
命令,我们需要修改Makefile文件,添加以下代码:
1 2 run : image bochs -f bochsrc
现在,完成了实验的第一步,我们发现框架代码中已经实现了输入字符、大小写切换、换行、退格键删除等功能,下面需要我们继续完善。
实现初始清屏 为了实现从屏幕左上角开始,以白色显示键盘输入的字符,需要在初始化之前将整个屏幕打印满空格,然后把显存指针重置为0
在main.c
中加入以下方法,并在进入kernel_main
函数时调用该方法
1 2 3 4 5 6 7 void CleanScreen () { disp_pos = 0 ; for (int i = 0 ; i < SCREEN_SIZE; i++){ disp_str(" " ); } disp_pos = 0 ; }
实现tab和回车键 首先,我们需要阅读框架代码 中已有的回车实现和空格实现。
注意 :虽然框架代码中已经提供了回车换行的实现,但在该实现中,无法做到删除换行时回到上一行(它会一个一个空格删除)。
在tty.c
中的in_process
中识别当前按下的键,并做出相应的处理;在console.c
中的out_char
函数中实现对具体字符的输出。要实现tab,实际上就是输出4个空格。
1 2 3 4 case TAB: put_key(p_tty, '\t' ); break ;
console.c
:(根据实验要求,TAB_WIDTH
是为4的常量)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 if (p_con->cursor < p_con->original_addr + p_con->v_mem_limit - TAB_WIDTH) { PushIntoCursorStack(&(p_con->cursorStack), p_con->cursor); for (int i = 0 ; i < TAB_WIDTH; i++) { *p_vmem++ = ' ' ; *p_vmem++ = DEFAULT_CHAR_COLOR; } p_con->cursor += TAB_WIDTH; PushIntoRecordStack(&(p_con->recordStack), INSERT_TY, ch); } break ;
现在,我们实现了tab和回车的输入,但是它们不能被整体地删除。
一个较为简单解决的方法是,用一个栈 记录每次输入的位置,删除时取出上一次光标的位置。
具体步骤:
在console.h的struct s_console
的定义中加入CursorStack cursorStack;
,需要自己设计栈需要保存的内容,并添加入栈、出栈等基本方法。注意要在init_screen中初始化栈。
修改out_char
方法,在改变cursor之前将当前的cursor压入栈内,退格时使用pop获取之前的cursor值。
实现定时清屏 这个功能是独立于其他的功能的,我们需要一个新的进程,在global.c中添加一个TASK :
1 2 3 PUBLIC TASK task_table[NR_TASKS] = { {task_tty, STACK_SIZE_TTY, "tty" }, {TimeClear, STACK_SIZE_TIMECLEAR, "TimeClear" }};
注意,这里要修改宏定义proc.h
中的NR_TASKS
:
接下来在main.c中添加一个函数(mode
是全局变量,在这里用于控制查找模式不用清屏,可暂时不管它):
1 2 3 4 5 6 7 8 9 10 11 void TimeClear () { while (1 ) { if (mode == NORMAL_MODE) { ClearScreen(); init_all_tty(); milli_delay(250000 ); } else { milli_delay(10 ); } } }
init_all_tty
是在tty.c中添加的函数,用于初始化screen,实现光标复位
1 2 3 4 5 6 void init_all_tty () { for (TTY * p_tty = TTY_FIRST; p_tty < TTY_END; p_tty++) { init_tty(p_tty); } select_console(0 ); }
到这里,清屏的任务就完成了
实现查找功能 模式切换 为了区分两个模式(普通模式和查找模式),我们在global.c中定义了一个全局变量mode
1 2 3 4 #define NORMAL_MODE 0 #define FIND_MODE 1 extern int mode;
我们需要在按下ESC键的时候切换模式,在tty.c的in_process
函数中处理
1 2 3 4 5 6 7 8 9 10 11 12 13 #define change_mode(x) ((x) ^ 1) case ESC: mode = change_mode(mode); if (mode == NORMAL_MODE) { exit_search_mode(p_tty->p_console); g_shield = 0 ; } else { EnterFindMode(p_tty->p_console); } break ;
下面修改console.c中输出字符的函数(out_char
函数default分支),将字符颜色从默认颜色改为分情况讨论:
1 2 3 4 5 if (mode==0 ){ *p_vmem++ = DEFAULT_CHAR_COLOR; }else { *p_vmem++ = RED; }
你需要在struct s_console
中保存进入查找模式时的光标位置,以实现光标复位。具体方法是把当前的cursor到保存的cursor之间的显存全部置为空格和DEFAULT_CHAR_COLOR
,然后复位指针,将保存的cursor值赋值给cursor。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 PUBLIC void exit_search_mode (CONSOLE* p_con) { u8* p_vmem = (u8*)(V_MEM_BASE + p_con->cursor * 2 ); int sm_input_length = p_con->cursor - p_con->start_pos; for (int i = 1 ; i <= sm_input_length; i++) { *(p_vmem - 2 * i) = ' ' ; *(p_vmem - 2 * i + 1 ) = DEFAULT_CHAR_COLOR; } p_con->cursor = p_con->start_pos; p_con->cursorStack.top = p_con->cursorStack.search_mode_top; for (int i = 0 ; i < p_con->start_pos; i++) { *(u8*)(V_MEM_BASE + 2 * i + 1 ) = DEFAULT_CHAR_COLOR; } flush(p_con); }
查找功能 字符串匹配可以用KMP算法,但这里选择更为简单的直接匹配,将匹配到的字符改变颜色:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 PRIVATE u8 getFindChar (CONSOLE* p_con, int i) { return *((u8*)(V_MEM_BASE + p_con->start_pos * 2 + i * 2 )); } PRIVATE u8 getCurChar (CONSOLE* p_con, int i) { return *((u8*)(V_MEM_BASE + i * 2 )); } PUBLIC void find (CONSOLE* p_con) { int len = p_con->cursor - p_con->start_pos; for (int i = 0 ; i < p_con->start_pos ; i++) { int left = i; int isMapped = 1 ; int j = 0 ; for (; j < len && i + j < p_con->start_pos; j++) { if (getCurChar(p_con, i + j) != getFindChar(p_con, j)) { isMapped = 0 ; } } if (isMapped && j == len) { int right = left + len - 1 ; for (int k = left; k <= right; k++) { *((u8*)(V_MEM_BASE + 2 * k + 1 )) = RED; } } } }
下面修改in_process()
的ENTER
分支,代码略。
为了实现在查找模式中屏蔽除ESC外的输入,我们新建了一个输入屏蔽全局变量g_shield
,这里不多介绍。至此我们已经完成了基本功能。
实现撤销 首先需要识别Ctrl+z/Z的组合键,在keyboard.c中添加以下方法:
1 2 3 PUBLIC int getCtrlStat () { return ctrl_l || ctrl_r; }
在out_char函数中添加对撤销的识别:
1 2 3 4 5 6 7 8 case 'z' :case 'Z' : if (getCtrlStat()) { isRevoke = 1 ; }
怎么实现撤销功能呢,我们需要一个栈(叫线性表会更为准确,因为不止在栈顶操作),记录每一次的操作,包括操作类型(插入/删除)和对应的字符。
对插入的撤销:执行一次删除
对删除的撤销:执行对应的插入
这里同样要注意tab、回车等特殊字符
注意连续的撤销删除,需要在栈中找到对应的被删的字符
具体代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 typedef struct record_stack { int top; int types[CURSOR_STACK_SIZE]; u8 chars[CURSOR_STACK_SIZE]; } RecordStack; typedef struct s_console { unsigned int current_start_addr; unsigned int original_addr; unsigned int v_mem_limit; unsigned int cursor; CursorStack cursorStack; unsigned int start_pos; RecordStack recordStack; }CONSOLE;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 case 'z' : case 'Z' : if (getCtrlStat()) { isRevoke = 1 ; int lastType = GetTopType(&(p_con->recordStack)); if (lastType == INSERT_TY) { if (p_con->cursor > p_con->original_addr) { int lastPos = PopCursorStack(&(p_con->cursorStack)); int deleteNum = p_con->cursor - lastPos; p_con->cursor = lastPos; for (int i = 1 ; i <= deleteNum; i++) { *(p_vmem-2 ) = ' ' ; *(p_vmem-1 ) = DEFAULT_CHAR_COLOR; p_vmem -= 2 ; } } PopRecordStack(&(p_con->recordStack)); } else { char cha = GetTopChar(&(p_con->recordStack)); switch (cha) { case '\n' : if (p_con->cursor < p_con->original_addr + p_con->v_mem_limit - SCREEN_WIDTH) { PushIntoCursorStack(&(p_con->cursorStack), p_con->cursor); p_con->cursor = p_con->original_addr + SCREEN_WIDTH * ((p_con->cursor - p_con->original_addr) / SCREEN_WIDTH + 1 ); } break ; case '\t' : if (p_con->cursor < p_con->original_addr + p_con->v_mem_limit - TAB_WIDTH) { PushIntoCursorStack(&(p_con->cursorStack), p_con->cursor); for (int i = 0 ; i < TAB_WIDTH; i++) { *p_vmem++ = ' ' ; *p_vmem++ = DEFAULT_CHAR_COLOR; } p_con->cursor += TAB_WIDTH; } break ; default : if (p_con->cursor < p_con->original_addr + p_con->v_mem_limit - 1 ) { PushIntoCursorStack(&(p_con->cursorStack), p_con->cursor); *p_vmem++ = cha; if (mode == NORMAL_MODE) { *p_vmem++ = DEFAULT_CHAR_COLOR; } else { *p_vmem++ = RED; } p_con->cursor++; } break ; } PopRecordStack(&(p_con->recordStack)); } }
完成lab3,撒花