Featured image of post 北航OS Challenge Shell

北航OS Challenge Shell

实验报告

实验报告

题面

感兴趣的同学可以看看:

1
https://demiurge-zby.github.io/p/%E5%8C%97%E8%88%AAos-challenge-shell/challenge_shell.pdf

说在前面

本次shell挑战性任务整体来说难度还是很大的,需要实现的内容太多了。不过在今年的几个挑战性任务中可能还算是最容易拿到满分的,因为数据点评测比较简单,难度不高于样例。(虽然样例也称不上简单)

由于本次作业我选择在17周(也就是考试周)完成,所以做的时候时间比较赶,总共用了周一周二两天时间很极限地完成了。具体时间安排可以看下图。

git log

我完成的顺序是:

  • touch,mkdir,rm指令
  • 完成O_APPEND(没必要这么早)
  • 重写sh.c,完成AST架构构建(顺便就实现了注释,;.b省略
  • 实现工作路径以及cd,pwd
  • 实现相对路径操作
  • 实现环境变量的declare与unset
  • 实现环境变量的值替换
  • 完成各种按键的效果
  • 完成history
  • 完成反引号
  • 完成条件执行所需的返回值传递

因为内容太多,我这里将一些比较为难我的地方,以及我比较个性化的地方作为重点来讲。

内建指令与外部指令

首先要理解这两种指令的实现方式,以及区别。

外部指令

外部指令通过先fork子进程,子进程再spawn孙进程来实现。

spawn本身的功能是开一个新进程来执行二进制文件,因此外部指令的执行方式就是单独在/user目录下建立新的.c文件,并经过make形成.b文件,通过spawn执行ELF文件,结束。

(一定注意修改的是user/new.mk,修改内容是USERAPPS += mkdir.b \touch.b \ rm.b)

外部指令不应对shell的环境产生任何影响。

内建指令

内建指令则涉及到改变shell的环境,因此必须在原shell下执行。

那么他就不能fork新进程,因为新进程中的信息改动可能带不回父进程。(这个要看具体实现,但如果我的shell信息与进程强绑定就不可以fork)

执行的过程就必须在sh.c内部完成了,对于某个command就直接进行strcmp判断,如果是某条内部指令就直接执行然后结束。

区别

那么区别主要就在于fork。我们的原版sh.c是无脑先fork再runcmd的,我们的实现就只能runcmd,进去之后再看是否fork。

AST架构实现

这个可能是最阻挡人的一环,这一关过了,后面就会有内驱力促使你做下去。

因为这个实在有点庞大,本来对sh.c就一知半解,现在更是要全部推翻重来,哪些要改哪些不改要改成啥都不是很明确。

基本流程

个人建议是先用大模型直接根据sh.c生成一版AST版本的,然后再进行一定量的修改。大模型在枚举类构建,结构体构建以及函数调用层次架构上都是比较清楚和直观的。

一般的流程是,readline读取buf,然后runcmd(buf)

runcmd里面是parse_list和execute_list两个函数,这两个函数都是递归下降的方法执行的。

具体parse的过程和OOU1很像,根据时机使用get_next_token(也就是consume),访问的时候可以直接通过全局变量cur_token。

大模型的问题

不过大模型基本上刚开始都会犯一个问题:使用malloc等动态空间请求和一些安全的字符串函数(比如strdup),以及一些我们不支持的字符串函数。因此我们需要告诉它我们实现的是微内核,对安全要求没那么高,内存空间用静态数组分配,支持的字符串函数只有strlen,strcpy,strcmp和strchr。

此外大模型也经常在consume的时候出现问题,你可以对于lexer和parser进行简单的内容重写,并积极添加debugf调试,实时查看token的读取,parser的内容是否有问题。

检查与调试

上面所说的debugf大法当然很有帮助。不过注意避免不同位置输出相同格式的话,这可能会引发误解。

这个工作你可以通过写好的mkdir等指令去调试。不过请注意你还没有实现相对路径,因此根据ls.c,当你执行ls命令时(无其他参数),就会默认执行ls /,而不是ls ./。不要因此误以为自己的实现有问题。

工作路径

我设置了两个系统调用进行工作路径的set与get。

这里注意,在fork的时候需要让子shell继承工作路径,因此需要在sys_exofork前get,然后子函数中set。

当然也要注意子函数退出时要恢复工作路径为原shell。

相对路径转绝对路径

这个就相对复杂,关键在于处理无数的...,其中..的处理还不是很简单。

但是字符串相关的繁杂内容,就大胆交给大模型就好了!最终的结果也确实都是一次过。

..的处理

相对路径处理

相对路径支持

只需要进行类似于以下内容的修改。

因为你各种路径信息的最终使用者还是这些函数。类似的函数还有fsipc_remove等等。

比如:把

1
r = fsipc_open(path, mode, fd);

改为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
	char result[128];
	char middle[128];
	strcpy(middle, path);
	if (path[0] != '/') {
		char curpath[128];
		syscall_get_cur_path(curpath);
		change_to_abs_path(curpath, middle, result);
	} else {
		strcpy(result, path);
	}

	r = fsipc_open(result, mode, fd);

环境变量设置

本来打算用链表存的,但是宏写起来还是没那么方便。

因此开了几个全局静态数组,存储这些信息。并且强制按顺序进行环境变量的放置,被删除了也不再使用这个位置。

为了解决环境变量这一关,我写了七个系统调用:

1
2
3
4
5
6
7
	SYS_get_env_var,
	SYS_set_env_var,
	SYS_unset_env_var,
	SYS_print_env_var,
	SYS_clear_my_env_var,
	SYS_get_shell_id,
	SYS_set_shell_id,

get主要在下一步环境变量值替换里使用。

这里主要讲讲shell_id和clear两个设计。

环境变量最难的点就在于 子Shell对环境变量的修改不会影响父Shell

但环境变量的值需要传递给子进程,还可能被修改,又要返现,如何处理?

shell_id

首先是shell_id的设置。shell_id和子进程是两个概念,我在这个shell里面完成外部指令是需要开子进程的,但是这个子进程就完全可以使用我这个shell的非global环境变量。

因此必须设置shell_id。

get就不说了,set方法是对shell_id进行加一或减一。在shell启动界面加一,执行exit指令时就减一。这两个是个人认为非常合适的时机。

子shell如何不影响原shell

关于clear,主要就是在exit指令的时候进行一次clear,直接把这个shell_id对应的环境变量全删了。

那有人要问了,你对父shell的环境变量的改变怎么复原?我采用的是个性化的set与unset。

当我只是使用而不改变值就无事发生。如果要改变值,就新开一个条件变量。在get匹配的时候优先shell_id也对应的即可。如果要unset,就也新开一个条件变量,但是把global属性设置为异常值2,get的时候优先匹配同shell_id,检测到是2就直接返回没有找到。那如果unset之后又set呢?我们在set的时候也优先查找同shell_id的,然后在赋值的时候自然就实现了global属性的正常化,还能保证不影响原shell的环境变量。

大模型的使用

在解析的时候要处理 xxx=xxx 的结构,这种字符串任务还是交给大模型。

区分key与value

环境变量值替换

这个的实现我放在了get_token之中。如果我读到$,就也把他当做WORD型token,但token的value就要经过get_env_var的解析。

可是难点在于$key与其他字母是可以直接拼接的,

就比如declare A=l的条件下, $As也要翻译为ls

这种复杂任务也是选择交给了大模型。

寻找是否包含key并完成替换

这里的实现是错误的,必须$紧跟的那一串有var_key才行。

比如上面的例子,理应不能处理$decAare,而我的这种设计会处理为declare。不过这个问题交给大模型也容易修复。

快捷键

这一块的内容我没有什么特别的地方要说,主要就是回显的抵消以及字符串相关函数,改动的内容是sh.c里的readline函数。

之前确实也不知道printf一些字符居然可以实现光标的移动与显示的变化。

快捷键怎么解析和操作,可以参考往年的博客。

这里特别强调一些内容,在qemu中Ctrl+A会进入某种等待触发qemu快捷键(如 Ctrl+A+X)的特殊状态,这可能导致本地出现一些问题。

提问

回复

此外,在VSCode中,Ctrl+E可能会触发VSCode的快捷键;在跳板机中,Ctrl+W可能触发网页的快捷键。解决方式是把代码中case的内容改为其他不常用的快捷键进行调试。调试成功再改回去即可。

history

历史信息处理难度不小。

基本流程是开启shell时加载/.mos_history,加载到本地数组。 然后每readline完毕执行一条非空指令就写入.mos_history的末尾。

难点在于切换指令。也就是本地数组与屏幕显示的处理。

  1. 在你按上下键切换指令的时候,你需要将当前的内容存进你本地数组的对应位置。
  2. 你要存一整个字符串,不能因为空格而影响存储
  3. 切换后显示内容清空再加载,光标自动放在末尾

反引号

基本实现方式

实现方式是开启一个子shell,在子shell中完成指令并用输出的内容替换反引号及引住的内容。

这个我也是在get_token中实现的,不过这个的优先级很高,只要读到反引号我就一直读一直等,直到等到反引号结束其属性为WORD,否则异常退出。

那么读完反引号之后,我们要把内容取出来,开一个新shell,然后把内容传进shell让他去readline和runcmd,输出的内容还要传出来作为token的value。

传递的方式是管道。

debugf与printf

其实当时很担心,怎么把输出的内容提取出来?每次控制台上的输出除了我们想要的之外,还有一堆env_kill的相关句子,难道要手动排除吗?

实践发现只有printf的句子会被提取。

那么就警示我们两点:

  1. 标准的输出必须要使用printf而不能用debugf
  2. shell启动页面是printf!需要被特殊处理!

其中关于第二点,我的方式是为shell启动提供-s参数,并设置如果有这个参数就不执行哪些启动页面的输出。

条件执行

这个是我花费时间最长的一个点,最终也是耗到凌晨三点半才搞定。其实条件执行功能本身并不难,在AST里已经实现了,难点在于如何得到返回值。

内建指令的返回值直接就返回,很直白。难点在于外部指令。

外部指令先是套了一层fork,fork里面spawn又是一层fork,更何况spawn是通过执行ELF文件,我怎么获取ELF文件本身的返回值?

经过研究发现,那些.b文件的返回值只能通过user/lib/libos.c中的main的返回值来体现。

那么这么一个奇怪的位置的值又怎么传回那么遥远的原shell呢?

我的答案还是管道。此外,我还强制了文件描述符2,3,便于解决困难的传参问题。其中libos.c只能写管道,并且强制通过fdnum=3的fd来实现。

进程必须要趁它在kill之前发出信息,否则就再也找不到了。

当然还有一种解法是修改libos.c的exit函数,让父进程掌管子进程的destroy,从而在子进程消失前拿到它的exit_value。这种一般要为进程添加属性exit_value,并添加set与get的系统调用。

后记

实验报告很粗浅地写完了,OS的路程也终于画上了句号。在内核态打下的基础终于在用户态体现出如此强大的功能,个人还是很有成就感的。shell本身的可交互性与易懂性是吸引很多人选择它这个挑战的重要原因,实际上它也确实相较于swap更便于调试。但是难点就是内容实在是太多了,又不简单,两整天30个小时才侥幸拿下。我当前的实现也有一些我已知的错误,但是温柔的测试点并没有测出来。

赞美OS。

另外,祝今天高考查分的学弟学妹们取得优异成绩!

comments powered by Disqus
Easy Life and Easy Learning
使用 Hugo 构建
主题 StackJimmy 设计