BUAA操作系统lab6_challenge_shell

Cdostan MVP++

Lab6挑战性任务SHELL

任务回顾

本次SHELL挑战性任务要求我们实现的内容有:支持相对路径,支持环境变量,支持指令的自由输入,支持快捷键,支持历史指令,支持注释功能。支持反引号,支持一行多指令,支持不带.b后缀指令,支持指令条件执行,支持追加重定向,以及实现新增的多种指令。
该挑战性任务具有十足的难度,也符合挑战性任务的名号。

实现过程

我对SHELL的实现几乎按照指导书的顺序来进行,这样能够方便本地测试,但按顺序进行的时候可能由于前期没有太过于考虑架构层面的东西,导致在实现后面功能时很可能牵扯到已经实现好的内容并对其修改,最终的架构比较杂乱,但不影响功能的完整实现。

第一部分

1.1 相对路径

对于相对路径,我的想法是让每个进程存储自己当前的工作目录(以绝对路径形式),所以需要在Env结构体里新增一个工作目录属性。

1
2
3
4
struct Env
{
char env_path[1024] //新增
};

针对这个属性,需要注意的是在创建进程时需要初始化其为”/“(env_create),在fork的时候需要让子进程继承父进程的工作目录。(sys_exofork)
有了这个属性,就可以添加系统调用来获取当前进程的工作目录,以方便后续对相对路径的处理。添加系统调用:

1
int syscall_pwd_do(char *path, int mode);

其中mode参数用来指定该系统调用具体行为,如果为1,则将当前进程的工作目录改为传入的参数path,如果为0则将当前进程的工作目录传入参数path。需要注意的是,我们lab6的SHELL在执行指令时会fork出一个子进程来执行,如果指令为外部指令,则子进程会通过spawn函数再创建一个子进程来执行具体指令,因此这里存在一个问题,如果我们的系统调用只改了当前进程的工作目录,那么实际上在SHELL中切换工作目录的时候是没有达到切换效果的。针对该问题,我修改了系统调用,具体来说,就是从当前进程开始遍历父进程,并修改这一路上所有进程的工作目录即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int sys_pwd_do(char *path, int mode) {
if(mode == 1) {
struct Env *parent = curenv;
while(parent->env_parent_id > 0) {
int r;
r = envid2env((u_int)parent->env_parent_id, &parent, 0);
if (r < 0){
return r;
}
strcpy(parent->env_path, path);
}
strcpy(curenv->env_path, path);
return 0;
} else if (mode == 0) {
strcpy(path, curenv->env_path);
return 0;
} else {
return -1;
}
}

有了该系统调用,接下来让整个SHELL支持相对路径就变得较为简单了,其实就是对字符串的解析处理。为了方便,我新增了一个库文件pwd_func.c,里面存放一些和路径处理相关的函数。

1
2
3
4
void get_pwd(char *pwd);
int dealpath(char *path, char *realpath);
void getPath(char *path, char *realpath);
int move_to(char *path);

get_pwd函数用来获取当前工作目录,getPath函数用来拼接当前输入路径和工作目录(如果输入路径为相对路径形式),dealpath函数用来处理已经拼接好的路径中的“.”以及“..”,使其成为一个系统能识别的绝对路径,move_to函数用来移动当前进程的工作目录。
然后需要对使用路径的命令来进行修改使得它们能够识别相对路径,但其实观察可以发现,这些命令由于要识别路径对应的文件,所以最终要使用到的就只有两个函数,一个是open,另一个是remove,所以只需要在这两个函数的开头对传入的路径进行一下处理即可,以open为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int open(const char *path, int mode)
{
char realpath[MAXPATHLEN];
char stdpath[MAXPATHLEN];
getPath(path, realpath);
int r;
r = dealpath(realpath, stdpath);
if (r < 0)
{
return r;
}

/*原有内容*/
}

这样当前SHELL就能支持相对路径了。

1.2 pwd和cd

接下来实现pwd以及cd指令。由于这两个指令是内置指令,所以只需要在runcmd函数中添加对这两个命令的特判即可,两个命令所需要使用的函数也在pwd_func.c中全部定义好,因此实现起来没有太大的难度。

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
36
37
38
39
if (strcmp("pwd", argv[0]) == 0)
{
int argNum = 0;
while (argNum < MAXARGS - 1 && argv[argNum + 1] != NULL)
{
argNum++;
}
if (argNum != 0)
{
debugf("pwd: expected 0 arguments; got %d\n", argNum);
exit();
}
char nowpath[MAXPATHLEN];
syscall_pwd_do(nowpath, 0);
debugf("%s\n", nowpath);
exit();
}
else if (strcmp("cd", argv[0]) == 0)
{
char pwd[MAXPATHLEN];
int argNum = 0;
while (argNum < MAXARGS - 1 && argv[argNum + 1] != NULL)
{
argNum++;
}
if (argNum == 0)
{
move_to("/");
}
else if (argNum == 1)
{
move_to(argv[1]);
}
else
{
debugf("Too many args for cd command\n");
}
exit();
}

1.3 mkdir && touch && rm && exit

要能对第一个测试点进行测试,这四个指令便是最后需要实现的内容。
我首先对exit进行了实现,因为其为内建指令,所以也放在runcmd中即可。但在最初的时候我对其理解产生了偏差,直接仿照halt的实现来实现它,结果导致后续在测试子SHELL中的命令时产生了和预期不符的表现。阅读相关代码后,发现用user_halt会直接关闭所有SHELL,而不是退出当前SHELL,因此我对其进行了修改,我直接把它放在了父进程中执行,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
if (r == 0)
{
runcmd(buf);
exit();
}
else
{
wait(r);
if (strcmp(buf, "exit") == 0)
{
return 0;
}
}

针对touchmkdirrm三个指令,需要自行编写相应的文件让spawn来执行它们,这三个指令的功能其实是相近的,这里拿touch举例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc, char **argv)
{
if (argc != 2)
{
printf("usage: touch [filename]\n");
return;
}
int r = open(argv[1], O_CREAT);
if (r < 0)
{
printf("touch: cannot touch '%s': No such file or directory\n", argv[1]);
}
return 0;
}

在实现mkdir的时候,当使用open函数来创建文件时并不能将文件类型设置为目录,同时发现已有的结构中已经为我们预留了O_MKDIR这一模式,因此直接在open函数里添加mkdir时的类型设置即可。

1
2
3
4
5
6
7
8
9
10
11
if ((rq->req_omode & O_MKDIR) && (r = file_create(rq->req_path, &f)) < 0 &&
r != -E_FILE_EXISTS)
{
ipc_send(envid, r, 0, 0);
return;
}

if (rq->req_omode & O_MKDIR)
{
f->f_type = FTYPE_DIR;
}

至此,第一部分全部实现完成。

第二部分

2.1 环境变量

怎么实现环境变量其实值得斟酌,环境变量分为全局变量和用户变量,全局变量所有SHELL都可以访问,而局部变量只能被其所属的SHELL访问,这其中还有一个值得注意的地方是全局变量虽然是可以被所有SHELL访问,但并不意味着这些SHELL共享一个变量,每个SHELL对全局变量的修改都是独立的,不影响该变量在其他SHELL中的值。
首先我又在Env中新增了一个属性env_shid

1
u_int env_shid;

该属性的作用主要是来描述该进程是属于哪个SHELL的,这样才能正确访问环境变量。同样,在fork的时候该属性应该继承,然后在创建新的SHELL时需要通过系统调用重新为该属性分配一个值。
经过思考后,我决定使用一种比较简单的方法来实现环境变量,那就是把所有环境变量全部放在内核空间,然后使用系统调用来访问环境变量。
首先定义环境变量结构体,并创建一个环境变量数组以及环境变量个数:

1
2
3
4
5
6
7
8
9
10
struct EnvVar
{
char value[17];
char name[17];
int type; // 0 for all , 1 for jubu
int mode; // 0 for all , 1 for o_rdonly
int sh_id;
};
struct EnvVar envars[1024];
int varnum = 0;

接下来添加五个系统调用:

1
2
3
4
5
int syscall_alloc_shell_id(u_int envid);
int syscall_declare_var(int type, int mode, char *name, char *value);
int syscall_unset_var(char *name);
int syscall_get_var(char *name, char *result);
int syscall_get_allvar(char *result);

这些系统调用的功能如下:

  • syscall_alloc_shell_id:为当前进程分配一个新的shell_id,直接令为其进程号即可,同时将当前已有的全局变量再复制一份,并将这些全局变量的所属SHELL的id改为新分配的这个shell_id。
  • syscall_declare_var:实现declare的创建功能。
  • syscall_unset_var:实现unset的删除功能。
  • syscall_get_var:根据name找到相应的环境变量并读取其值。
  • syscall_get_allvar:读取当前SHELL所有的环境变量的值。

以上内容全部实现完后,对环境变量的支持还差最后一步,那就是支持$的解析。我最终采用了一种比较粗鲁的方法,那就是在runcmd时的开始直接对命令解析,如果遇到该替换的环境变量则替换。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
int dealenv(char *s)
{
int envidx = -1;
int wteidx = -1;
char sig = 0;
int len = strlen(s);
for (int i = 0; i < len; i++)
{
if (s[i] == '#')
{
s[i] = 0;
break;
}
}
len = strlen(s);
char res[36];
char temp[100];
for (int i = 0; i < len; i++)
{
if (s[i] == '$')
{
envidx = i;
}
if ((s[i] == ' ' || s[i] == '/') && envidx >= 0)
{
wteidx = i;
sig = s[i];
break;
}
}
if (envidx >= 0 && wteidx < 0)
{
wteidx = len;
}
if (envidx >= 0)
{
strcpy(temp, s + wteidx);
s[wteidx] = 0;
int r = syscall_get_var(s + envidx + 1, res);
if (r < 0)
{
if (wteidx != len)
{
s[wteidx] = sig;
}
return -1;
}
strcpy(s + envidx, res);
len = strlen(s);
strcpy(s + len, temp);
// printf("%s\n", s);
return 1;
}
return 0;
}

int denv(char *s)
{
while (dealenv(s) > 0)
;
if(dealenv(s) < 0) {
return -1;
}
return 0;
}
void runcmd(char *s)
{
int isdeal=denv(s);
if(isdeal < 0) {
exit();
}
//已有内容
}

针对环境变量的内容到此实现完毕。

2.2 declare && unset

这两个内建命令在实现了上面的东西之后变得格外简单,就是调用几个系统调用即可,拿unset举例:

1
2
3
4
5
6
7
8
9
10
if (strcmp("unset", argv[0]) == 0)
{
if (argc == 1)
{
printf("unknown error");
exit();
}
syscall_unset_var(argv[1]);
exit();
}

在这里有一个需要注意的地方是对于没有参数的declare,需要通过系统调用syscall_get_allvar来获取当前SHELL所拥有的所有环境变量的值,而这时需要传入一个char*型参数,但是由于当前操作系统并未实现malloc函数,自己实现又较为麻烦,所以最好的方法是传入一个数组,我在用户空间以全局变量定义了这个数组。但是在测试过程中,我发现当前操作系统对.bss段的内存分配是很有限的,数组一旦开大就会产生错误,因此只能酌情开一个大小适中的数组,但显然这对有很多环境变量的情况是不适用的,这也是我实现的一个缺点,但幸亏这样的实现针对评测已经够用。

2.3 注释

针对第二个测试点,只需要再完成一个注释功能即可进行测试与调试。而注释功能其实也是一个字符串处理问题,在刚刚的dealenv函数中已经处理,所以已经实现好了。

至此,第二部分全部实现完成。

第三部分

3.1 不带.b后缀命令

这个功能很好实现,只用在spawn里面给命令加上“.b”就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
if ((fd = open(prog, O_RDONLY)) < 0)
{
char buf1[MAXPATHLEN + 3] = "/";
strcpy(buf1 + 1, prog);
int len = strlen(buf1);
strcpy(buf1 + len, ".b");
fd = open(buf1, O_RDONLY);
if (fd < 0)
{
return fd;
}
}

3.2 指令自由输入和快捷键

这一部分研究的其实就是个显示问题,而且现在的SHELL其实是有自动回显功能的(别问我怎么知道的,要是你输入一个a看到终端上有两个a你也会知道),但是笨人找了很久没找到这个自动回显的功能在哪,于是只好委曲求全,根据它的自动回显来进一步调整显示。
这一部分我修改的内容全部都在readline里,首先我们需要知道相应的快捷键对应的字符序列。

快捷键 序列
up-arrow \x1b[A
down-arrow \x1b[B
left-arrow \x1b[D
right-arrow \x1b[C
backspace \b
Ctrl-E \x05
Ctrl-A \x01
Ctrl-K \x0b
Ctrl-U \x15
Ctrl-W \x17

这里我拿backspace来做示范,因为我认为这一个按键是最能让我们理解光标显示机制的。我们需要知道的是,移动光标,其实就是输出字符串,比如说让光标往左移动一位,最简单的可以直接printf("\b"),但是这个移动是不会覆盖掉字符的。还需要知道的是,目前光标在哪,我们printf的时候就会从哪里开始输出,并会覆盖掉原来已有的内容。举个简单的例子,假设最初输出了abcd,那这时光标在d之后,现在输出两次\b,光标便在c处,再输出一个e,现在看到的便是abed,光标在d处。
根据上面的例子,就能发现其实要删除一个字符并让光标正确移动,其实只需要printf("\b \b")即可,这便是我认为的实现所有快捷键的基础。下面展示backspace的具体实现,其它快捷键类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (tmp == '\b' || tmp == 0x7f)
{
int len = strlen(buf);
if (pos != 0)
{
for (int i = pos - 1; i < len; i++)
{
buf[i] = buf[i + 1];
}
pos--;
len--;
char_num--;
printf("\b \b");
printf("%s", buf + pos);
printf(" \b");
for (int i = 0; i < len - pos; i++)
{
printf("\b");
}
}
}

3.3 历史指令

要实现上下快捷键,还需要实现历史指令功能,这一部分其实也没有太大的难点,就是在每次执行指令时处理好读文件和写文件即可,同时在sh.c里面添加存储历史指令的数组,每次执行指令前把文件里存储的历史指令读到该数组中去。还需要注意的是上键当遇到不存在更往前的指令时应该停留在当前指令,同理对下键,同时下键要能回到我们最初输入的指令,比如最开始输入了a,然后又输了上键,那这时再输入下键应该回到a,这一点通过一个临时数组把最开始输入的内容存下来就好了。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
else if (tmp == 27)
{
read(0, &tmp, 1);
read(0, &tmp, 1);
//...
else if (tmp == 'A')
{
printf("%c[B", 27);
int len = strlen(buf);
for (int i = 0; i < len - pos; i++)
{
printf("%c[C", 27);
}
for (int i = 0; i < len; i++)
{
printf("\b \b");
}
strcpy(buf, history_cmd[top]);
printf("%s", buf);
char_num = pos = len = strlen(buf);
top = top - 1;
if (top < 0)
{
top += history_top;
}
sigdown = 0;
}
else if (tmp == 'B')
{
int len = strlen(buf);
for (int i = 0; i < len - pos; i++)
{
printf("%c[C", 27);
}
for (int i = 0; i < len; i++)
{
printf("\b \b");
}
if (sigdown == 0)
{
strcpy(buf, history_cmd[top]);
}
else
{
buf[0] = 0;
}
printf("%s", buf);
char_num = pos = len = strlen(buf);
top = top + 1;
if (top >= history_top)
{
top -= 1;
sigdown = 1;
}
}

至此,第三部分全部实现完成。

第四部分

4.1 一行多指令

一行多指令其实就是根据输入中的分号将整个输入分为几个指令,然后让指令顺序执行即可,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
case ';':
r = fork();
if (r == 0)
{
return argc;
}
else
{
wait(r);
*rightpipe = 0;
return parsecmd(argv, rightpipe);
}

4.2 反引号

有关反引号的实现我是在_gettoken函数中处理的,当当前字符为反引号时,遍历到下一个反引号出现的地方,从而获取到反引号内部的指令,再通过创建子进程来执行指令以及管道机制让最终执行的结果替换掉原来反引号部分的内容,实现如下:

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
else if (*s == '`')
{
char cmd[1024];
char cmdans[1024];
char after[1024];
for (int i = 1; s[i]; i++)
{
//遍历寻找反引号内部指令
}
int p[2];
pipe(p);
int r = fork();
if (r == 0)
{
dup(p[1], 1);
close(p[0]);
close(p[1]);
runcmd(cmd);
exit();
}
else
{
close(p[1]);
int len, pos = 0;
char tmp[1024];
while ((len = read(p[0], tmp, 1024)) > 0)
{
//读取执行结果
}
close(p[0]);
}
strcpy(s, cmdans);
strcpy(s + strlen(cmdans), after);
}

4.3 追加重定向

原有的SHELL已经实现了重定向的功能,追加重定向完全可以仿照重定向来实现,唯一不同的就是要从文件末尾添加。在这里我是在打开文件时添加新的模式O_APPEND,当这个模式被使用时,将相应的文件描述符的偏移设为文件大小即可。然后追加重定向就和重定向的实现几乎一样了,唯一不同的就是打开文件时使用的模式。

1
2
3
if (rq->req_omode & O_APPEND) {
ff->f_fd.fd_offset = f->f_size;
}
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
case '>':
int next = gettoken(0, &t);
if (next != 'w' && next != '>')
{
debugf("syntax error: > not followed by word\n");
exit();
}
if (next == 'w')
{
fd = open(t, O_WRONLY | O_CREAT | O_TRUNC);
if (fd < 0)
{
debugf("failed to open '%s'\n", t);
exit();
}
dup(fd, 1);
close(fd);
}
else if (next == '>')
{
if (gettoken(0, &t) != 'w')
{
debugf("syntax error: >> not followed by word\n");
exit();
}
fd = open(t, O_WRONLY | O_CREAT | O_APPEND);
if (fd < 0)
{
debugf("failed to open '%s'\n", t);
exit();
}
dup(fd, 1);
close(fd);
}

4.4 条件指令执行

这一功能应该是第四部分甚至是整个SHELL挑战性任务中我认为最困难的地方,简单想想好像很简单,通过“&&”和“||”将输入分为几个指令,然后类似一行多指令那样顺序执行,当有指令执行完没能触发执行下一条指令的条件时,便停止即可。但是里面难以实现的是如何去获取这些指令执行的结果,应该怎么知道其到底是执行成功了还是执行失败了。
解决上面的问题有很多方法,可以通过很多系统调用来比较无脑地实现,但是会相对比较麻烦,我选择了下面的实现方式。
首先给进程控制块新加属性用于存储返回值:

1
2
u_int env_return_value;
u_int env_isreceived;

然后编写一个系统调用使得能够修改进程的返回值。

1
int syscall_env_set_return_value(u_int envid,int value,int isreceived);

接下来,修改libos.c文件使得父进程能接收到子进程main的返回结果。

1
2
int r = main(argc, argv);
r=syscall_env_set_return_value(envs[ENVX(env->env_parent_id)].env_parent_id,r,1);

_gettoken函数中增加识别到“&&”和“||”的返回值,方便在parsecmd时直接使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (strchr(SYMBOLS, *s))
{
int t = *s;
if (t == '|' && (*(s+1)) == '|')
{
t = 1;
*p1 = s;
*s++ = 0;
*p2 = s;
return t;
}
else if (t == '&' && (*(s+1)) == '&')
{
t = 2;
*p1 = s;
*s++ = 0;
*p2 = s;
return t;
}
*p1 = s;
*s++ = 0;
*p2 = s;
return t;
}

parsecmd中实现条件指令执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
case 1:
//...
if (r == 0)
{
return argc;
}
else
{
//等待,当返回值非0时继续执行
}
case 2:
//...
if (r == 0)
{
return argc;
}
else
{
//等待,当返回值为0时继续执行
}

至此,第四部分全部实现完成。

心得感悟

历时一周的SHELL实现也终于是画上了句号,还正好处在烤漆期间,断断续续的,中途也少不了磕磕绊绊。说实话,我最初看到这个挑战性任务是感觉自己实现它的希望性是渺茫的。OO最后一节课上老师给我们讲这门课程培养了我们面对大型项目的勇气,但当我在面临一个操作系统时,且实现的功能没有明确指导时,我发现我还是缺乏勇气。好在有很多学长学姐的优秀博客,能让我对所要实现的功能有一个恰当的理解,并能参考他们的思路构建出我自己的实现方案。这中途遇见了好几次完全想不通的bug,因为想不通,所以根本没法修改,只好重写,换思路,最后把一个鲜活的SHELL创造了出来。完成的那一刻,我的内心是激动的,但也没那么激动,因为我觉得我所付出的努力是值得这份结果的。
最后附上一张我的SHELL图:

  • Title: BUAA操作系统lab6_challenge_shell
  • Author: Cdostan
  • Created at : 2025-06-25 21:00:00
  • Updated at : 2025-06-26 12:00:50
  • Link: https://cdostan.github.io/2025/06/25/OS/Lab6_challenge_shell/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments