☰
Linux 0.11终端控制实验:F12键实现字符过滤的完整解析
2026/9/30 10:16:53 网站建设 项目流程

简介:操作系统实验 7 终端设备的控制是一份面向计算机专业学生的课程实验报告,围绕 Linux 0.11 终端设备处理代码的修改展开。文档从实验目的、实验内容、详细步骤到测试结果与结论,完整呈现如何通过 F12 开关变量控制键盘输入和字符显示,实现按下字母时输出为“*”、再次按下恢复正常的交互效果。报告还涉及键盘中断、扫描码、con_write 输出函数、相关头文件配置等关键知识点,并附有思考题,便于学习者对照检查。资源包内共 1 个 docx 文件,大小 876KB,内容结构清晰,可直接作为操作系统实验报告的参考模板或复习资料。已有 151 人学习浏览,适合正在完成操作系统设备管理实验或需要撰写 Linux 0.11 实验报告的高校学生使用。

1. 终端设备的控制:这份实验报告到底要你做什么

如果你正在做操作系统课程里的「终端设备控制」实验,多半会被 Linux 0.11 这套老内核的源码绕晕——键盘中断、扫描码、tty_write、con_write,概念一个接一个,代码却散在 kernel/chr_drv/ 和 fs/ 下面好几个文件里。这份实验报告的核心目标其实很朴素:按下 F12,屏幕上输出的字符全部变成“*”,再按一下 F12,恢复正常。听起来像个小把戏,但做完它,你就把「键盘输入如何进内核」「字符如何从 tty 写到显示器」「文件输出和终端输出走的是不是同一条路」这几个问题全部串起来了。实验报告里已经给了八个步骤和关键代码位置,适合正在做 HNU 或类似课程操作系统实验、需要对照思路写实验报告的同学,也适合想用最小改动验证 Linux 0.11 设备管理机制的读者。下面我按自己拆过的思路,把原理、改法、坑位一次讲透。

2. 键盘中断与 F12 的调用路径:先搞清楚你要改的是哪一段

2.1 Linux 0.11 的键盘输入链路:从扫描码到 show_stat

Linux 0.11 里键盘属于字符设备,挂在 8259A 中断控制器的 IRQ1 上。你按下一个键,键盘控制器 8042 把扫描码发到端口 0x60,中断响应后 CPU 跳转到键盘中断服务程序。0.11 的键盘处理入口在 kernel/chr_drv/keyboard.S,中断服务程序先把端口数据读出来,判断是按下(make code)还是松开(break code),然后查表转换成 ASCII 或功能键标志,放入 tty 的读缓冲队列。最终用户进程通过 read() 系统调用从 tty 队列里拿字符。

这里有一个关键点:F12 这类功能键并没有 ASCII 码,它在扫描码表里对应一个特殊条目。Linux 0.11 的 keyboard.S 里维护了一张 func_table,每个功能键(F1~F12)有一个处理函数指针。原始代码里 F12 对应的是调用 do_self_test 或者直接调用了 _show_stat——从实验报告的截图描述看,按下 F12 后中断服务程序调用的是 show_stat(),这个函数是 fork.c 里用来打印内核栈和进程信息的调试函数。你按 F12 能看到一坨进程表信息,就是因为这里挂了这个钩子。

所以你要改的核心入口就很明确了:不是去改键盘中断的底层读端口逻辑,而是去改 F12 在功能键映射表里对应的那个动作。实验步骤里「修改 F12 对应的入口函数」「反转一下,转到处理函数」说的就是这一层。

2.2 显示输出路径:con_write 才是“把字符画到屏幕上”的地方

键盘输入搞定后,另一条链路是字符显示。你在终端里敲一个字符,它不会直接飞到显存里,而是先经过 tty 层的处理。Linux 0.11 的 tty 分三层:tty_io.c 负责通用读写队列,console.c 里的 con_write() 负责把字符真正输出到显示器(也就是往显存映射区写),而 write() 系统调用会根据文件描述符的类型走不同的底层函数。如果你往 /dev/tty0 写字符,最终会调到 con_write;如果你往一个普通文件写,走的是 fs/file_dev.c 里的 file_write()。

这里就是实验报告里那个灵魂问题的来源:con_write 只过滤终端输出,而 file_dev.c 管的是文件输出。你在 con_write 里把字符改成“”,只影响屏幕显示,不影响你 cat 重定向到文件的内容。反过来,如果你想让写文件也被过滤,就得动 file_dev.c 的 file_write(),也就是实验报告里提到的第 81 行附近那句*(p++)='*'。我自己做的时候是把两处都改了,然后对比验证:只改 con_write,文件里是正常的;再改 file_dev.c,文件里的字符也变成“”了。这个对照实验能把“终端输出 ≠ 文件输出”这个抽象概念变成看得见的现象。

提示:Linux 0.11 的源码规模不大,console.c 总共不到一千行。建议先把 con_write 函数从头到尾读一遍,找出它处理普通可见字符的那段 switch-case,你在那里插入过滤逻辑最省事。

3. 动手改码:开关变量、F12 入口和两级字符过滤的实现

3.1 添加 F12 状态开关变量:全局变量比位标志更直观

实验第一步是「添加开关变量以记录 F12 的状态」。常见做法是在 kernel/chr_drv/console.c 文件顶部加一个全局变量,比如:

// kernel/chr_drv/console.c int f12_pressed = 0; // 0 = 正常模式,1 = 字符被过滤为 '*' 的模式

为什么不用一个位标志塞进现有状态寄存器?因为 0.11 的 tty 结构里没有现成的“用户自定义开关”位,你硬塞进去还要改 tty_io.h 的结构体定义,牵扯面大。单独开一个全局变量,编译链接时其它文件通过 extern 声明就能访问,改动的文件最少。这个变量的语义要明确:按下 F12 切换一次,不是按下时置 1、松开时置 0,否则你按住 F12 不放会反复切换。我们用的是「按一次翻转一次」的 toggle 语义。

3.2 修改 F12 入口函数并反转控制逻辑

接下来要找到键盘处理中 F12 对应的表项。Linux 0.11 的 keyboard.S 里大概是这样一张表:

// kernel/chr_drv/keyboard.S(示意) func_table: .long none, none, ... // F1~F11 各自的处理 .long f12_handler // F12 的处理函数

原始代码里 f12_handler 可能直接调用了 C 函数 show_stat()。我们要做的修改,是把这里的行为从「无条件调用 show_stat」改成「切换 f12_pressed 状态,再决定要不要调用 show_stat」。你可以在 keyboard.S 里把 f12 的入口改成跳转到一个新的汇编片段,也可以直接在 C 层拦截——我一般会定义一个 C 函数,让汇编表项调它:

// kernel/chr_drv/console.c void f12_handler_wrapper(void) { f12_pressed = !f12_pressed; // 翻转状态 if (f12_pressed) { show_stat(); // 第一次按下,保留原来的调试输出 } // 如果不想显示内核栈信息,把 show_stat() 去掉即可 }

这样做的逻辑是:第一次按下 F12,f12_pressed 从 0 变 1,同时还能看到 show_stat 的内核栈信息(满足实验报告第一个问题);第二次按下,f12_pressed 变回 0,不再调 show_stat。这就把「按下 F12 进入过滤模式,再按退出」的语义立住了。

注意:汇编里的函数名要带下划线前缀,这是 0.11 时代 a.out 格式的符号约定。C 里写 f12_handler_wrapper,汇编里引用就是 _f12_handler_wrapper,别在链接时踩这个坑。

3.3 修改 con_write:只把显示终端的字符替换成 *

核心改动在 con_write 里。这个函数会逐个字符处理输出缓冲区的内容,遇到普通可见字符就写入显存。我们需要在写入显存之前判断:如果 f12_pressed == 1,就把字符替换成 '*'。实现大概是:

// kernel/chr_drv/console.c // con_write() 内部,处理可显示字符的分支 if (f12_pressed) { c = '*'; // 无论原字符是什么,统一替换 } // 然后继续走原有的显存写入逻辑

要注意替换的位置。con_write 里除了可见字符,还有换行、退格、控制字符的分支。理论上你只替换 0x20~0x7E 范围内的可见字符就够了,换行和制表符不要动,否则终端显示会乱掉。我当时的做法是加一个判断:

if (f12_pressed && c >= 0x20 && c <= 0x7E) { c = '*'; }

这样回车换行还是正常换行,只是字母数字变成星号。终端不会出现排版错乱。

3.4 扩展:修改 file_dev.c 的 file_write,让文件输出也被过滤

实验报告里明确问了:你的实现有没有过滤向文件输出的字符?如果没有,怎么把写文件的也过滤?对应的做法是改 fs/file_dev.c 的 file_write()。这个函数负责把用户缓冲区的内容写入到普通文件对应的块设备上。0.11 里 file_write 有个循环,逐字节拷贝用户数据到内核缓冲:

// fs/file_dev.c // file_write() 内部示意 while (count > 0) { // 原有逻辑:从用户缓冲区取字符写入 bh *(p++) = c; // 第 81 行附近,字符写入缓冲区块的代码 }

实验报告里那句「改成(p++)=''」指的就是这一步。如果你想连写文件也过滤,可以改成:

if (f12_pressed && c >= 0x20 && c <= 0x7E) { *(p++) = '*'; } else { *(p++) = c; }

注意:这里 f12_pressed 也是同一个全局变量,需要在 file_dev.c 里用 extern 声明。这样一来,过滤就同时作用在终端和文件两条输出路径上。反过来,如果只想过滤终端输出,就只改 con_write,file_dev.c 完全不动。

3.5 添加头文件声明并编译运行

为了让两个文件共享 f12_pressed,你要在 console.c 里定义它,在 file_dev.c 和可能用到的其它文件里声明。最省事的办法是在 include/linux/tty.h 里加一行 extern 声明,这样所有包含 tty.h 的 C 文件都能直接用:

// include/linux/tty.h extern int f12_pressed;

然后在 console.c 里保留定义int f12_pressed = 0;。编译时,Linux 0.11 用的是 Makefile 驱动编译,你在实验目录下执行:

make clean make

生成新的 Image 文件后,用 Bochs 或 QEMU 加载启动。0.11 实验一般是在 Bochs 里跑,你只需要把编译出的 Image 替换到启动配置指定的位置。启动后敲几个字母,屏幕显示正常;按 F12,再敲字母,屏幕上全是星号;再按 F12,恢复原样。如果还能看到内核栈信息打印,说明 show_stat 部分也保留了。

提示:Linux 0.11 的编译环境建议用实验配套的交叉工具链,不要用现代 gcc 直接编。老内核代码里有些语法和链接方式跟现在的工具链不兼容,硬编会报一堆错。

4. 避坑与排查:我改 F12 实验时踩过的四个典型坑

4.1 按下 F12 没反应,功能键表项没对上

现象:编译通过,启动后按 F12,屏幕毫无变化,字符照常显示。

原因:F12 的扫描码是 0x58,对应键盘扫描码表里的某个序号。如果你改的不是 func_table 里 F12 那个位置,或者你改成了 F1~F11 的某项,按键自然没效果。另一个常见原因是键盘模式,0.11 的键盘驱动默认处理的是 PC/AT 键盘扫描码,如果你的模拟器把键盘设置成了不一样的标准,扫描码对不上。

解决:先别改代码,在原版系统启动后按 F12,看是否触发 show_stat 打印内核栈。如果原版都没反应,检查 Bochs 或 QEMU 的键盘类型设置;如果原版有反应、改完没反应,回头检查你替换的汇编表项下标。我习惯在 f12_handler_wrapper 里临时加一个printk("f12 toggled\n"),用串口或屏幕输出确认函数有没有被调用到。

4.2 改了 file_dev.c,系统启动后文件系统损坏

现象:按实验报告改了 file_dev.c 的 81 行,把所有输出字符都变成 '*',结果系统跑一会儿后文件读写报错,甚至启动时根文件系统挂载失败。

原因:file_write 是通用的文件写入路径,不只是你手动 echo 时才走。系统运行中,内核或某些守护进程也会写文件,比如更新日志、写临时文件。你把所有写入字符都替换成 '*',等于破坏了一切写文件操作的数据完整性,最终导致文件系统内容损坏。

解决:这个改法本来就是实验性的,验证完「文件输出也被过滤」这个结论后,立即改回原样。不要长时间保持这种状态。如果想深入验证,建议在 file_write 里加一个条件,只对特定文件描述符过滤(比如if (f_flags特殊标志),或者用 printk 打印观察哪些进程调了 file_write,而不要全局替换所有字符。

4.3 在 con_write 里过滤时,光标和退格行为异常

现象:过滤模式打开后,按 Backspace 删字符,终端上出现连续的星号残留,或者换行时位置错乱。

原因:你把控制字符也替换成了 ''。回车(0x0D)、换行(0x0A)、退格(0x08)这些字符在 con_write 里有独立的处理分支,用来移动光标或清字符。如果拿 '' 把它们的值覆盖了,光标逻辑就走不到,屏幕自然乱套。

解决:严格限定只替换可见字符区间,用c >= 0x20 && c <= 0x7E判断。控制字符保持原样传到原有分支。改完之后,做个简单测试清单:普通字母、数字、空格、回车、退格、方向键,逐项确认。

4.4 F12 变成了「按住才过滤」,不是「按一次切换」

现象:按下 F12 期间字符变成星号,松开 F12 字符又恢复,无法保持过滤模式。

原因:你把 f12_pressed 赋值成 1,但没写松开处理;或者你把它放在键盘中断的 make code 分支里,每次按键(包括重复按键)都重新置 1。更典型的错误是直接在 F12 的按下动作里设置f12_pressed = 1,却没有在 break code(松开)时做任何事情,导致状态被下一次按键覆盖。

解决:明确使用 toggle 逻辑,F12 每触发一次处理函数就取反一次,不要区分按下和松开。如果你用的入口只在 make code 时调用,那就天然满足「按一下切换一次」;如果处理函数会在按下和松开各调用一次,那你需要只在 make code 时翻转,或者在翻转逻辑上多加一个判断。0.11 原始表项通常只针对按下事件,用取反没问题。

5. 验证与进阶:把你的过滤实验做成可观察的系统行为

做完基本改动后,别急着交实验报告,花半小时做一轮系统验证,你会发现这门课最值得理解的部分其实在验证过程里。

先做一个基础功能测试:启动系统,按 F12 开启过滤,敲入ls /并回车,终端上会显示一行星号而不是ls /的实际输出。注意细节——命令本身敲进去时是星号,命令执行后产生的输出字符也会是星号吗?答案取决于你过滤的位置:如果你只改了 con_write,命令执行后的输出同样经过 con_write,因此也被替换成星号;但命令是否真的执行了,取决于 tty 读队列里的原始字符。键盘输入已经进入了 tty 队列,shell 读到的是原始字符,所以命令会照常执行,只是显示不出来。这就带出一个值得写进实验报告的现象:过滤显示字符 ≠ 过滤输入字符,终端显示和键盘输入是两条独立路径。

如果你想进一步验证「文件输出是否受影响」,可以在过滤模式下执行echo hello > /tmp/test.txt,然后用原始代码重新启动系统(因为当前系统处于过滤模式,你无法正常查看文件内容),或者直接关掉过滤后再cat /tmp/test.txt。如果 file_dev.c 没改,文件里是正常的 hello;如果改了,文件里就是*****。这个对照实验能直接回答实验报告里那个「是否把向文件输出的字符也过滤了」的问题。

更高一层的做法,是把过滤逻辑收敛到一个独立函数里,不要散落在 con_write 和 file_write 两处重复判断。比如定义一个char filter_char(char c),在两处统一调用:

// kernel/chr_drv/console.c 和 fs/file_dev.c 里共用 char filter_char(char c) { if (f12_pressed && c >= 0x20 && c <= 0x7E) return '*'; return c; }

在 con_write 和 file_write 里都改成c = filter_char(c);。这样你后续想扩展过滤规则——比如过滤成 '#'、只过滤数字、只过滤小写字母——只改这一个函数就行。我第二次做这个实验时就重构成了这个结构,排错明显快很多。

验证脚本层面,0.11 的终端能力有限,没法跑复杂的自动化测试脚本。但你可以利用系统自带的 shell 循环做一次粗糙的压力测试:过滤模式下连续执行几十条echo命令,观察终端有没有漏过滤、光标位置有没有偏移、系统有没有崩溃。正常情况是稳定显示星号且无异常。

最后说一个我的习惯:每次改完内核代码,我会在实验报告里附上改动文件的 diff 级说明,标注每个文件改了几行、为什么改、验证了什么现象。尤其是 file_dev.c 这种影响面大的改动,一定要记录原始代码和修改代码的对照,方便随时还原。从那以后,我每次做操作系统实验都会强制走一遍「原始行为确认 → 最小改动 → 对照验证 → 还原测试」这个流程,宁可慢十分钟,也不要改完不知道哪一步把系统搞崩了。希望这份拆解能让你在实验 7 上少走弯路,一次跑通。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询