Shell挑战性任务

任务要求

本次挑战性任务是在 lab6 实现的 MOS Shell 基础上继续实现新的功能。

实现功能

具体内容参考任务说明。(各测试点间无依赖关系)

关于shell

在 MOS 中,进程是一个很重要的概念,我们需要清楚 MOS 在运行的过程中存在哪些进程、这些进程之间的关系是什么,这有助于我们完成 shell 的相关任务。

比如,当我们启动 shell 时,它的 main 函数构成了一个新的进程。这个主进程主要负责完成命令的读取(readline)等工作(这个进程从 mosh 被打开到结束也都是不会被 free 的),不断有新的进程被 fork 出来通过调用runcmd去执行命令。在runcmd中,则首先调用parsecmd解析命令的参数。

parsecmd通过返回argc表明完成对命令和参数的解析,或者递归调用parsecmd进行更复杂的解析工作。在这个过程中,主要通过gettoken_gettoken对字符串命令进行拆解。

解析完成后,又会调用spawn函数加载对应的可执行文件、fork 新的进程并设置跳转入口,以执行对应的命令。spawn会返回这个进程的 id ,使父进程得以等待(或根据要实现的功能进程不同的处理)子进程完成可执行文件的运行(所有会结束的进程最后会调用exit()),然后父进程也就将结束它的使命。

以上是 mosh 运行的大致逻辑,对这些有了基本的了解后,让我们着手开始增强 mosh 的功能吧。

具体实现

不带.b后缀指令

只需在spawn加载可执行文件时进行额外判断即可,若可执行文件不存在,则添加.b后缀再次尝试

1
2
3
4
5
6
7
8
9
10
int fd;
char cmd[1024] = {0};
if ((fd = open(prog, O_RDONLY)) < 0) {
strcpy(cmd, prog);
strcat(cmd, ".b\0"); // lib/string.c 中实现
//debugf("cmd: %s\n", cmd);
if ((fd = open(cmd, O_RDONLY)) < 0) {
return fd;
}
}

指令条件执行

显然,这首先需要我们修改 MOS 中对用户进程exit的实现,使其能够返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
void exit(int r) {
// After fs is ready (lab5), all our open files should be closed before dying.
#if !defined(LAB) || LAB >= 5
close_all();
#endif
env = &envs[ENVX(syscall_getenvid())];
if (envs[ENVX(env->env_parent_id)].env_ipc_recving != 0) {
//debugf("%d should send: %d\n", env->env_id, r);
ipc_send(env->env_parent_id, r, 0, 0);
}
syscall_env_destroy(0);
user_panic("unreachable code");
}

这里我采用了 ipc 来实现返回值的传递,当父进程需要返回值时,让子进程调用ipc_send

以条件||为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
case -2: // Or
if ((*rightpipe = fork()) == 0) {
return argc; // 子进程执行左指令
} else {
u_int who;
r = ipc_recv(&who, 0, 0);
//debugf("%d %d %d\n", *rightpipe, who, r);
if (r == 0) { // 若左为真,则跳过之后的指令,直到遇到注释'#',或者遇到条件'&&'能让它停下来
do {
c = gettoken(0, &t);
} while (c != -1 && c != '#' && c);
if (c != -1) {
return 0;
}
}
return parsecmd(argv, rightpipe, background); // 可以继续解析
}
break;

另外由于可执行文件执行之后的返回值不能直接传给上述父进程,所以可以修改wait函数,让其将值先返回给子进程,子进程再将此值返回

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
// wait.c
int wait(u_int envid) {
// 注意我们这里再 wait 中使用了 ipc,之后在其他地方再想使用这个 wait 时需要注意 ipc 是否会产生影响
const volatile struct Env *e;
int r;
u_int who;
e = &envs[ENVX(envid)];
// debugf("%08x waiting %08x\n", syscall_getenvid(), envid);
while (e->env_id == envid && e->env_status != ENV_FREE) {
r = ipc_recv(&who, 0, 0);
syscall_yield();
}
return r;
}

// sh.c
void runcmd(char *s) {
// ...
int child = spawn(argv[0], argv);
if (child >= 0) {
r = wait(child);
}
// ...
exit(r);
}

更多指令

需要实现touchmkdirrm,并需要考虑给出的情形

首先我们需要实现 MOS 的创建文件的用户接口(文件系统的接口已给出),新增以下内容

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
// fsreq.h
struct Fsreq_create {
char req_path[MAXPATHLEN];
u_int f_type;
};

// lib.h
int fsipc_create(const char *, int);
int create(const char *path, u_int f_type);

// serv.h
int file_create(char *path, struct File **file);

// file.c
int create(const char *path, u_int f_type) {
return fsipc_create(path, f_type);
}

// fsipc.c
int fsipc_create(const char *path, int f_type) {
int len = strlen(path);
if (len == 0 || len > MAXPATHLEN) {
return -E_BAD_PATH;
}

struct Fsreq_create *req = (struct Fsreq_create *) fsipcbuf;
req->f_type = f_type;
strcpy((char *) req->req_path, path);

return fsipc(FSREQ_CREATE, req, 0, 0);
}

// serv.c
void *serve_table[MAX_FSREQNO] = {
..., [FSREQ_CREATE] = serve_create,
};

void serve_create(u_int envid, struct Fsreq_create *rq) {
struct File *f;
int r;

if ((r = file_create(rq->req_path, &f)) < 0) {
ipc_send(envid, r, 0, 0);
return;
}

f->f_type = rq->f_type;
ipc_send(envid, 0, 0, 0);
}

然后,对于普通的touchmkdir命令,我们只需要在对应*.c文件中进行以下操作即可

1
int r = create(argv[1], FTYPE_REG); // 或修改类型为 FTYPE_DIR 以创建文件目录

对于mkdir,我们需要额外实现-p参数:当使用 -p 选项时忽略错误,若目录已存在则直接退出,若创建目录的父目录不存在则递归创建目录。

首先我们可以借助 ARG 宏来解析参数

1
2
3
4
5
6
7
8
9
10
ARGBEGIN {
// 以此类推
case 'p':
pmod = 1;
break;
default:
usage();
break;
} ARGEND
// 要注意解析完成后从 argv[0] 开始就是命令的执行参数

对于-p参数,我的处理办法为,对路径进行解析,忽视错误对路径中每一个子目录进行创建

1
2
3
4
5
6
7
8
9
10
11
12
13
char *path = argv[0];
if (*path == '/') {
path++;
}
int len = strlen(path);
*(path + len) = '/'; // 方便创建最后一个目录
for (int i = 0; i <= len; ++i) {
if (*(path + i) == '/') {
*(path + i) = 0;
create(path, FTYPE_DIR);
*(path + i) = '/';
}
}

对于rm指令,用户接口remove已给出,直接使用即可

不过我们需要处理不同的情况

  • rm <file>:若文件存在则删除 <file>,否则输出错误信息。
  • rm <dir>:输出错误信息。
  • rm -r <dir>|<file>:若文件或文件夹存在则删除,否则输出错误信息。
  • rm -rf <dir>|<file>:如果对应文件或文件夹存在则删除,否则直接退出。

因此我们需要先获取路径对应的文件描述符,然后针对不同情况进行不同的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int fd = open(argv[0], O_RDONLY); // 只读
if (rmod && fmod) {
remove(argv[0]);
} else {
if (fd < 0) {
printf("rm: cannot remove \'%s\': No such file or directory\n", argv[0]);
} else {
struct Filefd *filefd = (struct Filefd *) INDEX2FD(fd);
// 获取文件描述符
if (!rmod && filefd->f_file.f_type == FTYPE_DIR) {
printf("rm: cannot remove \'%s\': Is a directory\n", argv[0]);
} else {
remove(argv[0]);
}
}
}

反引号

本次任务只需考虑echo进行的输出且数据较弱,故忽略echo指令和反引号,直接执行反引号中的指令貌似也能过。不过这里还是对反引号原有的要求进行了实现。

首先我们在SYMBOLS里面添加反引号以完成识别

1
#define SYMBOLS "<|>&;()`#"

这里我采用了如下的方式区分前后的反引号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int backquote;
int _gettoken(char *s, char **p1, char **p2) {
// ...
if (strchr(SYMBOLS, *s)) {
int t = *s;
*p1 = s;
*s++ = 0;
*p2 = s;
if (t == '`') {
backquote ^= 1;
}
return t;
}
// ...
}

然后使用管道将反引号中指令的所有输出传给父进程,对其解析以作为使用反引号的指令的参数

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
case '`':
if (backquote) {
if ((r = pipe(p)) < 0) {
debugf("failed to allocate a pipe: %d\n", r);
exit(0);
}
if ((*rightpipe = fork()) == 0) {
// fork 子进程指令解析反引号中内容
// 并将标准输入端与管道写端相连
dup(p[1], 1);
close(p[1]);
close(p[0]);
return parsecmd(argv, rightpipe, background);
} else {
// 将标准输入端与管道读端相连,使用 read 读取信息
dup(p[0], 0);
close(p[0]);
close(p[1]);
char *tmp = (char *) bf;
for (int i = 0, count = 0; i < MAXLEN; i++) {
count += read(0, bf + i, 1);
if (count == i) break;
syscall_yield();
}
while (strchr("\t\r\n", *tmp)) {
*tmp++ = 0;
}
// 以换行符进行分割
while (*tmp) {
char *s1 = tmp;
while (!strchr("\t\r\n", *tmp) && *tmp) {
tmp++;
}
*tmp++ = 0;
argv[argc++] = s1;
while (strchr("\t\r\n", *tmp) && *tmp) {
tmp++;
}
}
do {
c = gettoken(0, &t);
// 这里父进程需要跳过直到右引号的所有内容
} while (c != '`');
}
} else {
return argc;
// 子进程完成了对反引号中内容的解析,返回执行
}
break;

这里最开始我使用原始的wait但是没有成功,因此使用了类似于轮询的方式完成了对数据的读取

不过后来我需要在wait.c中添加新的功能,所以上面比较丑陋的轮询方式可以简化为下面的函数调用

1
2
3
4
5
6
7
8
// wait.c
void check2(u_int envid) {
const volatile struct Env *e;
e = &envs[ENVX(envid)];
while (e->env_id == envid && e->env_status != ENV_FREE) {
syscall_yield();
}
}

注释功能

这个比较简单,完成对#的识别后进行如下处理即可

1
2
3
case 0:
case '#':
return argc;

一行多指令

常规的进程 fork

需要注意的点是可能之前会有重定向,将输出指向了其他地方,这里需要将其重新指回控制台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case ';':
if ((*rightpipe = fork()) == 0) {
return argc;
} else {
wait(*rightpipe);
if (redirect) {
close(0);
close(1);
dup(opencons(), 1);
dup(1, 0);
redirect = 0;
}
return parsecmd(argv, rightpipe, background);
}
break;

追加重定向

首先我们需要实现文件操作的O_APPEND模式,即追加写功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// lib.h
#define O_APPEND 0x1000

// serv.c
void serve_open(u_int envid, struct Fsreq_open *rq) {
// ...
o->o_mode = rq->req_omode;
ff->f_fd.fd_omode = o->o_mode;

if (ff->f_fd.fd_omode & O_APPEND) {
ff->f_fd.fd_offset = f->f_size;
}
// ...
}

然后在parsecmd中进行相应的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
case '>':; // 这个分号是因为 C 语言不允许在 case 后面直接定义变量
int cc = gettoken(0, &t);
int mode = O_WRONLY | O_CREAT; // 没有则创建
if (cc == '>') { // 判断是否是追加重定向
mode |= O_APPEND;
if ((cc = gettoken(0, &t)) != 'w') {
debugf("syntax error: > not followed by word\n");
exit(0);
}
} else if (cc == 'w') {
mode |= O_TRUNC; // 普通重定向需先清空文件内容
} else {
debugf("syntax error: > not followed by word\n");
exit(0);
}
if ((r = open(t, mode)) < 0) {
debugf("failed to open \'%s\': %d\n", t, r);
exit(0);
}
fd = r;
dup(fd, 1); // 标准输出端与对应文件符相连
close(fd);
break;

引号支持

由于不用考虑引号和反引号的嵌套处理,所以也就相对简单,只需在_gettoken时将引号内所用内容标记为一个wordw

1
2
3
4
5
6
7
8
9
10
11
int _gettoken(char *s, char **p1, char **p2) {
// ...
if (*s == '\"') { // 这个判断需要在其他所有情况的前面
*p1 = ++s;
while (*s != '\"' && *s++);
*s++ = 0;
*p2 = s;
return 'w';
}
// ...
}

历史指令

重量级

首先本着要做就要做好的原则,我先实现了对键入命令任意位置的修改的支持(使用,,Backspace,Del),这样不仅看起来更加顺眼,还可以实现对历史指令的修改

查阅资料可知,我们需要判断的 ascii 为

编码
\033[D
\033[C
\033[A
\033[B
Backspace \b or 0x7f
Del ~

通过以下宏我们可以实现光标的移动,从而能够通过输出新的内容覆盖指定位置的原有内容

1
2
3
4
#define MoveLeft(x) debugf("\033[%dD", x)
#define MoveRight(x) debugf("\033[%dC", x)
#define MoveUp(x) debugf("\033[%dA", x)
#define Movedown(x) debugf("\033[%dB", x)

然后修改readline的逻辑,维护当前指令的长度len和光标位置i(表示在第i个字符前),以及历史指令的读取

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
void readline(char *buf, u_int n) {
int r, len = 0, i = 0;
char tmp;
while (i < n) {
if ((r = read(0, &tmp, 1)) != 1) {
if (r < 0) {
debugf("read error: %d\n", r);
}
exit(0);
}
if (删除字符) {
// ...
} else if (方向键) {
// ...
} else if (tmp == '\r' || tmp == '\n') { // 结束
buf[len] = 0;
return;
} else { // 普通字符
if (i == len) {
buf[i++] = tmp;
} else {
for (int j = len; j > i; j--) {
buf[j] = buf[j - 1];
}
buf[i] = tmp;
buf[len + 1] = 0;
MoveLeft(++i); // 光标移动至开头
debugf("%s", buf); // 显示新的内容
MoveLeft(len - i + 1); // 将光标移动至原来的位置
}
len += 1;
}
}
// ...
}

进一步,对于退格键Backspace和删除键Del的处理,主要思路是用空格覆盖想要删除的字符(以实现删除效果),对于光标位置的计算没什么好说的,细心即可

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
if (tmp == '\b' || tmp == 0x7f) {
if (i > 0) {
if (i == len) {
buf[--i] = 0;
MoveLeft(1);
debugf(" ");
MoveLeft(1);
} else {
for (int j = i - 1; j < len - 1; ++j) {
buf[j] = buf[j + 1];
}
buf[len - 1] = 0;
MoveLeft(i--);
debugf("%s ", buf);
MoveLeft(len - i);
}
len -= 1;
}
} else if (tmp == '~') {
if (i < len) {
for (int j = i; j < len; ++j) {
buf[j] = buf[j + 1];
}
buf[--len] = 0;
if (i != 0) {
MoveLeft(i);
}
debugf("%s ", buf);
MoveLeft(len - i + 1);
}
}

对于方向键的处理,左移右移比较直接,只需要注意不要移出范围

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
} else if (tmp == '\033') {
read(0, &tmp, 1);
if (tmp == '[') {
read(0, &tmp, 1);
switch (tmp) {
case 'A':
// ...
break;
case 'B':
// ...
break;
case 'C':
if (i < len) {
i += 1;
} else {
MoveLeft(1);
}
break;
case 'D':
if (i > 0) {
i -= 1;
} else {
MoveRight(1);
}
break;
default:
break;
}
}
}

对于上下键的处理,我们先不考虑历史指令如何保存和读取,而是关注如何实现上下键的逻辑功能
这里我采用了一个全局变量hislen来记录历史指令的数量、一个局部变量hisline来记录当前是哪条指令([0,hislen - 1]表示历史指令),histmp用来保存当前输入的指令(因为可能会上键选取历史指令、最后又下键回到当前指令)

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
void deleteLine(int pos, int len) { // 光标位置和buf长度
if (pos != 0) {
MoveLeft(pos);
}
for (int k = 0; k < len; ++k) {
debugf(" ");
}
if (len != 0) {
MoveLeft(len);
}
}
void readline(char *buf, u_int n) {
int hisline = hislen;
// ...
while (i < n) {
// ...
} else if (tmp == '\033') {
read(0, &tmp, 1);
if (tmp == '[') {
read(0, &tmp, 1);
switch (tmp) {
case 'A':
Movedown(1); // 将光标移回来
if (hisline == hislen) {
// 保存一下当前输入的指令
strcpy(histmp, buf);
*(histmp + len) = 0;
}
if (hisline > 0) {
hisline--;
readHistory(hisline, buf);
// 读取第 hisline 条历史指令
deleteLine(i, len);
// 清除当前行的所有内容
debugf("%s", buf);
len = strlen(buf);
i = len;
}
break;
case 'B':
//MoveUp(1); 往下则不用移动
if (hisline < hislen - 1) {
hisline++;
readHistory(hisline, buf);
} else if (hisline + 1 == hislen) {
hisline++;
strcpy(buf, histmp);
// 回到当前指令
}
deleteLine(i, len);
debugf("%s", buf);
len = strlen(buf);
i = len;
break;
// ...
}
}
}
// ...
}

现在我们可以开始尝试实现历史指令的功能了
首先是初始化创建/.mosh_history文件

1
2
3
4
5
6
7
if ((r = open("/.mosh_history", O_RDONLY)) < 0) {
if (create("/.mosh_history", FTYPE_REG) < 0) {
user_panic("create failed");
}
} else {
close(r);
}

对于如何从文件中读取历史指令,我采用了一个数组hisoffset来记录每条指令的偏移,这样我们就可以通过seekread来读取指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int hislen, hisoffset[HISTFILESIZE + 5];
// hisoffset[i] -> 前 (i+1) 条指令的长度之和 + (i+1) 个换行符
void readHistory(int line, char *buf) {
int r, fd;
if ((fd = open("/.mosh_history", O_RDONLY)) < 0) {
user_panic("open failed");
}
if (line >= hislen) {
*buf = 0;
return;
}
int offset = (line > 0 ? hisoffset[line - 1] : 0);
int len = (line > 0 ? hisoffset[line] - hisoffset[line - 1] : hisoffset[line]);
if ((r = seek(fd, offset)) < 0) {
user_panic("seek failed");
}
if ((r = read(fd, buf, len)) != len) {
user_panic("read failed");
}
close(fd);
buf[len - 1] = 0;
}

读取指令需要注意的是最大指令条数为HISTFILESIZE=20,当超过时需要将最早的指令删除。这里我采用的是将后19条指令读出,清空文件,再和新指令一起写入的简单粗暴的方式。

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
void saveHistory(char *buf) {
int r, fd;
if (hislen < HISTFILESIZE) { // 未达上限
if ((fd = open("/.mosh_history", O_WRONLY | O_APPEND)) < 0) { // 追加写
user_panic("open failed");
}
int len = strlen(buf);
*(buf + len) = '\n'; // 加换行符
len += 1;
*(buf + len) = 0;
if ((r = write(fd, buf, len)) != len) {
user_panic("write error");
}
hisoffset[hislen++] = len + (hislen > 0 ? hisoffset[hislen - 1] : 0);
close(fd);
} else { // 已满
if ((fd = open("/.mosh_history", O_RDONLY)) < 0) {
user_panic("open failed");
}
if ((r = seek(fd, hisoffset[0])) < 0) {
user_panic("seek failed"); // 从第 1 条指令开始读
}
int len = hisoffset[HISTFILESIZE - 1] - hisoffset[0];
// 读出除第 0 条指令外的所有指令
if ((r = read(fd, buftmp, len)) != len) {
user_panic("read error");
}
if ((fd = open("/.mosh_history", O_TRUNC | O_WRONLY)) < 0) { // 清空
user_panic("trunc failed");
}
if ((r = write(fd, buftmp, len)) != len) {
debugf("%s\n", buftmp);
debugf("%d %d\n", len, r);
user_panic("rewrite error");
}
if ((fd = open("/.mosh_history", O_WRONLY | O_APPEND)) < 0) {
user_panic("rewrite not correctly");
}
len = strlen(buf);
*(buf + len) = '\n';
len += 1;
*(buf + len) = 0;
if ((r = write(fd, buf, len)) != len) {
user_panic("write error");
}
int ttmp = hisoffset[0]; // 重新计算所有偏移
for (int i = 0; i < HISTFILESIZE; i++) {
hisoffset[i] = hisoffset[i + 1] - ttmp;
}
hisoffset[hislen - 1] = hisoffset[hislen - 2] + len;
close(fd);
}
}

对于history指令,直接将其变为cat /.mosh_history即可

1
2
3
4
5
6
7
8
9
int runcmd(char *s) {
// ...
if (!strcmp(argv[0], "history") || !strcmp(argv[0], "history.b")) {
argv[0] = "cat";
argv[1] = ".mosh_history";
argc = 2;
}
// ...
}

前后台任务管理

  • 当命令的末尾添加上 & 符号时,该命令应该在后台执行
  • 实现 jobs 指令列出当前 shell 中所有后台任务的状态。需要为任务创建 ID(每次启动 mosh 时,任务从 1 开始编号,每个新增任务编号应加 1),并且通过 jobs 指令输出包括:任务 ID(job_id)、任务的运行状态(status:可能的取值为 Running,Done)、任务的进程 ID(env_id)与运行任务时输入的指令(cmd)。
  • 实现 fg 将后台任务带回前台继续运行,用户通过 fg <job_id> 的方式将对应任务带回前台。
  • 实现 kill 指令,用户通过 kill <job_id> 来实现结束后台任务。
  • 以上指令均需要设计为内置指令(与history一样)

坑点很多,相比于历史指令,这部分更是在 bug 里找 bug

总之这里收回前文对进程之间关系的强调伏笔,需要注意的是一定要在mosh主进程中读写 jobs 的相关信息(子进程被 free 信息就没了),然后对于 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
33
char cmd[1024];
int jobscount;
struct Job {
int status;
int env_id;
char cmd[1024];
} jobs[20];

int savetask(char *s) {
strcpy(cmd, s);
int len = strlen(s);
while (s[len - 1] != '&' && len > 1) {
len--;
}
if (s[len - 1] == '&') {
strcpy(jobs[++jobscount].cmd, cmd);
jobs[jobscount].status = 1;
return 1;
}
return 0;
}

int main(int argc, char **argv) {
// ...
for (;;) {
// ...
readline(buf, sizeof buf);
int rr = savetask(buf);
// ...
}
// ...
return 0;
}

接着需要使用 ipc 去拿到对应的进程 id
先将runcmd改为有返回值,其中与 jobs 相关的操作返回非 0 值,否则返回 0,然后通过类似之前处理条件操作的方法将值传回 mosh 主进程

1
2
3
4
5
6
int ret = 0;
if (r == 0) {
exit(runcmd(buf));
} else {
ret = wait(r);
}

parsecmd中解析命令时需要检测是否有&符号,如果有则将参数*background置为 1,并在runcmd中返回执行该命令的进程 id ,然后记录

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
int parsecmd(char **argv, int *rightpipe, int *background) {
// ...
case '&':
*background = 1;
break;
// ...
}

int runcmd(char *s) {
// ...
int child = spawn(argv[0], argv);
close_all();
if (child >= 0) {
if (background) {
r = 0; // 不等待,即意味着在后台挂起
} else {
r = wait(child);
}
} else {
debugf("spawn %s: %d\n", argv[0], child);
}
if (background) {
return child;
}
// ...
}

int main(int argc, char **argv) {
// ...
if (ret) {
if (rr) {
jobs[jobscount].env_id = ret;
}
}
// ...
}

对于fg指令,为了避免使用 ipc 时出现的各种玄学错误,最后选择了轮询等待对应的子进程执行完毕,然后return 0

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
int atoi(char *s) {
int ret = 0;
while (*s) {
ret = ret * 10 + (*s++ - '0');
}
return ret;
}
int runcmd(char *s) {
// ...
if (!strcmp(argv[0], "fg")) {
int jobid = atoi(argv[1]);
if (jobid <= jobscount) {
debugf("fg: job (%d) do not exist\n", jobid);
return 0;
}
if (jobs[jobid].status == 0) {
debugf("fg: (0x%08x) not running\n", jobs[jobid].env_id);
return 0;
}
check2(jobs[jobid].env_id); // 等待这个进程执行完
return 0;
}
// ...
}

// wait.c
void check2(u_int envid) {
const volatile struct Env *e;
e = &envs[ENVX(envid)];
while (e->env_id == envid && e->env_status != ENV_FREE) {
syscall_yield();
}
}

对于kill命令,直接返回任务 id ,然后再主进程中将这个进程设置为ENV_NOT_RUNNABLE即可

1
2
3
4
5
6
7
8
else if (ret <= jobscount) {
if (jobs[ret].status) {
syscall_set_env_status(jobs[ret].env_id, ENV_NOT_RUNNABLE);
jobs[ret].status = 0;
} else {
debugf("fg: (0x%08x) not running\n", jobs[ret].env_id);
}
}

对于jobs指令,使用返回值-1来表示,然后在主进程输出对应的信息

1
2
3
4
5
else if (ret == -1) {
for (int i = 1; i <= jobscount; i++) {
debugf("[%d] %-10s 0x%08x %s\n", i, jobs[i].status ? "Running" : "Done", jobs[i].env_id, jobs[i].cmd);
}
}

另外值得注意的是,任务挂起以后会自己执行完毕,所以方便起见我们需要每次查询对应进程是否结束(状态是否为ENV_FREE),并更新对应状态信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for (int i = 1; i <= jobscount; i++) {
if (jobs[i].status) {
if (check(jobs[i].env_id)) {
jobs[i].status = 0;
}
}
}

// wait.c
int check(u_int envid) {
const volatile struct Env *e;
e = &envs[ENVX(envid)];
if (e->env_status == ENV_FREE) {
return 1;
}
return 0;
}