任务要求

挑战性任务共分为必做和选做两部分。

必做部分

实现一行多命令

; 分开同一行内的两条命令,表示依次执行前后两条命令。; 左右的命令都可以为空。

实现后台任务

& 分开同一行内的两条命令,表示同时执行前后两条命令。& 左侧的命令应被置于后台执行,Shell 只等待 & 右侧的命令执行完毕,然后继续执行后续语句,此时用户可以输入新的命令,并且可能同时观察到后台任务的输出。你需要自行设计测试,以展现此功能的运行效果。& 左侧的命令不能为空。

实现引号支持

实现引号支持后,shell 可以处理如: echo.b "ls.b | cat.b" 这样的命令。即 shell 在解析时,会将双引号内的内容看作单个字符串,将 ls.b | cat.b 作为一个参数传递给 echo.b

实现键入命令时任意位置的修改

现有的 shell 不支持在输入命令时移动光标。你需要实现:键入命令时,可以使用 ←Left→Right 移动光标位置,并可以在当前光标位置进行字符的增加与删除。要求每次在不同位置键入后,可以完整回显修改后的命令,并且键入回车后可以正常运行修改后的命令。

实现程序名称中 .b 的省略

目前的用户程序被烧录到文件系统中后,其可执行文件以 .b 为后缀,为 shell 中命令的输入带来了不便。你需要修改现有的实现,以允许命令中的程序名称省略 .b 后缀,例如当用户指定的程序路径不存在时,尝试在路径后追加 .b 再打开。

实现更丰富的命令

参考实验环境中的 Linux 命令 treemkdirtouch 来实现这三个命令,请尽可能地实现其完整的功能。

为了实现文件和目录的创建,你需要实现用户库函数 mkdir() 和文件打开模式 O_CREAT

实现文件的创建后,你需要修改 shell 中输出重定向 > 的实现,使其能够在目标路径不存在时自动创建并写入该文件。

实现历史命令功能

Linuxshell 中我们输入的命令都会被保存起来,并可以通过 ↑Up↓Down 键回溯,这为我们的 shell 操作带来了极大的方便。在此项任务中,需要实现保存所有输入至 shell 的命令,并可以通过 history.b 命令输出所有的历史命令,以及通过上下键回溯命令并运行。

禁止使用局部变量或全局变量的形式实现保存历史命令,即不能用进程的堆栈区保存历史命令。

禁止在烧录 fs.img 时烧录一个 .history 文件,即你需要在第一次写入时,创建一个 .history 文件,并在随后每次输入时在 .history 文件末尾写入。

选做部分

实现 shell 环境变量

支持 declare [-xr] [NAME [=VALUE]] 命令,其中:

-x 表示变量 NAME 为环境变量,否则为局部变量。

环境变量对子 shell 可见,也就是说在 shell 中输入 sh.b 启动一个子 shell 后,可以读取并修改 NAME 的值,即支持环境变量的继承。

局部变量对子 shell 不可见,也就是说在 shell 中输入 sh.b 启动一个子 shell 后,没有该局部变量。

-r 表示将变量 NAME 设为只读。只读变量不能被 declare 重新赋值或被 unset 删除。

如果变量 NAME 不存在,需要创建该环境变量;如果变量 NAME 存在,将该变量赋值为 VALUE

其中 VALUE 为可选参数,缺省时将该变量赋值为空字符串即可。

如果没有 [-xr][NAME [=VALUE]] 部分,即只输入 declare,则输出当前 shell 的所有变量,包括局部变量和环境变量。

支持 unset NAME 命令,若变量 NAME 不是只读变量,则删除变量 NAME

支持在命令中展开变量的值,如使用 echo.b $NAME 打印变量 NAME 的值。

支持相对路径

MOS 中现有的文件系统操作并不支持相对路径,对于一切路径都从根目录开始查找,因此在 shell 命令中也需要用绝对路径指代文件,这为命令的描述带来了不便。你需要为每个进程维护工作目录这一状态,并为 open() 等用户库函数增加对参数中相对路径的支持,将不以 / 开头的路径视为相对路径,从当前进程的工作目录开始查找。同时,你需要添加 chdir()getcwd() 等库函数,以支持切换当前进程的工作目录,并使进程的工作目录在 fork()spawn() 时被子进程继承,从而实现以下功能:

支持内部命令 cd <path>,切换工作目录到 <path>,其中 <path> 可以是绝对路径或相对路径;

支持 pwd 命令,输出当前工作目录;

在切换工作目录后,测试 cat.bls.btouch.b 等接受文件参数的命令,确保其参数中的相对路径能够正常工作。

实现

添加文件、命令

添加新的命令时,需要修改 /user/include.mk,在 USERAPPS 下添加对应文件的 .b

想要将自己的 C 语言代码添加到编译过程(用户库)中,需要在 user/include.mkUSERLIB 后添加对应文件的 .o

一行多命令

sh.c 中已经为我们预留了 ;token,因此我们无需改变词法部分,只需改变语法解释部分。

由于需要实现命令的依次执行,因此我的做法是在 fork() 之后,子进程运行 ; 之前的指令,父进程等待子进程结束之后运行剩下部分的指令。

后台任务

sh.c 中已经为我们预留了 &token,因此我们无需改变词法部分,只需改变语法解释部分。

同样是在 fork() 之后让父子进程分别运行前后的部分。我的方法是让子进程运行 & 之前的指令,父进程无阻塞地运行 & 之后的指令。

注意到 read() 函数会让进程阻塞在内核态从而没法进行进程调度,需要更改阻塞逻辑,将阻塞位置从内核态转移到用户态。

引号支持

sh.c 中没有为我们预留 "token,因此需要根据需求改变词法解释器或语法解释器。

我的做法是改变词法解释器,当解析到 " 时则继续读取字符直到读取到匹配的 " 为止。将整个字符串作为一个元素返回给语法解释器。

我的实现可以支持转义字符。效果如下所示:

1
2
3
4
$ echo "a\',\nancd\t,me"
a',
ancd ,me
$

后来发现 shell 下的转义与 C 语言中的转义不是一个意思……不过问了助教这么做也可以所以就没动了。

键入命令时任意位置的修改

这部分进行的工作比较多。我重写了一遍读取行的部分(user/sh.c 中的 readline() 函数)。因为我认为在原有代码基础上新增功能比较复杂且容易出 bug(最主要是要理解原本的逻辑太麻烦了而且也不方便自己维护),还好原本 readline() 的内容就不算多,因此重构得还算顺利。

重构后的逻辑是:保存当前光标的位置,每次键入时都会保存光标之后的字符串(若有的话),依次输出光标前的字符串、输入字符、光标后的字符串。在实现过程中要注意维护光标的位置。

部分代码:

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
if (c == ESC) {
char temstr[5] = {27};
if ((r = read(0, temstr+1, 1)) != 1) {
if (r < 0) {
debugf("read error: %d\n", r);
}
exit();
}
if ((r = read(0, temstr+2, 1)) != 1) {
if (r < 0) {
debugf("read error: %d\n", r);
}
exit();
} // 读取键盘输入
switch (judge_esc(temstr)) { // 判断输入
case RIGHT:
if (cnt > buf_len) {
move_cursor(LEFT);
cnt--; // 维护光标位置
}
break;
case LEFT:
if (cnt > 0) { // 移动光标位置
cnt -= 2;
} else {
cnt = -1;
move_cursor(RIGHT); // 维护光标位置
}
break;
default:
user_panic("error in readline\n");

}
continue;
}

程序名称中 .b 的省略

这部分需要修改 spawn.c 中的内容。

具体而言,在尝试打开文件失败时,首先将 .b 拼接到当前文件名之前(当前文件名末尾不是 .b),然后再次运行 spawn() 函数即可。

tree

实现这个功能需要遍历目录。打印树需要递归实现。

关于如何遍历目录,可以参考 ls.c 文件。

伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void print_tree(char* dir_path, const char* indent) {

FILE file = getfile(dir_path);
if (file is dir) {
for (sons in file) {
if (sons is dir) {
printf(indent + "|-" + file.name);
print_tree(dir_path + file.name, indent + "| ")
} else if (sons is file) {
printf(indent + "|-" + file.name);
}
}
} else {
return;
}
}

touch 和 mkdir

完成创建文件和文件夹的功能,具体而言是在 fs/serv.c 下修改 serve_open() 函数。在 fs/fs.c 里已经为我们实现了 file_create 函数,我们主要需要做的是在 fs/serv.c 中实现这一接口。

完成接口之后只需要以 O_CREAT 模式调用 open() 函数即可完成创建文件。(当然也可以支持类似于 O_CREAT | O_WRONLY,完成创建并以可写模式打开文件)

创建文件夹的时候将文件的属性更改为 FTYPE_DIR 即可。

修改 >

我们需要修改对 > 的处理以保证在重定向的目标文件不存在时能够自动创建该文件并写入。在实现了 touchmkdir 之后这个功能是很容易实现的。

具体而言,在打开文件失败时就尝试创建文件,对新创建的文件执行写入操作即可。

history 功能

我们需要实现 history 功能,完成上下键切换历史指令。这部分的工作量是必做部分最大的。我们需要完成写入、读取 .history。为了实现函数,我在 user/lib/ 目录下新建了 history.c 文件,单独放置实现 history 功能的函数。就我个人而言,我实现的函数包括:

  • history_init(): 初始化 .history 文件。
  • int history_open(): 打开 .history 文件。
  • void history_write(const char* content): 将字符串内容写入到 .history 中。
  • int history_get_last(char* buf): 获取最近的历史命令。
  • int history_next(char* buf, int off, int direction): 从给定的偏移处,按照指定的方向寻找下一个(上一个)历史指令。

对于每一条历史指令,存入 .history 时采取 <strlen><string><strlen> 的格式,从而能够实现双向的遍历。

相对路径

我选择做相对路径部分,因为实现了相对路径之后 shell 使用起来将更加方便。

想要支持工作目录,需要在每个进程中维护各自的工作目录。需要在 include/env.h 中添加相应的变量。

注意,在进程控制块中添加对应变量后需要维护进程块初始化、fork() 时相应变量的变化。

此外需要至少实现两个系统调用。一个用于实现用户进程获取当前的工作目录,一个用于用户进程改变自己当前的工作目录。

添加系统调用的流程如下:

  • user/include/lib.h 中添加 syscall_*() 函数。
  • user/lib/syscall_lib.c 中实现对应的函数。
  • include/syscall.h 中添加对应的系统调用号 SYS_*
  • kern/syscall_all.c 中实现内核部分的函数并将调用关系添加到 syscall_table 中。

获取工作目录直接在内核态 strcpy() 一下就行。

比较麻烦的是改变工作目录这个东西,不得不进行父子进程的通信(或是进行一些神秘的危险操作……)。原因在于在 sh.c 中会 fork() 一个子进程运行 runcmd() 函数,这样在进行 cd 操作时是在子进程中操作的。这个问题的解决办法不唯一。也可以允许子进程直接在内核态修改父进程的工作目录(不过这很危险)。

下一步是解析相对目录。这个是一些字符串操作,相对而言没什么困难的地方。

我在实现解析相对目录之后,修改了 open() 函数,对传入 open() 函数的参数进行相对目录解析,从而基本实现了对相对目录下文件的各种操作。

环境变量

在提交 ddl 的前一天终究是也把环境变量给做了…做起来发现环境变量比相对路径好做多了,泪目。

为了实现环境变量,我是直接在内核态新定义了一种结构体 struct Var,用于保存环境变量相关的信息。

具体要保存的信息至少有:变量的名、变量的值、变量的权限、变量的所有者。

共实现了五个对应的系统调用:

  • sys_get_shell_id:给一个 shell 分配一个独特的 shell_id,用以标识一个独特的 shell
  • sys_declare_write:写入一个环境变量。
  • sys_declare_unset:取消一个环境变量。
  • sys_declare_get:获取一个环境变量。
  • sys_declare_getall:获取所有环境变量。

在所有操作里都需要对环境变量的权限、所有者等进行逻辑判断。

在实现 declareunset 两个函数时,需要额外将当前 shellsheel_id 作为参数压入。

在进行参数的替换时,对当前所有的参数进行判断,若参数首字母为 $ 且能取得对应的环境变量,则用环境变量的值替换掉参数。

以上均为我个人的实现方式,不保证思路完全正确,欢迎讨论交流。