实现不带 .b 后缀指令

你需要实现不带 .b 后缀的指令,但仍需兼容带有 .b 后缀的指令,如 lsls.b 都应能够正确列出当前目录下的文件。

外部指令的执行实际上是在spawn函数中打开可执行的指令文件创建新进程,并将其装载在新建子进程中,运行指令文件中的主程序。实现思路是更改文件打开程序,当文件名对应文件存在时直接打开即可,不存在时文件名末尾加.b再次尝试打开。

spawn()函数中更改文件打开方式,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//user/lib/spawn.c spawn()
if ((fd = open(prog, O_RDONLY)) < 0) //尝试打开文件
{
int len = 0;
char newProg[128];
for (len = 0; len < 128 && prog[len] != '\0'; len++)
{
newProg[len] = prog[len];
}
newProg[len++] = '.';
newProg[len++] = 'b';
newProg[len++] = '\0'; //若打开文件失败则在文件名尾加上.b并再次尝试
if ((fd = open(newProg, O_RDONLY)) < 0) //再次尝试打开文件,若失败则证明不存在该指令,返回报错信息
{
return fd;
}
}

实现指令条件执行

你需要实现 Linux shell 中的 &&||。 对于 command1 && command2command2 被执行当且仅当 command1 返回 0;对于 command1 || command2command2 被执行当且仅当 command1 返回非 0 值。

注: 评测中保证不出现括号。并且需要注意的是,在 bash 中 &&|| 的优先级相同,按照从左到右的顺序求值。

例如 cmd1 || cmd2 && cmd3,若 cmd1 返回 0,则 cmd1 执行后 cmd2 不会被执行,cmd3 会被执行;若 cmd1 返回非 0 且 cmd2 返回非 0,则 cmd3 将不会被执行。

提示:你可能需要修改 MOS 中对用户进程 exit 的实现,使其能够返回值。

cmd执行分为多层,会出现parsercmd进程,runcmd进程,而每种进程又有负责执行的子进程和整体的父进程。指令执行结束后,会返回执行结果,并退出进程。实现思路是将每个指令的执行结果发送给父进程,并在parser环节使用,总体上计算标志之前的全部指令运行结果,并判断是否执行当前指令。

include/env.h的进程结构体中加入返回值定义,具体代码如下:

1
2
3
4
struct Env {
...
int env_return_value; //子进程返回值
};

在内核态中添加子进程返回值的系统调用函数,具体代码如下:

1
2
3
4
5
6
7
//kern/syscall_all.c
int sys_return_value(int value) {
struct Env *e;
try(envid2env(curenv->env_parent_id, &e, 0));
e->env_return_value = value; //设置父进程获得的子进程返回值
return 0;
}

在用户态中添加相应的调用系统调用函数的信息发送函数,具体代码如下:

1
2
3
4
//user/lib/syscall_lib.c
int syscall_return_value(int value) {
return msyscall(SYS_return_value, value);
}

更改exit()函数,使其在退出进程的同时向父进程发送程序执行返回值,具体代码如下:

1
2
3
4
5
6
7
8
9
10
//user/lib/libos.c
void exit(int value) {
#if !defined(LAB) || LAB >= 5
close_all();
#endif
debugf("child: 0x%x send %d to parent: 0x%x\n", env->env_id, value, env->env_parent_id);
syscall_return_value(value);
syscall_env_destroy(0);
user_panic("unreachable code");
}

更改libmain()函数,使其获得程序执行结束的返回值,具体代码如下:

1
2
3
4
5
6
//user/lib/libos.c
void libmain(int argc, char **argv) {
env = &envs[ENVX(syscall_getenvid())];
int return_value = main(argc, argv); //程序执行结果
exit(return_value);
}

_gettoken()函数中增加运算符与&&和或||的解析,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//user/sh.c _gettoken()
if (*s == '&' && *(s + 1) == '&')
{
*p1 = s;
*s++ = 0;
*s++ = 0;
*p2 = s;
return 'y';
}

if (*s == '|' && *(s + 1) == '|')
{
*p1 = s;
*s++ = 0;
*s++ = 0;
*p2 = s;
return 'h';
}

更改parsercmd()函数的参数,使其可以传递上一个操作和之前全部命令执行结果,并增加与或运算的解析处理,具体代码如下:

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
//user/sh.c
int parsecmd(char **argv, int *rightpipe, int last, int flag) {
//last表示上一个运算符(0代表&,1代表|),flag代表之前所有命令执行结果运算后的结果
//解析时遇到的符号是带判断指令的后一个符号,因此需要传入上一个符号
...
}

//user/sh.c parsercmd()
case 'y':;
if (last == 0 && flag != 0)
{
return parsecmd(argv, rightpipe, 0, 1);
} //如果上一个指令是&且之前算式运算结果为false,则跳过
if (last == 1 && flag == 0)
{
return parsecmd(argv, rightpipe, 0, flag);
} //如果上一个指令是|且之前算式运算结果为true,则跳过
r = fork();
if (r == 0)
{
return argc;
}
else
{
wait(r);
if (!iscons(1))
{
dup(0, 1);
}
if (!iscons(0))
{
dup(1, 0);
}
int nextFlag = (last == 2) ? env->env_return_value :
(last == 0 && (flag == 0 && env->env_return_value == 0)) ? 0 :
(last == 1 && (flag == 0 || env->env_return_value == 0)) ? 0 : 1;
return parsecmd(argv, rightpipe, 0, nextFlag);
}
break;
case 'h':;
if (last == 0 && flag != 0)
{
return parsecmd(argv, rightpipe, 1, 1);
}
if (last == 1 && flag == 0)
{
return parsecmd(argv, rightpipe, 1, flag);
}
r = fork();
if (r == 0)
{
return argc;
}
else
{
wait(r);
if (!iscons(1))
{
dup(0, 1);
}
if (!iscons(0))
{
dup(1, 0);
}
int nextFlag = (last == 2) ? env->env_return_value :
(last == 0 && (flag == 0 && env->env_return_value == 0)) ? 0 :
(last == 1 && (flag == 0 || env->env_return_value == 0)) ? 0 : 1;
return parsecmd(argv, rightpipe, 1, nextFlag);
}
break;

更改parsercmd()函数的参数后,还应更改其余解析位置的return parsecmd(argv, rightpipe, lastOp, flag)。应注意,管道处传递时应该保持上一次的符号和之前的结果。

runcmd()函数中更改exit()的返回值以及parsercmd()的参数,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//user/sh.c runcmd()
void runcmd(char *s) {
gettoken(s, 0);
char *argv[MAXARGS];
int rightpipe = 0;
int argc = parsecmd(argv, &rightpipe, 2, 0);
if (argc == 0) {
exit(1);
}
argv[argc] = 0;
int child = spawn(argv[0], argv);
close_all();
if (child >= 0) {
wait(child);
} else {
debugf("spawn %s: %d\n", argv[0], child);
}
if (rightpipe) {
wait(rightpipe);
}
exit(env->env_return_value);
}

实现更多指令

你需要实现 touchmkdirrm 指令,只需要考虑如下情形:

  • touch:
  • touch <file>:创建空文件 file,若文件存在则放弃创建,正常退出无输出。 若创建文件的父目录不存在则输出 touch: cannot touch '<file>': No such file or directory。 例如 touch nonexistent/dir/a.txt 时应输出 touch: cannot touch 'nonexistent/dir/a.txt': No such file or directory
  • mkdir:
  • mkdir <dir>:若目录已存在则输出 mkdir: cannot create directory '<dir>': File exists,若创建目录的父目录不存在则输出 mkdir: cannot create directory '<dir>': No such file or directory,否则正常创建目录。
  • mkdir -p <dir>:当使用 -p 选项时忽略错误,若目录已存在则直接退出,若创建目录的父目录不存在则递归创建目录。
  • rm:
  • rm <file>:若文件存在则删除 <file>,否则输出 rm: cannot remove '<file>': No such file or directory
  • rm <dir>:命令行输出: rm: cannot remove '<dir>': Is a directory
  • rm -r <dir>|<file>:若文件或文件夹存在则删除,否则输出 rm: cannot remove '<dir>|<file>': No such file or directory
  • rm -rf <dir>|<file>:如果对应文件或文件夹存在则删除,否则直接退出。

文件的增删主要在内核态进行,因此构建新的serv函数,用于创建和删除文件。实现思路是在用户态创建相应fsipc函数,并使用ipcfs发送消息进行调用,在内核态创建对应服务函数,调用最底层的file相关函数实现文件的构建与删除。创建指令文件touch.cmkdir.crm.c,并实现相应程序。

fs中创建新的底层文件操作函数,具体代码如下:

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
//fs/fs.c
int file_create(char *path, struct File **file) {
char name[MAXNAMELEN];
int r;
struct File *dir, *f;
if ((r = walk_path(path, &dir, &f, name)) == 0) {
return -E_FILE_EXISTS;
}
if (r != -E_NOT_FOUND || dir == 0) {
return r;
}
if (dir_alloc_file(dir, &f) < 0) {
return r;
}
strcpy(f->f_name, name);
*file = f;
return 0;
}

int file_remove(char *path) {
int r;
struct File *f;
if ((r = walk_path(path, 0, &f, 0)) < 0) {
return r;
}
file_truncate(f, 0);
f->f_name[0] = '\0';
file_flush(f);
if (f->f_dir) {
file_flush(f->f_dir);
}
return 0;
} //remove的函数之前已经构建,可以直接使用

int file_noDir_remove(char *path) {
int r;
struct File *f;
if ((r = walk_path(path, 0, &f, 0)) < 0) {
return r;
}
if (f->f_type == FTYPE_DIR)
{
return -E_RM_DIR;
} //当待删除文件是
file_truncate(f, 0);
f->f_name[0] = '\0';
file_flush(f);
if (f->f_dir) {
file_flush(f->f_dir);
}
return 0;
} //remove的函数之前已经构建,可以直接使用,此函数的区别是不能删除文件夹

user/include/fsreq.h中构建增删文件传递信号的相应结构体,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum { //创建信号宏定义
...
FSREQ_CREATE,
FSREQ_REMOVE, //代表任意文件
FSREQ_NDREMOVE,
...
};

struct Fsreq_remove {
char req_path[MAXPATHLEN];
int rm_type;
}; //remove的结构体之前已经构建,可以直接使用

struct Fsreq_create {
char req_path[MAXPATHLEN];
uint32_t f_type; //代表文件类型,普通文件或文件夹
};

fs中创建新的服务函数,具体代码如下:

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
//fs/serv.c
void serve_create(u_int envid, struct Fsreq_create *rq) {
struct File *f;
int r;
r = file_create(rq->req_path, &f); //调用文件创造函数
if (r < 0)
{
ipc_send(envid, r, 0, 0);
return;
}
f->f_type = rq->f_type;
ipc_send(envid, 0, 0, 0);
}

void serve_remove(u_int envid, struct Fsreq_remove *rq) {
int r;
r = file_remove(rq->req_path);
ipc_send(envid, r, 0, 0);
} //remove的函数之前已经构建,可以直接使用

void serve_noDir_remove(u_int envid, struct Fsreq_remove *rq) {
int r;
r = file_noDir_remove(rq->req_path);
ipc_send(envid, r, 0, 0);
} //remove的函数之前已经构建,可以直接使用,此函数的区别是不能删除文件夹

在用户态下创建对应fsipc函数,用于ipc信号发送,具体代码如下:

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
//user/lib/fsipc.c
int fsipc_create(const char *path, uint32_t type)
{
struct Fsreq_create *req;
req = (struct Fsreq_create *)fsipcbuf;
if (strlen(path) >= MAXPATHLEN) {
return -E_BAD_PATH;
}
strcpy((char *)req->req_path, path);
req->f_type = type;
return fsipc(FSREQ_CREATE, req, 0, 0);
}

int fsipc_remove(const char *path) {
if (strlen(path) == 0 || strlen(path) > MAXPATHLEN)
{
return -E_BAD_PATH;
}
struct Fsreq_remove *req = (struct Fsreq_remove *)fsipcbuf;
strcpy((char *)req->req_path, path);
return fsipc(FSREQ_REMOVE, req, 0, 0);
}

int fsipc_noDir_remove(const char *path) {
if (strlen(path) == 0 || strlen(path) > MAXPATHLEN)
{
return -E_BAD_PATH;
}
struct Fsreq_remove *req = (struct Fsreq_remove *)fsipcbuf;
strcpy((char *)req->req_path, path);
return fsipc(FSREQ_NDREMOVE, req, 0, 0);
}

在用户态创建具体文件操作函数,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
//user/lib/file.c
int create(const char *path, uint32_t type) {
return fsipc_create(path, type);
}

int remove(const char *path) {
return fsipc_remove(path);
}

int nd_remove(const char *path) {
return fsipc_noDir_remove(path);
}

创建三个命令文件,调用文件操作函数,具体代码如下:

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
//user/touch.c
int main(int argc, char **argv) {
int r;
r = create(argv[1], FTYPE_REG);
if (r < 0 && r != -E_FILE_EXISTS)
{
printf("touch: cannot touch '%s': No such file or directory\n", argv[1]);
}
return 0;
}

//user/mkdir.c
int main(int argc, char **argv){
int r;
if (argc == 2)
{
r = create(argv[1], FTYPE_DIR);
if (r == -E_FILE_EXISTS)
{
printf("mkdir: cannot create directory '%s': File exists\n", argv[1]);
}
else if (r < 0)
{
printf("mkdir: cannot create directory '%s': No such file or directory\n", argv[1]);
}
}
else
{
char name[MAXPATHLEN] = {};
int pos = 0;
for (char *c = argv[2]; *c != '\0'; c++)
{
if (*c == '/')
{
name[pos] = '\0';
r = create(name, FTYPE_DIR);
name[pos] = '/';
pos++;
}
else
{
name[pos] = *c;
pos++;
}
}
if (name[pos] != '/')
{
name[pos] = '\0';
r = create(name, FTYPE_DIR);
}
} //-p类型迭代建立文件夹
return 0;
}

//user/rm.c
int main(int argc, char **argv)
{
int r;
if (argc == 2)
{
r = nd_remove(argv[1]);
if (r == -E_RM_DIR)
{
printf("rm: cannot remove '%s': Is a directory\n", argv[1]);
}
else if (r < 0)
{
printf("rm: cannot remove '%s': No such file or directory\n", argv[1]);
}
}
else
{
if (strcmp(argv[1], "-r") == 0)
{
r = remove(argv[2]);
if (r < 0)
{
printf("rm: cannot remove '%s': No such file or directory\n", argv[2]);
}
}
else
{
r = remove(argv[2]);
}
} //-r-rf类型,可以删除文件夹
return 0;
}

实现反引号

你需要使用反引号实现指令替换。只需要考虑 echo 进行的输出,你需要将反引号内指令执行的所有标准输出替换为 echo 的参数。

题目只需考虑echo指令的反引号输出,其等同于直接执行反引号内指令。实现思路是在指令解析环节,如果遇到反引号,则将当前解析的argv数组退格,重新解析cmd

_gettoken()函数中增加反引号的解析,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//user/sh.c _gettoken()
if (*s == '`')
{
*p1 = s;
*s++ = 0;
*p2 = s;
char *str = s;
for (; *str && *str != '`'; str++);
if (*str == '`')
{
*str = 0;
}
return '`';
}

parsercmd()函数中增加反引号的解析处理,具体代码如下:

1
2
3
4
5
6
7
8
//user/sh.c parsercmd()
case '`':
if ((gettoken(0, &t)) != 'w') {
debugf("syntax error: ` not followed by word\n");
exit(1);
} //如果反引号后不是指令,则报错并退出
argv[argc - 1] = t;
break;

实现注释功能

你需要使用 # 实现注释功能,例如 ls | cat # this is a comment meowls | cat 会被正确执行,而后面的注释则会被抛弃。

对于一行多条指令,注释符优先级最高,#之后的指令均不需要解析或执行。实现思路是在指令解析环节,如果遇到注释符号,使用fork函数创建子进程,并使用子进程返回之前解析出的命令,主进程等待子进程解析完毕后直接退出。

parsercmd()函数中增加注释符的解析处理,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
//user/sh.c parsercmd()
case '#':
r = fork();
if (r == 0) //r是子进程,则返回解析的argc
{
return argc;
}
else
{
wait(r);
exit(0);
}
break;

实现历史指令

你需要实现 shell 中保存历史指令的功能,可以通过 UpDown 选择所保存的指令并执行。你需要将历史指令保存到根目录的 .mosh_history 文件中(一条指令一行),为了评测的方便,我们设定 $HISTFILESIZE=20(bash 中默认为 500),即在 .mosh_history 中至多保存最近的 20 条指令。你还需要支持通过 history 命令输出 .mosh_history 文件中的内容。

注:在 bash 中,history 为 shell built-in command,我们规定需要将 history 实现为 built-in command。

实现历史指令可以在用户态建立数组,存储当前指令、当前指令数目、当前光标位置,在访问时进行使用。具体思路是创建相关全局变量,并在命令解析环节按照输入的方向键调整光标以及控制台输出,实现历史指令的输出与执行。

user/sh.c中定义相关变量,具体代码如下:

1
2
3
4
5
//user/sh.c
int nCmds = 0;
int lenCmds[500];
int index;
char curCmd[1024];

user/sh.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
40
41
42
43
44
45
46
47
48
49
50
//user/sh.c
void saveCmd(char *cmd) {
int r, fd;
if ((r = open("/.mosh_history", O_WRONLY | O_APPEND)) < 0)
{
if ((r = create("/.mosh_history", FTYPE_REG)) < 0)
{
user_panic("failed to create .mosh_history\n");
}
if ((r = open("/.mosh_history", O_WRONLY | O_APPEND)) < 0)
{
user_panic("failed to open .mosh_history\n");
}
} //尝试打开.mosh_history文件,如果该文件不存在则新建
fd = r;
if ((r = write(fd, cmd, strlen(cmd))) != strlen(cmd))
{
user_panic("fail to write cmd in .mosh_history\n");
}
if ((r = write(fd, "\n", 1)) != 1)
{
user_panic("fail to write \n in .mosh_history\n");
} //向.mosh_history文件写入指令并换行
lenCmds[nCmds] = strlen(cmd) + 1;
nCmds++;
close(fd);
}

void getCmd(int serial, char *cmd) {
int r, fd;
char buf[MAXFILESIZE];
if ((r = open("/.mosh_history", O_RDONLY)) < 0)
{
user_panic("failed to open .mosh_history\n");
}
fd = r;
for (int i = 0; i < serial; i++)
{
if ((r = read(fd, buf, lenCmds[i])) != lenCmds[i])
{
user_panic("failed to read line%d of .mosh_history\n", i + 1);
}
} //读取serial前的内容,摒弃
if ((r = read(fd, cmd, lenCmds[serial])) != lenCmds[serial])
{
user_panic("failed to read serial line of .mosh_history\n");
} //读取序号为serial的内容,并将其写入cmd数组中
cmd[lenCmds[serial] - 1] = 0;
close(fd);
}

在解析指令环节增加savecmd()的函数,应注意全局变量的使用要求处于同一进程内,因此需要将其放置在主解析进程中,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
//user/sh.c
int main(int argc, char **argv) {
...
for (;;) {
...
readline(buf, sizeof buf);
if (buf[0] != '\0') saveCmd(buf);
...
}
...
}

在解析一行指令环节添加对于上下键带来的光标以及控制台显示操作,具体代码如下:

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
//user/sh.c
void readline(char *buf, u_int n) {
int r, lastLen;
index = nCmds; //保证每次输入指令时索引都指向当前指令
for (int i = 0; i < n; i++) {
...
if (buf[i] == '\b' || buf[i] == 0x7f) {
...
} else if (buf[i] == '\033') {
switch (getDirect())
{
case 'A':
printf("\033[1B"); //向下进行一格以抵消向上
if (index == nCmds)
{
strcpy(curCmd, buf); //记录输入的最后一条指令
}
if (index > 0)
{
index--;
printf("\033[2K"); //删除当前本行的输出显示
printf("\033[%dD", lastLen); //移动光标
getCmd(index, buf);
printf("%s", buf);
lastLen = strlen(buf);
}
break;
case 'B':
printf("\033[1A");
if (index < nCmds)
{
index++;
if (index == nCmds)
{
strcpy(buf, curCmd);
}
else
{
getCmd(index, buf);
}
printf("\033[2K");
printf("\033[%dD", lastLen);
printf("%s",buf);
lastLen = strlen(buf);
}
else
{
strcpy(buf, curCmd);
printf("\033[2K");
printf("\033[%dD", lastLen);
printf("%s",buf);
lastLen = strlen(buf);
}
break;
}
}
...
}
...
}

int getDirect() {
char temp1, temp2;
read(0, &temp1, 1);
read(0, &temp2, 1);
if (temp1 == '[') return temp2; //\033[A表示上键,\033[B表示下键
return -1;
}

runcmd()函数中添加内置指令history,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//user/sh.c runcmd()
if (strcmp(argv[0], "history") == 0)
{
int r, fd;
char buf;
if ((r = open("/.mosh_history", O_RDONLY)) < 0)
{
user_panic("fail to open file : .mosh_history\n");
}
fd = r;
while ((r = read(fd, &buf, 1)) == 1)
{
printf("%c", buf);
}
return;
}

实现一行多指令

你需要实现使用 ; 将多条指令隔开从而从左至右依顺序执行每条指令的功能。

对于一行多指令,在解析过程中,应该fork出多个子进程,并行解析处理指令,并返回给runcmd进程,进行执行操作。实现思路是在指令解析环节,如果遇到;,使用fork函数创建子进程,并使用子进程返回之前解析出的命令,主进程等待子进程解析完毕后继续执行解析操作,直至结束。

parsercmd()函数中增加多指令标识符;的解析处理,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//user/sh.c parsercmd()
case ';':
r = fork();
if (r == 0)
{
return argc;
}
else
{
wait(r);
if (!iscons(1))
{
dup(0, 1);
}
if (!iscons(0))
{
dup(1, 0);
} //在实现过程中,发现遇到重定向会产生输入位置混乱的现象,因此调整device的输入位置
return parsecmd(argv, rightpipe, 2, 0);
}
break;

实现追加重定向

你需要实现 shell 中 >> 追加重定向的功能,即在文件末尾进行追加输入。

追加重定向的实现与重定向基本一致,只需要更改文件的打开权限,定义追加权限即可。实现思路是新定义追加权限,在重定向打开文件时给予追加权限,以使其可以在原文件内容上进行追加输入。

serve_open()函数中,增加追加权限打开操作,具体代码如下:

1
2
3
4
5
//fs/serv.c serve_open()
if((rq->req_omode & O_APPEND) == O_APPEND)
{
ff->f_fd.fd_offset = ff->f_file.f_size; //当打开方式是扩展时修改偏移量
}

_gettoken()函数中增加追加重定向的解析,具体代码如下:

1
2
3
4
5
6
7
8
9
//user/sh.c _gettoken()
if (*s == '>' && *(s + 1) == '>')
{
*p1 = s;
*s++ = 0;
*s++ = 0;
*p2 = s;
return 'd';
}

parsercmd()函数中增加追加重定向的解析处理,具体代码如下:

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
//user/sh.c parsercmd()
case 'd':
if ((gettoken(0, &t)) != 'w') {
debugf("syntax error: >> not followed by word\n");
exit(1);
}
if ((r = open(t, O_RDWR | O_APPEND)) < 0)
{
if ((r = create(t, FTYPE_REG)) < 0)
{
debugf("failed to create %s in >>\n", t);
exit(1);
}
if ((r = open(t, O_RDWR | O_APPEND)) < 0)
{
debugf("failed to open %s in >>\n", t);
exit(1);
}
}
fd = r;
dup(fd, 1);
if ((r = close(fd)) < 0)
{
return r;
}
break;

实现引号支持

你需要实现引号支持,比如 echo "ls >",shell 在解析时需要将双引号内的内容看作是单个字符串。

在解析环节中,数组argv中存储的是各个w,如在echo aaa中就会有两个w: "echo" "aaa"。将双引号及其内部内容解析为一个w,即可将其看作单个字符串。实现思路是在解析过程中,遇到双引号时,开始存储,直到遇到下一个双引号,并将之中的内容当作一个整体存储到argv数组中,供运行阶段使用。

_gettoken()函数中增加双引号的解析,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//user/sh.c _gettoken()
if (*s == '\"')
{
s++;
*p1 = s;
while (*s && *s != '\"')
{
s++;
}
*s = 0;
s++;
*p2 = s; //需注意,p2代表下一个token的起始位置
return 'w';
}

实现前后台任务管理

  • 你需要支持 mosh 运行后台进程,当命令的末尾添加上 & 符号时,该命令应该在后台执行。
  • 实现 jobs 指令列出当前 shell 中所有后台任务的状态。你需要为任务创建 ID(每次启动 mosh 时,任务从 1 开始编号,每个新增任务编号应加 1),并且通过 jobs 指令输出包括:任务 ID(job_id)、任务的运行状态(status:可能的取值为 RunningDone)、任务的进程 ID(env_id)与运行任务时输入的指令(cmd)。请以 printf("[%d] %-10s 0x%08x %s", job_id, status, env_id, cmd) 的格式进行输出。
  • 实现 fg 将后台任务带回前台继续运行,用户通过 fg <job_id> 的方式将对应任务带回前台。
  • 实现 kill 指令,用户通过 kill <job_id> 来实现结束后台任务。

fgkill 指令中,若 job_id 对应的后台任务不存在则输出 printf("fg/kill: job (%d) do not exist\n", job_id),若 job_id 对应的 ID 为 envid 的进程状态不为 Running 则输出 printf("fg/kill: (0x%08x) not running\n", envid)

前后台任务管理主要是在内核态中进行,实现jobs指令时获取全部任务信息,实现fg指令时将任务调至前台等待其完成,实现kill指令时将任务进程结束。实现思路是在内核态中建立结构体数组,并创建系统调用函数,实现任务的管理与展示。

include/env.h中定义任务结构体,具体代码如下:

1
2
3
4
5
6
7
//include/env.h
struct Job {
int job_id;
int env_id;
char status[10];
char cmd[1024];
};

kern/syscall_all.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
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
//kern/syscall_all.c
int sys_job_list(struct Job list[], int *num) {
*num = nJobs;
for (int i = 0; i < nJobs; i++)
{
list[i] = jobs[i];
}
return 0;
}

int sys_job_begin(u_int env_id, char cmd[]) {
jobs[nJobs].job_id = nJobs + 1;
jobs[nJobs].env_id = env_id;
strcpy(jobs[nJobs].status, "Running");
strcpy(jobs[nJobs].cmd, cmd);
nJobs++;
return 0;
}

int sys_job_end(u_int env_id) {
for (int i = 0; i < nJobs; i++)
{
if (jobs[i].env_id == env_id)
{
strcpy(jobs[i].status, "Done");
break;
}
}
return 0;
}

int sys_job_turn(int job_id, int *env_id) {
if (job_id < 0 || job_id > nJobs)
{
return -E_NO_JOB;
} //新定义错误没有该任务
int curenv_id = jobs[job_id - 1].env_id;
*env_id = curenv_id;
if (strcmp(jobs[job_id - 1].status, "Running") != 0)
{
return -E_NO_RUNJOB;
} //新定义错误该任务不处于运行状态
struct Env *e;
try(envid2env(curenv_id, &e, 0));
return 0;
}

int sys_job_kill(int job_id, int *env_id) {
if (job_id < 0 || job_id > nJobs)
{
return -E_NO_JOB;
}
int curenv_id = jobs[job_id - 1].env_id;
*env_id = curenv_id;
if (strcmp(jobs[job_id - 1].status, "Running") != 0)
{
return -E_NO_RUNJOB;
}
strcpy(jobs[job_id - 1].status, "Done");
struct Env *e;
try(envid2env(curenv_id, &e, 0));
printk("[%08x] destroying %08x\n", curenv->env_id, e->env_id);
env_destroy(e);
return 0;
}

int sys_cgetc(void) {
int ch;
ch = scancharc();
return ch;
} //需要调整原本的cgetc函数,去除无输入下的忙等待

在用户态中添加相应的调用系统调用函数的信息发送函数,具体代码如下:

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
//kern/syscall_lib.c
int syscall_job_list(struct Job list[], int *num) {
return msyscall(SYS_job_list, list, num);
}

int syscall_job_begin(u_int env_id, char cmd[]) {
return msyscall(SYS_job_begin, env_id, cmd);
}

int syscall_job_end(u_int env_id) {
return msyscall(SYS_job_end, env_id);
}

int syscall_job_turn(int job_id, int *env_id) {
return msyscall(SYS_job_turn, job_id, env_id);
}

int syscall_job_kill(int job_id, int *env_id) {
return msyscall(SYS_job_kill, job_id, env_id);
}

int syscall_cgetc() {
int ch;
while ((ch = msyscall(SYS_cgetc)) == 0) {
syscall_yield();
}
return ch;
} //需要调整原本的cgetc函数,去除无输入下的忙等待

parsercmd()函数中增加后台任务的解析处理,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//user/sh.c parsercmd()
case '&':
r = fork();
if (r == 0)
{
if ((last == 0 && flag != 0) || (last == 1 && flag == 0))
{
return 0;
}
return argc;
}
else
{
if (!iscons(1))
{
dup(0, 1);
}
if (!iscons(0))
{
dup(1, 0);
}
return parsecmd(argv, rightpipe, 2, 0);
} //父进程不需要等待执行解析的子进程
break;

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
//user/sh.c runcmd()
if ((strcmp(argv[0], "jobs") == 0)
|| (strcmp(argv[0], "fg") == 0)
|| (strcmp(argv[0], "kill") == 0))
{
int r, env_id, num;
if (strcmp(argv[0], "jobs") == 0)
{
struct Job list[1024];
syscall_job_list(list, &num);
for (int i = 0; i < num; i++)
{
printf("[%d] %-10s 0x%08x %s\n", list[i].job_id, list[i].status, list[i].env_id, list[i].cmd);
}
}
else if (strcmp(argv[0], "fg") == 0)
{
int job_id = turn_to_num(argv[1]);
r = syscall_job_turn(job_id, &env_id);
wait(env_id);
if (r == -E_NO_JOB)
{
printf("fg: job (%d) do not exist\n", job_id);
}
else if (r == -E_NO_RUNJOB)
{
printf("fg: (0x%08x) not running\n", env_id);
}
}
else if (strcmp(argv[0], "kill") == 0)
{
int job_id = turn_to_num(argv[1]);
r = syscall_job_kill(job_id, &env_id);
if (r == -E_NO_JOB)
{
printf("kill: job (%d) do not exist\n", job_id);
}
else if (r == -E_NO_RUNJOB)
{
printf("kill: (0x%08x) not running\n", env_id);
}
}
return;
}

为每条指令添加起始的任务初始化以及任务结束,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//user/sh.c
void runcmd(char *s) {
...
if (child >= 0) {
syscall_job_begin(child, cmd); //应确保spawn出的子进程正确后进行任务初始化
wait(child);
} else {
debugf("spawn %s: %d\n", argv[0], child);
}
...
}

//user/lib/libos.c
void libmain(int argc, char **argv) {
...
syscall_job_end(env->env_id);
...
}