Lab6-Challenge
(以下代码均已模糊化处理,请勿抄袭)
必做部分
1、实现一行多命令
要实现一行多命令,其实就是将一行的指令用;
分隔开来,每当遇到;
就让shellfork一次,然后子shell去执行前面已经解析出来的命令,父shell等待子shell解析完成之后再继续解析后面的指令,直到将一行解析完成。主要在sh中进行以下修改:
case ';':
fid = fork();
//do something
break;
使用以下命令进行测试:
touch 1 ; touch 2; touch 3; tree
2、实现后台任务
该任务要求在一行中输入两条命令,用&分隔,前面的指令在后台运行,shell只等待后面的指令运行完成就准备接受下一条命令。
要实现该要求,我们首先应该确定shell等待命令的执行是发生在哪里,可以在runcmd里面看到,当spawn指令执行后,如果child >= 0
,就执行wait指令,也就是在这里shell需要等待spawn出来的进程执行完毕在继续执行。那么为了实现后台任务,我们只需要在这里稍作修改,如果命令是&前面的命令,让shell无需等待其执行即可。具体来说就是添加一个判断是否等待的全局变量,然后在解析到&
的时候先fork,然后将子shell的该变量设置为false,再让子shell去执行该命令,父shell直接继续执行后续指令即可。
具体实现如下:
//parsecmd
case '&':
fid = fork();
//do something
break;
//runcmd
if (child >= 0) {
if(hang) wait(child);
} else {
debugf("spawn %s: %d\n", argv[0], child);
}
为了展示该指令的实现结果,需要前面的指令执行时间较长,可以在后面指令执行完毕后继续输入指令,同时观察到前面指令的执行结果,而tree指令就正好符合该要求。
所以在之前的基础上使用如下指令测试:
tree & touch 1
在开始执行后,tree还没来得及输出,touch指令已经执行完毕,并且shell可以继续输入。所以接下来我们直接输入一条空指令,然后会发现:
前面的tree指令继续执行,同时开头直接就destory了一个进程,该进程就是我们之前为了执行tree指令而fork出来的子shell,它没有等待spawn出来的tree进程结束就已经结束了,从而实现了任务的后台执行。
3、实现引号支持
该任务要求在解析指令时将""
内的内容看做是单个字符串,要实现该要求,我们只需要在解析指令的时候对""
进行特判,如果遇到双引号就直接继续向后解析,直到遇到另一边的双引号,并且将解析到的两个双引号之间的字符串作为参数返回即可。具体的实现如下:
//_gettoken
if(*s == '"') {
*s = 0;
*p1 = ++s;
//do something
return 'w';
}
对于该任务的测试,我采用以下命令:
echo "ls.b | cat.b"
4、实现键入命令时任意位置的修改
要实现该任务,大致思路就是在shell一开始读入字符串的时候对左右方向键进行特判,然后根据输入分别维护命令行的回显以及读入的buf两部分。
方向键在输入时其实是三个字符:
上键:27 '[' 'A'
下键:27 '[' 'B'
左键:27 '[' 'D'
右键:27 '[' 'C'
首先得设定一个表示光标位置的变量pos,如果遇到左右方向键就将该输入忽略,同时维护pos变量,让它的值与光标的实际位置保持一致,然后先维护命令行的回显,也就是根据输入的方向键移动命令行的光标。这样就实现了让光标根据我们输入的左右键进行移动,然后我们需要处理添加字符和删除字符的指令。
对于添加字符,当读入字符时,判断pos是否和此时的字符串总长一致,否则说明光标不在字符串末尾,此时我们首先需要删除光标之后的内容,将输入的字符显示出来,然后再将刚才删掉的字符串再次打印,之后维护光标的位置,就实现了添加字符的回显。但是此时我们的buf还没有更新,也就是说刚刚添加的字符依然在buf的末尾,我们需要将其移动到刚才插入的位置,对字符串进行操作即可。最后别忘了维护pos的值。
对于删除字符,当读到删除字符时,依然是先判断pos的位置,如果光标不在字符串末尾,就先删掉光标前一个位置到末尾的所有字符,然后重新打印光标到末尾的所有字符,再维护光标位置,实现shell窗口回显。然后依然是维护buf的内容将刚才删掉的字符对应删除即可。最后维护pos的值。
当然,在实际实现的时候并没有这么简单,还需要对左边界、右边界的各种情况进行特判,要保证光标不能到达他不应该到的地方,pos也应该限制在正常范围内,否则就会出现奇奇怪怪的bug。
具体实现的部分在readline之中:
//维护光标
else if(ch == 'D'){
//do something
continue;
} else if(ch == 'C') {
//do something
continue;
}
//添加字符
if(pos < i) {
//do something
}
//删除字符
if (buf[i] == '\b' || buf[i] == 0x7f) {
//do something
}
对于该部分的测试,主要就是尽可能地随便移动光标,然后添加删除字符,判断功能实现是否正确,比如我下面的测试(只是瞎输入的一小部分):
tou nnendfo12334enfoeqn ch
tou nne nfqn ch
1
touch 1
5、实现程序名称中 .b
的省略
这个任务比较简单,就是给指令改一个名字的问题。因为我们有.b的指令都是通过spawn来执行的,因此只需要在spawn的时候对传入的指令名称进行判断,如果指令名字后面没带.b的话就自己加上一个再打开即可。具体对spawn的修改如下:
if(len < 2 || prog[len - 1] != 'b' || prog[len - 2] != '.') {
//do something
}
测试命令:
cat
1122333344224444
6、实现tree 、mkdir、touch命令
mkdir指令和touch指令的实现比较类似,本质都是创建一个新文件的过程,只不过文件类型不同。因为目前我们的MOS并没有提供创建文件的用户接口,所以我们先得自己提供一个接口。大致过程就是先在include/fs.h中新建一个Fsreq_create结构体,内容是文件路径以及类型。然后在文件系统端实现serve_create函数,使用已有的file_create来创建一个新的文件。之后在用户态的fsipc.c中增加对应的用户态通讯接口,在file.c中添加创建文件的用户接口。
这样一来我们就有了创建文件的接口,然后我们只需要实现两个用户态指令文件mkdir.c和touch.c即可,这两个文件的实现基本类似,本质就是将参数传递给刚才实现的create接口,让文件系统按照我们的要求创建文件。具体实现如下:
//touch.c
#include <lib.h>
int main(int argc, char **argv) {
//do something
return 0;
}
mkdir的实现只需要修改对应的文件类型。
接下来是tree指令的实现,其实tree指令的本质就是一个递归问题,只需要递归输出所有的目录和文件名称即可,但是注意要保证一定的格式。具体实现如下:
#include <lib.h>
void print_tab(int num) {
int i;
for(i = 0; i < num; i++) {
fprintf(1, " ");
}
}
void walktree(int depth, char *dir) {
//do something
int num = ROUND(size, sizeof(struct File)) / sizeof(struct File);
struct File *file = (struct File *)fd2data(fd);
for(i = 0; i < num; i++) {
//do something
}
}
int main(int argc, char **argv) {
char *dirname;
if(argc > 2) {
fprintf(1, "usage: tree [dirname]\n");
return -1;
}
dirname = (argc == 1)? "/" : argv[1];
walktree(0, dirname);
return 0;
}
对以上三条命令集中测试:
touch a ; touch b; touch c; mkdir newdir;touch newdir/new;tree
当然,该任务到这里还并没有结束,还有一项要求是在管道重定向到一个不存在的文件时,需要自动创建该文件。要完成该项要求,需要实现O_CREAT的文件打开方式,在open中如果遇到该打开方式,首先判断文件是否存在,存在则正常打开,不存在的话就使用file_create创建一个即可,具体实现如下:
if ((r = file_open(rq->req_path, &f)) < 0) {
if((rq->req_omode & O_CREAT) == O_CREAT) {
//do something
} else {
ipc_send(envid, r, 0, 0);
return;
}
}
当然,还需要在解析到管道指令的时候给文件打开方式加上O_CREAT。
测试如下:
echo hello > hello.txt
cat hello.txt
7、实现历史命令功能
该任务的实现主要分为两部分,即history
指令的实现以及通过上下方向键回溯指令。
要实现这两个功能,基础是得先记录下来所有的输入指令,我们只要在每一次runcmd之前记录一下读取到的指令即可。
具体实现如下:
int save_history(char *cmd) {
int r = open(".history", O_CREAT | O_WRONLY | O_APPEND);
//do something
return 0;
}
history指令只需要读取.history文件的所有内容,将其输出:
#include <lib.h>
int main(int argc, char **argv) {
//do something
while(read(fd, &buf, 1)) {
//do something
}
return 0;
}
最复杂的部分还是实现通过上下键回溯指令。大致思路是像之前判断左右键一样对上下键进行特判,一旦输入到上下键,首先将已经输入内容清空,然后去.history文件中读取之前的指令并输出到控制台。但是这个过程中要注意许多的细节问题,例如如何记录已经读取到的指令位置,如何对光标的位置进行限制等等。
具体的实现如下:
//readline
if (ch == 'A') {
if(i) {
fprintf(1, "\x1b[1B\x1b[%dD\x1b[K", i);
} else {
fprintf(1, "\x1b[1B");
}
//do something
}
else if(ch == 'B') {
if(i) {
fprintf(1, "\x1b[%dD\x1b[K", i);
}
//do something
}
//get_history
int get_history(int back, char *buf) {
static struct Fd *fd;
static int fdnum = 0;
static char *va;
static char *begin;
static char *end;
if(!fdnum || newcmd) {
//do something
}
if(back) {
//do something
}
buf[i] = 0;
return i;
} else {
//do something
if(*va == '\n') {
p = va + 1;
}
while(*p != '\n' && p < end) {
buf[i++] = *p++;
}
buf[i] = 0;
return i;
}
}
在实现了基本的功能之后,我还增加了保存当前已输入内容的功能,也就是当回溯不到相关指令时,将之前已经输入的字符串再次打印到标准输出,这样一来就与bash更加接近。
测试如下(假装是动态的):
touch 1; touch 2
echo hello
echo bad
history
上下键回溯发现一切正常,并且因为对光标加以限制,它也不会乱跑到不该去的地方了(
选做部分
在选做部分中,我选择的是实现shell的环境变量。
要实现环境变量,首先要知道两种环境变量的类型,也就是环境变量和局部变量。其中,
环境变量:对子shell可见,但是子shell修改其内容时对父shell不可见。
局部变量:对子shell不可见。
概念中的子shell其实指代的是shell运行过程中通过sh命令spawn出来的shell,而对于fork出来的shell,应该是可以和父shell共享相同的环境变量和局部变量。
对于环境变量和局部变量,我采用了两种存储方式,环境变量存储在内核态中,局部变量则直接作为shell进程的一个全局变量。存储的方式都是使用EnviormentValue
结构体进行存储,该结构体包含了变量名、变量值、读写方式以及创建id等内容。
对于内核态中存储的环境变量,需要增加对它进行访问和修改的系统调用,具体步骤和之前添加系统调用的老套路一样,先添加中断向量,然后在内核态实现系统调用的具体函数,在用户态添加该调用的接口即可。需要注意,因为子shell对环境变量的修改对父进程不可见,因此需要在创建子shell的时候将父进程环境变量都给子进程复制一份。
对于用户态的局部变量,可以将其当做一个正常的数组进行修改和访问,但是要注意的一点是,为了保证仅仅在fork的时候子进程对局部变量的修改对父进程可见,但是对于spawn出来的shell则不行,需要新增一条地址权限PTE_FORK,使得拥有该权限的地址只和fork出来的子进程进行内存共享,然后给局部变量所在地址映射为该权限。
除此之外,因为对环境变量进行访问时需要判断是否为该进程的环境变量(读取无所谓,主要是修改的时候要用),因此我们需要设置一种标志不同shell的方式,也就是给shell一个id,这个id需要保证fork出来的子shell和父shell相同,但是spawn出来的不同。我的实现思路是在shell中加一个sh_id全局变量,然后在shell刚开始运行时将其赋值为该进程的envid,这样就能保证fork出来的子进程仍然和父进程有同一个sh_id,并且spawn出来的不同,实现了对环境变量的互斥访问。
一些重要的函数实现如下:
//系统调用主函数
int sys_getGlobalVar(char* name, char* val, int mode, int id, int rwMode) {
static int init = 1;
int i,j;
if(init) {
for(i = 0; i < 1000; i++) {
globalVar[i].mode = 0;
}
init = 0;
}
if(mode == 0) {//check whether has the envvalue
//do something
}
if(mode == 1) {//set value of var[name]
//do something
return -1;
}
if(mode == 2) {//delete var
//do something
return -1;
}
if(mode == 3) {//get the value
//do something
return -1;
}
if(mode == 4) {//create a new var
//do something
return -1;
}
if(mode == 5) {//copy parent to son
//do something
return 0;
}
if(mode == 6) {
//do something
return -1;
}
if(mode == 7) {
//do something
return 0;
}
return 0;
}
//环境变量和局部变量操作函数
void getVar(char *name) {
int i, r;
char val[16];
//do something
}
void showvar(){
for(i = 0;i < 1000;i++) {
r = syscall_getGlobalVar(name, val, VARGETALL, sh_id, i);
if(r >= 0) {
//do something
} else {
//do something
}
}
for(i = 0;i < 100;i++) {
//do something
}
}
void declare(int argc, char **argv) {
int i, r;
int isglobal = 0;
int isonlyrd = 0;
ARGBEGIN
{
case 'x':
isglobal = 1;
break;
case 'r':
isonlyrd = 1;
break;
defult:
fprintf(1,"usage: declare [name [=value]]\n");
break;
}
ARGEND
if(argc == 0) {
//do something
}
while(argv[0][i] != 0 && argv[0][i] != '=') {
//do something
}
j = 0;
while(argv[0][i] != 0) {
//do something
i++;
}
if(isglobal) {
//do something
} else {
//do something
}
if(i == 100) {
//do something
}
}
}
void unset(int argc, char **argv) {
int i;
ARGBEGIN
{
defult:
fprintf(1, "usage: unset[NAME]\n");
break;
}
ARGEND
if(argc == 0) {
fprintf(1, "usage: unset[NAME]\n");
}
//do something
}
总结
总体而言,这一次挑战性任务相对于之前的Lab来说的难度还是相当大的,可以说真的非常具有“挑战性”。因为无论是从里面各种数据结构的实现,还是不同函数之间的关系,亦或是对文件系统、用户进程和内核进程有关功能的添加,都需要我们对整个操作系统的结构非常熟悉,并且还得具有很强的编码和设计能力。这些对我而言都有着不小的难度。
但是将挑战性任务完成之后,我发现我对MOS系统的了解已经相对于之前有了一个质的飞跃。之前尽管完成了Lab6,但是依然对系统中实现的细节感到云里雾里,根本不太清楚各个进程之间交互的具体流程。写完挑战性任务,尤其是选做部分之后,我感觉自己对于系统调用、内核和用户态的交互,还有fork、spawn等部分都已经较为熟悉,对系统的整体架构掌握地更加充分,真正地体验到了作为一个系统设计者的感觉。