实验报告
题面
感兴趣的同学可以看看:
|
|
说在前面
本次shell挑战性任务整体来说难度还是很大的,需要实现的内容太多了。不过在今年的几个挑战性任务中可能还算是最容易拿到满分的,因为数据点评测比较简单,难度不高于样例。(虽然样例也称不上简单)
由于本次作业我选择在17周(也就是考试周)完成,所以做的时候时间比较赶,总共用了周一周二两天时间很极限地完成了。具体时间安排可以看下图。
我完成的顺序是:
- 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等等。
比如:把
|
|
改为
|
|
环境变量设置
本来打算用链表存的,但是宏写起来还是没那么方便。
因此开了几个全局静态数组,存储这些信息。并且强制按顺序进行环境变量的放置,被删除了也不再使用这个位置。
为了解决环境变量这一关,我写了七个系统调用:
|
|
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
的结构,这种字符串任务还是交给大模型。
环境变量值替换
这个的实现我放在了get_token之中。如果我读到$,就也把他当做WORD型token,但token的value就要经过get_env_var的解析。
可是难点在于$key与其他字母是可以直接拼接的,
就比如declare A=l
的条件下,
$As
也要翻译为ls
这种复杂任务也是选择交给了大模型。
这里的实现是错误的,必须$紧跟的那一串有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的末尾。
难点在于切换指令。也就是本地数组与屏幕显示的处理。
- 在你按上下键切换指令的时候,你需要将当前的内容存进你本地数组的对应位置。
- 你要存一整个字符串,不能因为空格而影响存储
- 切换后显示内容清空再加载,光标自动放在末尾
反引号
基本实现方式
实现方式是开启一个子shell,在子shell中完成指令并用输出的内容替换反引号及引住的内容。
这个我也是在get_token中实现的,不过这个的优先级很高,只要读到反引号我就一直读一直等,直到等到反引号结束其属性为WORD,否则异常退出。
那么读完反引号之后,我们要把内容取出来,开一个新shell,然后把内容传进shell让他去readline和runcmd,输出的内容还要传出来作为token的value。
传递的方式是管道。
debugf与printf
其实当时很担心,怎么把输出的内容提取出来?每次控制台上的输出除了我们想要的之外,还有一堆env_kill的相关句子,难道要手动排除吗?
实践发现只有printf的句子会被提取。
那么就警示我们两点:
- 标准的输出必须要使用printf而不能用debugf
- 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。
另外,祝今天高考查分的学弟学妹们取得优异成绩!