哈工大操作系统实验8 终端设备的控制
该篇文章是哈工大操作系统实验8——终端设备的控制完成笔记,其中包含了详细的步骤和相关代码,并有截图说明。实验内容都成功通过了,但是因为内容较多,记录中难免会有疏忽,如有发现错误,欢迎大家留言和我联系。
本次的实验比较简单,但了解到Linux系统的IO核心是文件和终端设备驱动编写的方法,收获也是很大。欢迎大家一键三连:点赞、关注加收藏,感谢大家的支持。
理论知识
实验内容推荐大家学习对应的视频课程:
- L26 I/O 与显示器
- L27 键盘
另外推荐阅读《注释》书籍:
- 第10章 字符设备驱动程序(char driver):介绍了字符设备的知识,终端设备就是字符设备,字符设备的实际读和写就在这里处理的;
- 第12.13节 char_dev.c程序:如果read和write函数操作的是一个字符设备,则调用该程序进行处理;
- 和12.14节 read_write.c程序:Linux0.11中终端设备的读取和写入统一封装到文件操作里了,read和write函数是最顶层的调用。
实验内容:
按下F12显示星号功能
思路分析
从实验内容来看,不难拆解出两个思路:按下F12系统是怎么处理的?显示器是怎么显示的? 解决这两个问题,实验就可以做了。
《注释》书籍中的这张图在这次实验中很有帮助,完整的说明了从按下键盘到显示器显示的流程。
根据这个流程图,我跟了一下代码的处理流程,思路上更加清晰了。代码流程如下:
// 键盘按下的代码处理流程是怎么样的?
./kernel/chr_drv/keyboard.S 中的 _keyboard_interrupt
-> call key_table
-> a-z:call do_self -> call put_queue 写入到read_q队列
-> F1~F12:call func -> call put_queue 写入到read_q队列
-> call ./kernel/chr_drv/tty_io.c 中的 do_tty_interrupt
-> call copy_to_cooked 从read_q队列中读取数据
-> 写入到 secondary 队列,
-> 如果有回显,则写入 write_q 队列,并调用 call tty->write() 在控制台上进行显示。
// 这里的tty实际是console,所以write函数对应con_write。相关文件:./kernel/chr_drv/console.c
// 线索可以查看 keyboard.S 68~69行、tty_io.c 51~92行。
-> 唤醒 secondary 队列中等待的进程
// secondary 队列中等待的进程是怎么来的?
tty_io.c文件tty_read函数中当secondary队列为空时会进入睡眠。256~260行。
// tty_read 函数是谁在调用?
./fs/read_write.c 中的sys_write和sys_read函数
-> ./fs/char_dev.c中 rw_char 函数 // 线索:./fs/char_dev.c 85~104行
-> ./fs/char_dev.c中 rw_tty 函数
-> call ./fs/char_dev.c中 rw_ttyx 函数
-> call tty_read
// 结论就很清晰了,最顶层是系统调用write和read,如果是字符设备则最终会调用到 tty_read和tty_write
// 如果有兴趣继续跟进代码,就会发现Linux系统的IO核心就是文件。
// 最顶层都是对文件的读和写,根据不同的文件类型调用不同设备的方法进行处理。
// Linux0.11实现了块设备、字符设备和常规文件。本次实验内容主要涉及字符设备。
按下F12处理
按下键盘的中断处理程序是 ./kernel/chr_drv/keyboard.S 中的 keyboard_interrupt 函数。其中断设置路径如下:
main.c中main()
-> tty_io.c中 tty_init()
-> console.c中 con_init()
-> set_trap_gate(0x21,&keyboard_interrupt);
通过查看 keyboard_interrupt 函数处理,可以知道按下F12的处理函数是func。
keyboard_interrupt: # 键盘中断处理函数
...
call key_table(,%eax,4) # 调用key_table定义好的函数
...
...
key_table:
...
.long func,none,none,none /* 58-5B f12 ? ? ? */ # f12定义的处理函数就是func
...
本来想着要兼容现有的F12按键处理功能,要在func上修改,后来查看func实现,发现里面啥事没干,就是打印出系统当前所有进程的信息(214行),然后打印一个自己的字符码L(230行)。F1~F12都是实现一样的功能。参考下图:
所以就考虑编写一个新的函数,替换掉F12的按键处理函数。有了思路就可以开始编写代码了。
1) 在 ./kernel/chr_drv/tty_io.c 中增加如下代码:
// ./kernel/chr_drv/tty_io.c 文件
unsigned short f12_flag=0; // 记录F12的标志
// 按下F12的处理函数
void f12_handler(void)
{
f12_flag = f12_flag ? 0 : 1;
}
2) 在 ./include/linux/tty.h 中声明 f12_flag 和 f12_handler,在有引入 tty.h 这个头文件中的地方都可用:
extern unsigned short f12_flag;
void f12_handler(void);
3) 修改 ./kernel/chr_drv/keyboard.S 文件中按下F12的处理函数为 f12_handler :
/* 525行 */
.long f12_handler,none,none,none /* 58-5B f12 ? ? ? */
至此,按下F12的功能就处理好了。
终端显示星号
经过上面的处理,按下F12按键后 f12_flag 会在0和1之间进行切换,如果为1,那么终端显示的时候要是星号。
从上面贴的《注释》书籍10-5的图,不难知道终端显示的处理在 ./kernel/chr_drv/console.c 中的 con_write 函数,在该函数中增加判断即可。
if (c>31 && c<127) { // 455行
if (x>=video_num_columns) {
x -= video_num_columns;
pos -= video_size_row;
lf();
}
// 新增代码,若字符为大小写字母或者数字,则改为*
if (f12_flag)
if((c>='A'&&c<='Z') || (c>='a'&&c<='z') || (c>='0'&&c<='9'))
c = '*';
编译运行
# 在 oslab 目录下
$ cd ./linux-0.11
$ make all
$ ../run
在Linux0.11中测试改动是否生效,结果如下图:
实验报告
完成实验后,在实验报告中回答如下问题:
1) 在原始代码中,按下 F12,中断响应后,中断服务程序会调用 func?它实现的是什么功能?
上面有分析过了:
- 就是打印出系统当前所有进程的信息(214行);
- 然后打印一个自己的字符码L(230行)。
这里贴出代码再看看就更容易理解了。
/*
* this routine handles function keys
*/
func:
pushl %eax
pushl %ecx
pushl %edx
call show_stat # 这行就是显示进程列表
popl %edx
popl %ecx
popl %eax
subb $0x3B,%al
jb end_func
cmpb $9,%al
jbe ok_func
subb $18,%al
cmpb $10,%al
jb end_func
cmpb $11,%al
ja end_func
ok_func:
cmpl $4,%ecx /* check that there is enough room */
jl end_func
movl func_table(,%eax,4),%eax # 168~170,将F12的字符码加入到read_q队列中。
xorl %ebx,%ebx
jmp put_queue
end_func:
ret
/* 这里就是F1~F12字符码的定义,最后1个就是F12的字符码 */
/*
* function keys send F1:'esc [ [ A' F2:'esc [ [ B' etc.
*/
func_table:
.long 0x415b5b1b,0x425b5b1b,0x435b5b1b,0x445b5b1b
.long 0x455b5b1b,0x465b5b1b,0x475b5b1b,0x485b5b1b
.long 0x495b5b1b,0x4a5b5b1b,0x4b5b5b1b,0x4c5b5b1b # F12的字符码:ESC [ [ L
2) 在你的实现中,是否把向文件输出的字符也过滤了?如果是,那么怎么能只过滤向终端输出的字符?如果不是,那么怎么能把向文件输出的字符也一并进行过滤?
2.1) 上面的实现并没有过滤向文件输出的字符。
2.2) 上面的实现是针对字符设备的处理,如果需要实现往文件输出的字符也过滤,实际就是在常规文件的写入处理,找到哪里是写入常规文件的代码即可。
《注释》书籍的12-12图可以给到很大的帮助。
Linux系统中,IO的设计思路核心就是文件,通过对文件的读写实现IO功能。底层的系统调用就是 read 和 write,然后针对不同的文件类型,调用不同的具体实现。例如:
- 本实验前面实现的终端变成星号,就是字符设备 rw_char() 实现的;
- 常规文件就是 file_read() 和 file_write() 实现的。
明白思路后,查看代码可以发现 file_write() 的实现在 ./fs/file_dev.c 文件,根据要求修改代码:
i += c;
while (c-->0) { // 从用户缓冲区 buf 中复制 c 个字节到高速缓冲区中 p 指向开始的位置处。
// 下面三行就是改动后的代码。
char temp_c = get_fs_byte(buf++);
if (f12_flag) temp_c = '*';
*(p++) = temp_c;
}
brelse(bh);
编译运行后,测试一下,修改成功。
参考资料
从以下资料得到了不少帮助,特此表示感谢。
完。