操作系统实验lab3

操作系统实验lab3

本实验为南京大学软件学院操作系统课程第三次实验,本文是实验完成后的回顾总结,也可作为本实验的指南。

实验要求

nasm + bochs平台上完成一个接受键盘输入,回显到屏幕上的程序

功能要求

基本功能

  1. 从屏幕左上角开始,以白色显示键盘输入的字符。可以输入并显示 a-z,A-Z

0-9 字符。

  1. 大小写切换包括 Shift 组合键以及大写锁定两种方式。大写锁定后再用 Shift组合键将会输入小写字母

  2. 支持回车键换行

  3. 支持用退格键删除输入内容

  4. 支持空格键Tab 键(4 个空格,可以被统一的删除)

  5. 每隔 20 秒左右, 清空屏幕。输入的字符重新从屏幕左上角开始显示。

  6. 要求有光标显示, 闪烁与否均可, 但⼀定要跟随输入字符的位置变化。

  7. 不要求支持屏幕滚动翻页,但输入字符数不应有上限。

  8. 不要求支持方向键移动光标。

查找功能

  1. 按 Esc 键进入查找模式,在查找模式中不会清空屏幕。

  2. 查找模式输入关键字,被输入的关键字以红色显示

  3. 按回车后,所有匹配的文本 (区分大小写) 以红色显示,并屏蔽除 Esc 之外

任何输入。4. 再按Esc键,之前输入的关键字被自动删除,所有文本恢复白颜色, 光标回到正确位置。参见示例。

附加题

按下 Ctrl + z 组合键可以撤回操作(包含回车和 Tab 和删除),直到初始状态。

要求

  • 使用 make 构建整个项目,程序必须进入到保护模式下完成。

  • 提交代码(包含 makefile)和运行截图,其中 makefile 必须支持make run 命令,即在 shell 中进入代码文件所在目录,输入make run并回车可直接启动程序,不需要其他命令。

具体实验

运行书上框架代码

本次实验可以使用《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
// tty.c
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和回车的输入,但是它们不能被整体地删除。

一个较为简单解决的方法是,用一个记录每次输入的位置,删除时取出上一次光标的位置。

具体步骤:

  1. 在console.h的struct s_console的定义中加入CursorStack cursorStack;,需要自己设计栈需要保存的内容,并添加入栈、出栈等基本方法。注意要在init_screen中初始化栈。

  2. 修改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:

1
#define NR_TASKS   2

接下来在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
// global.h
#define NORMAL_MODE 0
#define FIND_MODE 1
extern int mode;
1
2
// global.c
int mode;

我们需要在按下ESC键的时候切换模式,在tty.c的in_process函数中处理

1
2
3
4
5
6
7
8
9
10
11
12
13
// global.h
#define change_mode(x) ((x) ^ 1)

// tty.c
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;

// Set all characters to default color
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;
// 撤销的具体代码
// ...
}
// No break

怎么实现撤销功能呢,我们需要一个栈(叫线性表会更为准确,因为不止在栈顶操作),记录每一次的操作,包括操作类型(插入/删除)和对应的字符。

  • 对插入的撤销:执行一次删除

  • 对删除的撤销:执行对应的插入

这里同样要注意tab、回车等特殊字符

注意连续的撤销删除,需要在栈中找到对应的被删的字符

具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// console.h
typedef struct record_stack {
int top;
int types[CURSOR_STACK_SIZE];
u8 chars[CURSOR_STACK_SIZE];
} RecordStack;

/* CONSOLE */
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
// console.c 
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 { // DELETE_TYPE
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));
}
}
// No break

完成lab3,撒花