BUAA-OS Lab6 Shell挑战性任务记录

实现不带 .b 后缀指令

修改了 user/lib/spawn.c 中的库函数 spawn。当 prog 路径指向的文件不存在时,尝试添加 .b 后缀并重新尝试。

int spawnr(char *prog, char **argv, u_int recycle) { // 实现指令条件执行时增加了 spawn 库函数的功能并重命名为 spawnr
	char buf[MAXPATHLEN];

	int fd;
	if ((fd = open(prog, O_RDONLY)) < 0) {
		if (fd != -E_NOT_FOUND) {
			return fd;
		}
		int len = strlen(prog);
		if (len >= MAXPATHLEN - 3) {
			return fd; // prog 路径长度未超出长度限制,但添加 .b 后缀后超出长度限制
		}
		memcpy(buf, prog, len);
		memcpy(buf + len, ".b", 3);
		if ((fd = open(buf, O_RDONLY)) < 0) {
			return fd;
		}
	}
    ...
}

实现指令条件执行

内核与库函数扩展

若实现指令条件执行,则首先需要修改内核并增加相应的库函数,使进程的返回值可以被父进程获取。

在库函数中,扩展了 fork, spawn, exit, wait 函数的功能并重命名为 forkr, spawnr, exitr, waitr。具体如下:

  • 父进程创建子进程时,通过 int forkr(u_int recycle)int spawnr(char *prog, char **argv, u_int recycle) 函数的 recycle 参数指定是否需要回收子进程的返回值。规定:当 recycle 非 0 时表示需要回收,父进程在后续必须通过一次 waitr 函数回收子进程的返回值,否则会造成资源泄露;当 recycle 为 0 时,父进程仅可通过 waitr 函数等待子进程结束,不可获取子进程的返回值。
// user/lib/fork.c

int forkr(u_int recycle) {
	u_int child;
    ...
    child = syscall_exofork(recycle);
    ...
}

int fork(void) { // 兼容原库函数
	return forkr(0);
}

// user/lib/spawn.c

int spawnr(char *prog, char **argv, u_int recycle) {
    ...
    u_int child;
	if ((child = syscall_exofork(recycle)) < 0) {
		r = child;
		goto err;
	}
    ...
}

int spawn(char *prog, char **argv) { // 兼容原库函数
	return spawnr(prog, argv, 0);
}
  • 子进程退出时,通过 void exitr(int status) 向父进程传递返回值。若子进程创建时 recycle 参数为 0,则 status 参数没有任何作用。
// user/lib/libos.c

void exitr(int status) {
	close_all();
	
	syscall_env_destroy_r(0, status, 0);
	user_panic("unreachable code");
}

void exit(void) { // 兼容原库函数
	exitr(1);
}
  • 父进程通过 int waitr(u_int envid, int *status, int block) 函数等待子进程退出并获取子进程返回值。
// user/lib/wait.c

int waitr(u_int envid, int *status, int block) {
	const volatile struct Env *e;

	e = &envs[ENVX(envid)];
	int err = status ? -E_INVAL : 0; // 如果父进程希望获取子进程返回值,但创建子进程时未设置 recycle 参数,则返回 -E_INVAL
	while (e->env_id == envid && e->env_status != ENV_FREE) {
		if (e->env_status == ENV_END) {
			err = 0;
			if (status) {
				*status = e->env_exit_code; // 获取子进程返回值
			}
			panic_on(syscall_env_recycle(envid));
			panic_on(e->env_status != ENV_FREE);
			break;
		}
		if (!block) {
			return -E_NOT_END; // 实现前后台任务管理时使用
		}
		syscall_yield();
	}
	return err;
}

void wait(u_int envid) { // 兼容原库函数
	waitr(envid, 0, 1);
}

在系统调用中,为 syscall_exofork 函数增加了 recycle 参数;为 syscall_env_destroy 函数增加了 status 参数,并重命名为 syscall_env_destroy_r(u_int envid, int status, int flag)(flag 参数在实现前后台任务管理时添加,与当前无关);
增加了系统调用 int syscall_env_recycle(u_int envid)

在内核中,为 struct Env 结构体添加了 u_int env_recycleint env_exit_code 字段。

内核态代码的工作流程具体如下:

  • 当调用 forkr 或 spawnr,进而调用 int sys_exofork(u_int recycle) 时,对子进程 Env 结构体中的 env_recycle 字段赋值。
  • 当调用 exitr,进而调用 int sys_env_destroy(u_int envid, int status, int flag) 时,递归地调用 env_destroy_r(struct Env *e, int status, int flag),进而调用 void env_free_r(struct Env *e, int status)。在 env_free_r 中,若 Env 结构体中的 env_recycle 字段非 0,则将进程的状态变更为 ENV_END,同时对 Env 结构体中的 env_exit_code 字段赋值;否则保持原操作不变,即变更进程的状态为 ENV_FREE,并将进程控制块插入空闲链表中。
// include/env.h

#define ENV_FREE 0
#define ENV_RUNNABLE 1
#define ENV_NOT_RUNNABLE 2
#define ENV_END 3

// kern/env.c

void env_free_r(struct Env *e, int status) {
    ...
    if (e->env_recycle) {
		e->env_status = ENV_END;
		e->env_exit_code = status;
	} else {
		if (e->env_parent_id) {
			LIST_REMOVE(e, env_link); // 实现前后台任务管理时使用,与当前无关
		}
		e->env_status = ENV_FREE;
		LIST_INSERT_HEAD((&env_free_list), (e), env_link);
	}
}
  • 当进程状态变为 ENV_END 后,父进程即可通过结构体中 env_exit_code 字段获取子进程返回值(如上文 waitr 函数所示)。之后,父进程需要调用 syscall_env_recycle 回收子进程控制块,如下:
// kern/syscall_all.c

int sys_env_recycle(u_int envid) {
	return env_recycle(envid);
}

// kern/env.c

int env_recycle(u_int envid) {
	struct Env *e = envs + ENVX(envid);
	if (e->env_status != ENV_END || e->env_id != envid) {
		return -E_BAD_ENV;
	}
	if (e->env_parent_id) {
		LIST_REMOVE(e, env_link); // 同上
	}
	e->env_status = ENV_FREE;
	LIST_INSERT_HEAD((&env_free_list), (e), env_link);
	return 0;
}

用户程序(shell)扩展

规定重定向为最高优先级,管道为中等优先级,&&、||、; 为最低优先级。在 lab6 的 shell 中,runcmd 函数可执行一个包含重定向和管道的命令。因此,只需要把含 &&、||、; 的命令分割为若干个子命令,每个子命令调用 runcmd 函数执行即可。

由于 &&、|| 需要获取子命令的返回值,即 fork 出来运行子命令的子 shell 需要将子命令的返回值作为自身的返回值供父 shell 使用,故首先使用如下设计修改 runcmd 函数:当子命令中不包含管道时,子 shell 的返回值为命令的返回值;包含管道时,子 shell 的返回值为各个命令的返回值取逻辑或。

// user/sh.c

void runcmd(char *s) {
	...
	int result;
	if ((result = runincmd(argc, argv)) == -E_NOT_FOUND) { // 实现前后台任务管理(shell 内置命令)时使用,与当前无关
		int child = spawnr(argv[0], argv, 1);
		close_all();
		if (child > 0) {
			panic_on(waitr(child, &result, 1));
		} else {
			debugf("spawn %s: %d\n", argv[0], child);
		}
	} else {
		close_all();
	}
	if (rightpipe) {
		int status;
		panic_on(waitr(rightpipe, &status, 1));
		result = result || status;
	}
	exitr(result);
}

然后,实现函数 void runline(char *s),功能为执行一个包含重定向、管道、&&、||、; 的命令(即把含 &&、||、; 的命令分割为若干个子命令,每个子命令调用 runcmd 函数)。该函数也同时包含了实现反引号、实现注释功能、实现一行多指令、实现引号支持等功能。

// user/sh.c

char rbuf[MAXCMDLEN];

void runline(char *s) {
	int pos = 0, type = 0, ntype, result = 0, in = 0;
	for (int i = 0; s[i]; i++) {
        // 实现引号支持(见后文)
		if (s[i] == '"') {
			in ^= 0x80;
			continue;
		}
		if (!in) { // 当前字符在引号之外
            ...
            // 实现一行多指令
			if (s[i] == ';') {
				ntype = 0;
				goto separator;
			}
            // 实现指令条件执行
			if ((s[i] == '&' || s[i] == '|') && s[i] == s[i + 1]) {
				ntype = s[++i];
				goto separator;
			}
            ...
		}
		rbuf[pos++] = s[i] | in; // 实现引号支持
		panic_on(pos >= MAXCMDLEN - 1);
		continue;
separator:
        // 运行子命令
		rbuf[pos] = 0;
		if (type == '&') {
            // 对于 &&,仅之前的命令范围 0 才执行
			if (result == 0) result = run(rbuf);
		} else if (type == '|') {
            // 对于 ||,仅之前的命令返回非 0 才执行
			if (result) result = run(rbuf);
		} else {
            // 对于 ;,无论之前的命令如何返回,均执行
			result = run(rbuf);
		}
		pos = 0;
		type = ntype;
		continue;
	}
    // 运行最后一个子命令
	rbuf[pos] = 0;
	if (type == '&') {
		if (result == 0) runcmd(rbuf);
	} else if (type == '|') {
		if (result) runcmd(rbuf);
	} else {
		runcmd(rbuf);
	}
}

可以注意到其中使用了另一个辅助函数 run,这是因为执行 runcmd 函数会退出进程,所以需要 fork 一个子 shell 来完成。只有当运行最后一个子命令时,当前进程可以直接退出(因为当前进程也是由 main 函数 fork 出的子 shell),调用 runcmd 即可。

// user/sh.c

int run(char *buf) {
	int child = forkr(1);
	panic_on(child < 0);
	if (child == 0) {
		runcmd(buf);
		exit();
	}
	int status;
	waitr(child, &status, 1);
	return status;
}

最后,把 main 函数中的 runcmd(buf) 改为 runline(buf) 即可。

// user/sh.c

int main(int argc, char **argv) {
    ...
        if ((r = fork()) < 0) {
			user_panic("fork: %d", r);
		}
		if (r == 0) {
			runline(buf);
			exit();
		}
    ...
}

实现更多指令

内核与库函数扩展

简单回顾实验代码即可发现目前的内核并不支持创建文件夹。考虑到 touch 和 mkdir 指令都需要用到创建文件(夹)的功能,故新增文件系统请求类型 FSREQ_CREATE,并实现相应的库函数 int create(const char *path, u_int type)

// user/include/fsreq.h

struct Fsreq_create {
	char req_path[MAXPATHLEN];
	u_int req_type;
};

// user/lib/fsipc.c

int fsipc_create(const char *path, u_int type) {
	if (path[0] == '\0' || strlen(path) >= MAXPATHLEN) {
		return -E_BAD_PATH;
	}
	struct Fsreq_create *req = (struct Fsreq_create *)fsipcbuf;
	strcpy(req->req_path, path);
	req->req_type = type;
	return fsipc(FSREQ_CREATE, req, 0, 0);
}

// user/lib/file.c

int create(const char *path, u_int type) { // type 为 FTYPE_REG 或 FTYPE_DIR
	return fsipc_create(path, type);
}

然后新增相应的服务函数,并修改 file_create 函数供其调用。

// fs/serv.c

void serve_create(u_int envid, struct Fsreq_create *rq) {
	int r;
	r = file_create(rq->req_path, rq->req_type, NULL);
	ipc_send(envid, r, 0, 0);
}

// fs/fs.c

int file_create(char *path, u_int type, struct File **file) {
	...
	strcpy(f->f_name, name);
	f->f_type = type; // 新增:对 f_type 字段赋值
	if (file) {
		*file = f;
	}
	return 0;
}

PS:课程组其实在文件打开模式中预留了未实现的模式 O_MKDIR,大概希望我们把创建文件夹的功能写到 open 函数里。但 open 函数同时包含了创建文件(夹)和打开文件(夹)的功能,略显冗余,个人更倾向于新建一个库函数 create 仅负责创建文件(夹)而不打开。

用户程序扩展

  • 最简单的 touch :
// user/touch.c

#include <lib.h>

int main(int argc, char **argv) {
    if (argc == 1) {
        return 1;
    }
    int err = 0;
    for (int i = 1; i < argc; i++) { // 考虑了一次性创建多个文件的情况
        int r = create(argv[i], FTYPE_REG);
        if (r < 0 && r != -E_FILE_EXISTS) {
            err = 1;
            if (r == -E_NOT_FOUND) {
                printf("touch: cannot touch '%s': No such file or directory\n", argv[i]);
            } else {
                printf("touch: cannot touch '%s': Unknown error %d\n", argv[i], r);
            }
        }
    }
    return err;
}
  • 然后是 mkdir,-p 参数表示递归地创建目录。这里使用了较为平凡的方法进行递归创建,即依次调用 create 函数创建从根目录到子目录的各级父目录。
// user/mkdir.c

#include <lib.h>

char buf[MAXPATHLEN];

int main(int argc, char **argv) {
    if (argc == 1) {
        return 1;
    }
    int flag = 0;
    ARGBEGIN {
	case 'p':
		flag = 1;
        break;
    default:
        return 1;
	}
	ARGEND
    int err = 0;
    for (int i = 0; i < argc; i++) { // 同上
        if (flag) {
            for (int p = 0, r;;) {
                while (argv[i][p] == '/') {
                    p++;
                }
                if (argv[i][p] == '\0') {
                    break;
                }
                do {
                    p++;
                } while (argv[i][p] != '/' && argv[i][p] != '\0');
				// 每遇到一个 '/' 就对之前的路径调用一次 create
                memcpy(buf, argv[i], p);
                buf[p] = '\0';
                r = create(buf, FTYPE_DIR);
                if (r < 0 && r != -E_FILE_EXISTS) {
                    err = 1;
                    printf("mkdir: cannot create directory '%s': Unknown error %d\n", argv[i], r);
                    break;
                }
            }
        } else {
            int r = create(argv[i], FTYPE_DIR);
            if (r < 0) {
                err = 1;
                if (r == -E_FILE_EXISTS) {
                    printf("mkdir: cannot create directory '%s': File exists\n", argv[i]);
                } else if (r == -E_NOT_FOUND) {
                    printf("mkdir: cannot create directory '%s': No such file or directory\n", argv[i]);
                } else {
                    printf("mkdir: cannot create directory '%s': Unknown error %d\n", argv[i], r);
                }
            }
        }
    }
    return err;
}
  • 最后是 rm。虽然 remove 函数的注释中标明其可以删除文件夹,但阅读代码不难发现,如果使用 remove 删除一个非空文件夹,则文件夹下的文件不会被真正删除,其数据块在 bitmap 中仍是占用状态。所以最好先删除文件夹下的所有文件,再使用 remove 删除文件夹本身。(我是不会告诉你直接 remove 一个非空文件夹其实也能通过评测的。
// use/rm.c

#include <lib.h>

char buf[MAXPATHLEN];

int remove_dir(int p) { // 删除 buf 保存的非空目录,p 为 strlen(buf)
    int fd, r;
    struct File f;
    if ((fd = open(buf, O_RDONLY)) < 0) {
		return fd;
	}
    buf[p++] = '/';
	while ((r = readn(fd, &f, sizeof(f))) == sizeof(f)) { // 仿照 ls 的实现,读取目录下的文件
		if (f.f_name[0]) {
            int len = strlen(f.f_name);
            memcpy(buf + p, f.f_name, len);
            buf[p + len] = '\0';
			if (f.f_type == FTYPE_DIR) {
                if ((r = remove_dir(p + len)) < 0) { // 递归地删除子目录
                    close(fd);
                    return r;
                }
            } else {
                if ((r = remove(buf)) < 0) {
                    close(fd);
                    return r;
                }
            }
            //printf("rm: removed %s\n", buf); // 验证递归的正确性
		}
	}
    close(fd);
	if (r > 0) {
		return -E_UNSPECIFIED;
	}
	if (r < 0) {
		return r;
	}
    buf[--p] = '\0';
    return remove(buf);
}

int main(int argc, char **argv) {
    if (argc == 1) {
        return 1;
    }
    int rflag = 0, fflag = 0;
    ARGBEGIN {
	case 'r':
		rflag = 1; // 允许递归删除目录
        break;
    case 'f':
        fflag = 1; // 忽略不存在的文件
        break;
    default:
        return 1;
	}
	ARGEND
    int err = 0;
    for (int i = 0; i < argc; i++) { // 同上
        int r;
        struct Stat st;
        if ((r = stat(argv[i], &st)) < 0) {
            if (r == -E_NOT_FOUND) {
                if (!fflag) {
                    err = 1;
                    printf("rm: cannot remove '%s': No such file or directory\n", argv[i]);
                }
            } else {
                err = 1;
                printf("rm: cannot remove '%s': Unknown error %d\n", argv[i], r);
            }
            continue;
        }
        if (st.st_isdir) {
            if (rflag) {
                int len = strlen(argv[i]);
                memcpy(buf, argv[i], len);
                buf[len] = '\0';
                if ((r = remove_dir(len)) < 0) {
                    err = 1;
                    printf("rm: cannot remove '%s': Unknown error %d\n", argv[i], r);
                }
            } else {
                err = 1;
                printf("rm: cannot remove '%s': Is a directory\n", argv[i]);
            }
        } else {
            if ((r = remove(argv[i])) < 0) {
                err = 1;
                printf("rm: cannot remove '%s': Unknown error %d\n", argv[i], r);
            }
        }
    }
    return err;
}

实现反引号

需要在遇到反引号时,fork 一个子 shell,由子 shell 运行反引号内的命令,并通过管道传递给父 shell。父 shell 读取管道中的内容并将其作为命令中的字符写入 rbuf 字符串,供 runcmd 函数执行。

char rbuf[MAXCMDLEN];

void runline(char *s) {
	int pos = 0, ..., in = 0;
	for (int i = 0; s[i]; i++) {
		if (s[i] == '"') {
			in ^= 0x80;
			continue;
		}
		if (!in) {
			...
			if (s[i] == '`') {
				int pi = ++i;
				while (s[i] != '`') {
					panic_on(s[i] == 0);
					i++;
				}
                s[i] = 0; // 添加结束符 '\0',此时 (char *)(s + pi) 即为反引号内的字符串
				int fd[2];
				panic_on(pipe(fd));
				int child = fork();
				panic_on(child < 0);
				if (child) {
					close(fd[1]);
					int n;
					while((n = read(fd[0], rbuf + pos, MAXCMDLEN - pos)) > 0) pos += n; // 将命令的输出存入 rbuf
					panic_on(n < 0);
					panic_on(pos >= MAXCMDLEN - 1);
					close(fd[0]);
					continue;
				} else {
					close(fd[0]);
					dup(fd[1], 1); // 将输出重定向到管道
					close(fd[1]);
					runline(s + pi); // 子 shell 和父 shell 不共享内存,可以安全地递归调用
					exit();
				}
			}
		}
		rbuf[pos++] = s[i] | in;
		panic_on(pos >= MAXCMDLEN - 1);
		...
	}
	rbuf[pos] = 0;
	...
	runcmd(rbuf); // rbuf 既包含 s 本身的内容,也包含反引号内的命令输出
	...
}

实现注释功能

在引号外遇到 # 时,直接停止解析即可。

// user/sh.c

void runline(char *s) {
	int ..., in = 0;
	for (int i = 0; s[i]; i++) {
		if (s[i] == '"') {
			in ^= 0x80;
			continue;
		}
		if (!in) { // 当前字符在引号之外
            if (s[i] == '#') break;
            ...
		}
	}
    // 运行最后一个子命令
	...
}

实现历史指令

首先为 shell 添加执行内置命令的功能。添加内置命令表 in_cmd 和内置命令执行函数 int runincmd(int argc, char **argv),在调用 spawn 前判断命令是否为内置命令,若不是再调用 spawn。

// user/sh.c

int _jobs(int argc, char **argv) {
	...
}

int _fg(int argc, char **argv) {
	...
}

...

const struct {
	const char *name;
	int (*func)(int, char **);
} in_cmd[] = {{"jobs", _jobs}, {"fg", _fg}, {"kill", _kill}, {"history", _history}, {NULL, NULL}};

int runincmd(int argc, char **argv) {
	for (int i = 0; in_cmd[i].name; i++) {
		if (strcmp(argv[0], in_cmd[i].name) == 0) {
			return in_cmd[i].func(argc, argv);
		}
	}
	return -E_NOT_FOUND;
}

void runcmd(char *s) {
	...
	int result;
	if ((result = runincmd(argc, argv)) == -E_NOT_FOUND) {
		int child = spawnr(argv[0], argv, 1);
		close_all();
		if (child > 0) {
			panic_on(waitr(child, &result, 1));
		} else {
			debugf("spawn %s: %d\n", argv[0], child);
		}
	} else {
		close_all();
	}
	...
}

然后实现 history 指令和保存历史指令到 .mosh_history 文件中,难度并不大。

// user/sh.c

#define HISTFILE ".mosh_history"
#define HISTFILESIZE 20
char history_data[HISTFILESIZE + 1][MAXCMDLEN]; // +1 是为方便实现上下键选择指令
char *history_cmd[HISTFILESIZE + 1]; // 使用指针降低 swap 字符串的时间开销
int history_top;

int _history(int argc, char **argv) {
	for (int i = 0; i < history_top; i++) {
		printf("%s\n", history_cmd[i]);
	}
	return 0;
}

int main(int argc, char **argv) {
    ...
    // 读取原有 ".mosh_history" 文件的内容
    // 挑战性任务里貌似没有要求这一功能,不过如果不加这一功能,将历史指令保存到 ".mosh_history" 文件中似乎没有任何意义
    if ((r = open(HISTFILE, O_RDONLY)) >= 0) {
		for (; history_top < HISTFILESIZE; history_top++) {
			for (int pos = 0;; pos++) {
				if (read(r, history_data[history_top] + pos, 1) != 1) {
					if (pos) history_top++;
					goto history_end;
				}
				if (history_data[history_top][pos] == '\n') {
					history_data[history_top][pos] = '\0';
					break;
				}
			}
		}
history_end:
		close(r);
	}
    // 初始化
    for (int i = 0; i <= HISTFILESIZE; i++) {
		history_cmd[i] = history_data[i];
	}
    for (;;) {
		...
		int len = readline(buf, MAXCMDLEN); // 按照自身的喜好小小修改了 readline,使其返回值为 strlen(buf)
		...
		if (history_top >= HISTFILESIZE) {
            // 覆盖最早历史指令的内容,并使用指针高效地将历史指令向前移动一条
			char *cmd = history_cmd[0];
			strcpy(cmd, buf);
			for (int i = 0; i < HISTFILESIZE - 1; i++) {
				history_cmd[i] = history_cmd[i + 1];
			}
			history_cmd[HISTFILESIZE - 1] = cmd;
		} else {
            // 历史指令的条数小于最大容量,新增一条即可
			strcpy(history_cmd[history_top++], buf);
		}
        // 写入 ".mosh_history"
		if ((r = open(HISTFILE, O_WRONLY | O_CREAT | O_TRUNC)) >= 0) {
			for(int i = 0; i < history_top; i++) {
				write(r, history_cmd[i], strlen(history_cmd[i]));
				write(r, "\n", 1);
			}
			close(r);
		}
        // 执行指令
		...
	}
	return 0;
}

发现已经通过评测了,咱摆烂吧。

最后添加使用上下键选择历史指令的功能。使用 debugf 调试可以发现,按下 up 时,qemu 会连续接收到 0x1b, 0x5b, 0x41 三个字符;按下 down 时则连续收到 0x1b, 0x5b, 0x42 三个字符。细节见下:

// user/sh.c

int readline(char *buf, u_int n) {
	int r, cur = history_top; // cur < history_top 表示处于第 cur 条历史指令,cur == history_top 表示处于当前输入的指令(默认状态)
	for (int i = 0; i < n; i++) {
read_again:
		if ((r = read(0, buf + i, 1)) != 1) {
			...
		}
		...
		if ((buf[i] == 0x41 || buf[i] == 0x42) && i >= 2 && buf[i - 1] == 0x5b && buf[i - 2] == 0x1b) {
			int ni; // 表示将要切换到的历史指令
			if (buf[i] == 0x41) {
				printf("\x1b\x5b\x42"); // 按下 up 键时,控制台的光标会向上移动一格,此时输出一个 down 键可抵消,否则界面会十分丑陋
				ni = cur - 1;
			} else {
                // 按下 down 键光标却不会向下移动,试试就知道了
				ni = cur + 1;
			}
			i -= 2;
			buf[i] = 0; // up/down 键输入的三个控制字符不应存入 buf 中
			if (ni >= 0 && ni <= history_top) {
				while (i--) printf("\b"); // 清空屏幕上已经输入的内容
				if (cur == history_top) {
                    // 如果切换前位于当前输入的指令,则保存,再次切换回来时可以恢复之前输入的内容
					strcpy(history_cmd[cur], buf);
				}
				cur = ni;
				strcpy(buf, history_cmd[cur]); // 替换缓冲区的内容
				i = strlen(history_cmd[cur]); // i 也顺便替换一下,支持了对历史指令进行编辑(虽然评测并未要求)
				printf("%s", buf); // 输出切换到的历史记录内容
			} 
			goto read_again;
		}
		...
	}
	...
}

实现一行多指令

由于 ; 的行为与 &&、|| 相似,故将两者合并编写。见“实现指令条件执行”部分。

实现追加重定向

内核与库函数扩展

需要新增 open 函数打开文件的模式 O_APPEND。在 serve_open 函数中,若打开模式包含 O_APPEND,则将文件描述符中的指针置于文件末尾。这种实现并不支持多进程同时使用 O_APPEND 写入同一文件,但对于通过挑战性任务来说已经够了。

// fs/serv.c

void serve_open(u_int envid, struct Fsreq_open *rq) {
	...
	if (rq->req_omode & O_APPEND) {
		ff->f_fd.fd_offset = f->f_size;
	}
	...
}

用户程序(shell)扩展

对追加重定向符的解析,似乎只适合放到 _gettoken 函数中实现。在处理特殊字符时,额外判断是否为追加重定向符 >>,是则返回 ‘>’ | 0x80。

// user/sh.c

int _gettoken(char *s, char **p1, char **p2) {
    ...
    if (strchr(SYMBOLS, *s)) {
		int t = *s;
		*p1 = s;
		*s++ = 0;
		if (t == '>' && *s == '>') { // 判断是否为追加重定向符
			t |= 0x80;
			*s++ = 0; // 跳过第二个 '>'
		}
		*p2 = s;
		return t;
	}
    ...
}

int parsecmd(char **argv, int *rightpipe) {
    ...
	while (1) {
		char *t;
		int fd;
		int c = gettoken(0, &t);
		switch (c) {
            ...
		case '>':
		case ('>' | 0x80):
			if (gettoken(0, &t) != 'w') {
				debugf("syntax error: > not followed by word\n");
				exit();
			}

			if ((fd = open(t, O_WRONLY | O_CREAT | ((c & 0x80) ? O_APPEND : O_TRUNC))) < 0) { // 追加重定向使用 O_APPEND,普通重定向使用 O_TRUNC
				debugf("open returned %d", fd);
				exit();
			}
			dup(fd, 1);
			close(fd);
			
			break;
            ...
		}
	}
	...
}

实现引号支持

使 lab6 原有的 _gettoken 函数支持引号并不简单(好吧,或许也并不困难)。为了尽可能避免修改原有实验代码,我将实现引号支持的功能与其它新增功能一起放在 runline 函数中。如果某个字符在引号之内,则将其最高位置 1 作为标记(这或许并不是一个好习惯,因为其牺牲了 shell 对非 ASCII 字符的支持)。这样操作后,只需在 _gettoken 中新增一行解除标记即可。

// user/sh.c

#define SYMBOLS "<|>"

int _gettoken(char *s, char **p1, char **p2) {
	...
	if (strchr(SYMBOLS, *s)) { // 因为标记位的存在,<、>、| 不会被当作特殊字符处理
		...
		return t;
	}

	*p1 = s;
	while (*s && !strchr(WHITESPACE SYMBOLS, *s)) {
		*s &= 0x7f; // 解除标记(仅需新增此行)
		s++;
	}
	*p2 = s;
	return 'w';
}

char rbuf[MAXCMDLEN];

void runline(char *s) {
	int pos = 0, ..., in = 0;
	for (int i = 0; s[i]; i++) {
        // in 记录当前是否位于引号之内
		if (s[i] == '"') {
			in ^= 0x80;
			continue;
		}
		if (!in) { // 当前字符在引号之外
            // 处理 #、;、&、|、`等特殊字符
            ...
		}
		rbuf[pos++] = s[i] | in; // 如果在引号之内,则将字符最高位置 1 进行标记
		panic_on(pos >= MAXCMDLEN - 1);
        ...
	}
    ...
}

实现前后台任务管理

内核与库函数扩展

判断后台任务的运行状态需要调用某个库函数实现。目前与之相关的库函数只有 wait,但 wait 会阻塞等待子进程退出。因此考虑添加 block 参数,为 0 时表示非阻塞等待。此时,若子进程已退出,则 wait 的行为不变;若子进程尚未退出,则 wait 不再等待子进程,直接返回 -E_NOT_END。

// include/error.h

#define E_NOT_END 14

// user/lib/wait.c

int waitr(u_int envid, int *status, int block) {
	const volatile struct Env *e;

	e = &envs[ENVX(envid)];
	int err = status ? -E_INVAL : 0;
	while (e->env_id == envid && e->env_status != ENV_FREE) {
		if (e->env_status == ENV_END) {
			err = 0;
			if (status) {
				*status = e->env_exit_code;
			}
			panic_on(syscall_env_recycle(envid));
			panic_on(e->env_status != ENV_FREE);
			break;
		}
		if (!block) {
			return -E_NOT_END; // 不再等待子进程,直接返回
		}
		syscall_yield();
	}
	return err;
}

void wait(u_int envid) {
	waitr(envid, 0, 1);
}

另外,在 kill 命令中,shell 需要终止某个运行中的后台进程。考虑到目前的代码架构以及 ls | cat & 这类需要在 kill 时终止多个进程的组合命令(虽然挑战性任务没有要求),故在内核中实现了递归终止进程树的功能。

在 syscall_env_destroy 中,增加 flag 参数。当 flag 最低位为 1 时,表示需要递归地终止此进程及其创建的所有子进程;当 flag 最高位(符号位)为 1 时,表示取消对权限的检查,否则,envid 必须为当前进程本身或当前进程的子进程(即 flag 的最高位最终会被传入 envid2env 函数的 checkperm 参数,这个设计在 shell 中会用到)。

// user/lib/syscall_lib.c

int syscall_env_destroy_r(u_int envid, int status, int flag) {
	return msyscall(SYS_env_destroy, envid, status, flag);
}

int syscall_env_destroy(u_int envid) {
	return msyscall(SYS_env_destroy, envid, 1, 0);
}

然后修改 sys_env_destroy 函数。其中枚举子进程的实现方式在后文会提及。

// kern/env.c

// 结束进程 e 及其所有子进程,parent_id 仅作断言使用
void _sys_env_destroy(struct Env *e, int parent_id) {
	panic_on(e->env_status == ENV_FREE || e->env_parent_id != parent_id);

	struct Env *child, *temp;
	LIST_FOREACH_RW(child, temp, &e->child_list, env_link) { // 枚举 e 的子进程
		_sys_env_destroy(child, e->env_id);
	}
	
	printk("[%08x] destroying %08x\n", curenv->env_id, e->env_id);
	e->env_recycle = 0; // 递归结束后,e 的父进程一定会被终止,不可能再调用 waitr 获取子进程 e 的状态,所以将 env_recycle 置 0,避免资源泄露
	env_destroy_r(e, 1, 1);
}

int sys_env_destroy(u_int envid, int status, int flag) {
	struct Env *e;
	try(envid2env(envid, &e, flag >= 0)); // flag 的最高位(符号位)表示是否检查权限

	if (flag & 1) { // 递归结束进程树
		struct Env *child, *temp;
		LIST_FOREACH_RW(child, temp, &e->child_list, env_link) { // 枚举 e 的子进程
			_sys_env_destroy(child, envid);
		}
	}
	
	printk("[%08x] destroying %08x\n", curenv->env_id, e->env_id);
	env_destroy_r(e, status, 0);
	if (curenv == NULL) {
		schedule(1);
	}
	return 0;
}

接下来阅读 env_destroy 函数,发现该函数在参数 e 为当前进程时会调用 schedule(1) 进行调度。在终止进程树时,这样可能导致递归过程提前结束,为稳妥起见,将这一功能移至 sys_env_destroy 函数中进行。

// kern/env.c

void env_destroy_r(struct Env *e, int status, int flag) {
	env_free_r(e, status);

	if (curenv == e) {
		curenv = NULL;
		printk("i am killed ... \n");
		if (!flag) schedule(1); // 若 flag 非 0,则不进行调度
	}
}

void env_destroy(struct Env *e) {
	env_destroy_r(e, 1, 0);
}

再次回看 sys_env_destroy 函数:

// kern/env.c

void _sys_env_destroy(struct Env *e, int parent_id) {
	panic_on(e->env_status == ENV_FREE || e->env_parent_id != parent_id);

	struct Env *child, *temp;
	LIST_FOREACH_RW(child, temp, &e->child_list, env_link) {
		_sys_env_destroy(child, e->env_id);
	}
	
	printk("[%08x] destroying %08x\n", curenv->env_id, e->env_id);
	e->env_recycle = 0;
	env_destroy_r(e, 1, 1); // 此时递归尚未结束,将 flag 置 1 禁止调度
}

int sys_env_destroy(u_int envid, int status, int flag) {
	struct Env *e;
	try(envid2env(envid, &e, flag >= 0));

	if (flag & 1) {
		struct Env *child, *temp;
		LIST_FOREACH_RW(child, temp, &e->child_list, env_link) {
			_sys_env_destroy(child, envid);
		}
	}
	
	printk("[%08x] destroying %08x\n", curenv->env_id, e->env_id);
	env_destroy_r(e, status, 0); // 递归过程已结束,flag 可以置 0
	if (curenv == NULL) {
		schedule(1); // 将调度移至此处进行
	}
	return 0;
}

接下来仅需考虑怎样遍历某个进程的子进程。考虑在进程控制块中添加 child_list 字段作为子进程链表的入口。由于进程控制块中的 env_link 只在控制块空闲时用于构建空闲进程链表,当控制块非空闲时并没有实际作用,故可使 env_link 同时用于构建其父进程的子进程链表。创建进程时,若父进程 id 不为 0,则将进程块通过 env_link 插入其父进程的 child_list 中;进程结束时,将进程块从 child_list 中删除,如下。

// include/env.h

struct Env {
	...
	struct Env_list child_list;
};

// kern/env.c

int env_alloc(struct Env **new, struct Env *parent) { // 由于需要操作父进程控制块,将 u_int parent_id 改为 struct Env *parent
	...
	LIST_REMOVE(e, env_link); // 从空闲进程链表中移除

	if (parent) {
		e->env_parent_id = parent->env_id;
		LIST_INSERT_HEAD(&parent->child_list, e, env_link); // 插入父进程的子进程链表
	} else {
		e->env_parent_id = 0;
	}
	LIST_INIT(&e->child_list); // 初始化本进程的子进程链表
	...
}

void env_free_r(struct Env *e, int status) {
	...
	if (e->env_recycle) {
		e->env_status = ENV_END;
		e->env_exit_code = status;
	} else {
		if (e->env_parent_id) {
			LIST_REMOVE(e, env_link); // 将进程从其父进程的子进程链表中移除
		}
		e->env_status = ENV_FREE;
		LIST_INSERT_HEAD((&env_free_list), (e), env_link);
	}
}

int env_recycle(u_int envid) {
	struct Env *e = envs + ENVX(envid);
	if (e->env_status != ENV_END || e->env_id != envid) {
		return -E_BAD_ENV;
	}
	if (e->env_parent_id) {
		LIST_REMOVE(e, env_link); // 同上
	}
	e->env_status = ENV_FREE;
	LIST_INSERT_HEAD((&env_free_list), (e), env_link);
	return 0;
}

最后考虑一点小细节,即如果在 sys_env_destroy 中使用 LIST_FOREACH 宏对子进程进行遍历,则子进程控制块的 env_link 字段会在 env_free_r 中 LIST_INSERT_HEAD((&env_free_list), (e), env_link) 时被修改,使 LIST_FOREACH 宏将无法正确获取子进程链表中的下一元素。故实现读写安全的 LIST_FOREACH_RW 宏并用于对子进程的遍历中。

// include/queue.h

// 原有的宏
#define LIST_FOREACH(var, head, field) \
	for ((var) = LIST_FIRST((head)); (var); (var) = LIST_NEXT((var), field))

// 新实现的读写安全宏,temp 为与 var 同类型的临时变量
#define LIST_FOREACH_RW(var, temp, head, field) \
	for ((var) = LIST_FIRST((head)), ((temp) = (var) ? LIST_NEXT((var), field) : 0); (var); (var) = (temp), ((temp) = (var) ? LIST_NEXT((var), field) : 0))

用户程序(shell)扩展

首先定义用于记录后台任务的结构体数组 task:

// user/sh.c

#define MAXTASK 16
const char *status_str[] = {"Done", "Running"}; // jobs 命令需要输出的状态字符串
struct {
	int envid;
	int status; // 0 为 Done,1 为 Running
	char cmd[MAXCMDLEN]; // 命令行
} task[MAXTASK];
int task_top;

在 main 函数中解析命令末尾的 ‘&’,并将后台命令记录至 task 数组:

// user/sh.c

int main(int argc, char **argv) {
	...
	for (;;) {
		...
		int background = 0;
		while (len > 0 && strchr(WHITESPACE, buf[len - 1])) len--; // 忽略结尾空格
		if ((len > 0 && buf[len - 1] == '&') && (len == 1 || buf[len - 2] != '&')) {
			panic_on(task_top >= MAXTASK);
			task[task_top].status = 1; // 运行状态为 Running
			strcpy(task[task_top].cmd, buf); // 记录命令行
			background = 1;
			buf[--len] = 0; // 删除末尾的 '&'
		}
		...
		if ((r = fork()) < 0) {
			user_panic("fork: %d", r);
		}
		if (r == 0) {
			runline(buf);
			exit();
		} else if (background) {
			task[task_top++].envid = r; // 记录 envid
		} else {
			wait(r); // 只有不为后台命令时才等待命令完成
		}
	}
	return 0;
}

对于 jobs 命令,需要在每次执行新命令前更新后台命令的状态,并在 _jobs 函数中输出。之所以不在 _jobs, _fg, _kill 函数中进行更新操作,是因为这些函数实际上是由 fork 出的子 shell 执行的,仅可读取 task 数组,无法进行写入操作(写入后父 shell 的 task 数组不会发生任何变化)。

// user/sh.c

int _jobs(int argc, char **argv) {
	for (int i = 0; i < task_top; i++) {
		printf("[%d] %-10s 0x%08x %s\n", i + 1, status_str[task[i].status], task[i].envid, task[i].cmd);
	}
	return 0;
}

int main(int argc, char **argv) {
	...
	for (;;) {
		...
		for (int i = 0; i < task_top; i++) {
			if (task[i].status) {
				// 使用非阻塞等待,返回 -E_NOT_END 说明进程尚未结束
				task[i].status = waitr(task[i].envid, 0, 0) == -E_NOT_END;
			}
		}
		if ((r = fork()) < 0) {
			user_panic("fork: %d", r);
		}
		...
	}
	return 0;

对于 fg 指令,直接使用 wait 等待对应进程退出即可。

// user/sh.c

int _fg(int argc, char **argv) {
	if (argc != 2) return 1;
	int j_id = atoi(argv[1]); // 需自行实现 atoi 函数
	if (j_id <= 0 || j_id > task_top) {
		printf("fg: job (%d) do not exist\n", j_id);
		return 1;
	}
	j_id--;
	if (!task[j_id].status) {
		printf("fg: (0x%08x) not running\n", task[j_id].envid);
		return 1;
	}
	wait(task[j_id].envid);
	return 0;
}

对于 kill 指令,使用上文实现的 syscall_env_destroy_r,终止对应 envid 的进程树。之所以需要终止 envid 进程的子进程,是因为在当前架构中,task 数组中记录的 envid 实际上是子 shell 的 envid,而非 spawn 出的外部进程的 envid。这就导致我们如果不递归地终止 envid 进程的子进程,则实际上并不能终止后台命令,只是终止了在等待这个命令完成的子 shell。同样,之所以需要在 syscall_env_destroy_r 中取消对于权限的检查,是因为在当前架构中,执行 _kill 函数的是 fork 出的子 shell,子 shell 与 task 数组中 envid 对应的进程并不是父子关系,若进行权限检查则会出错。

// user/lib/wait.c

// 在 wait.c 中新增库函数 kill
int kill(u_int envid, int status, int flag) {
	return syscall_env_destroy_r(envid, status, flag);
}

// user/sh.c

int _kill(int argc, char **argv) {
	if (argc != 2) return 1;
	int j_id = atoi(argv[1]);
	if (j_id <= 0 || j_id > task_top) {
		printf("fg: job (%d) do not exist\n", j_id); // 不知为何,评测时要求这里输出 fg: 而不是 kill:
		return 1;
	}
	j_id--;
	if (!task[j_id].status) {
		printf("fg: (0x%08x) not running\n", task[j_id].envid);
		return 1;
	}
	panic_on(kill(task[j_id].envid, 1, -1)); // -1 即 0xffffffff,此处等价于 0x80000001
	return 0;
}

其它细节

比如修改 sys_cgetc,取消输入时的阻塞等待(否则 shell 获取键盘输入时,后台命令无法得到执行),在此不过多赘述。

对原实验代码疑似漏洞的修正和完善

  • kern/env.c 中的 env_free 函数会无条件地将进程控制块从 env_sched_list 中移除。如果此时进程的状态为 ENV_NOT_RUNNABLE 则可能出现错误(例如 spawn 函数结尾的错误处理代码中 syscall_env_destroy(child) 在 child 状态为 ENV_NOT_RUNNABLE 时调用了 env_free 函数)。为此将原实验代码修改为仅当进程的状态为 ENV_RUNNABLE 时将其从 env_sched_list 中移除。
void env_free_r(struct Env *e, int status) {
    ...
    if (e->env_status == ENV_RUNNABLE) {
		TAILQ_REMOVE(&env_sched_list, (e), env_sched_link);
	}
    ...
}
  • fs/serv.c 中的 serve_open 函数在处理错误时漏写了 return 语句。
void serve_open(u_int envid, struct Fsreq_open *rq) {
	...
	if (rq->req_omode & O_TRUNC) {
		if ((r = file_set_size(f, 0)) < 0) {
			ipc_send(envid, r, 0, 0);
			return; // 漏写
		}
	}
	...
}
  • user/sh.c 中的 readline 函数处理退格的逻辑有点混乱我没看懂,并且有访问 buf[-1] 导致内存溢出的风险。修改如下:
int readline(char *buf, u_int n) {
	int r, ...;
	for (int i = 0; i < n; i++) {
read_again:
		if ((r = read(0, buf + i, 1)) != 1) {
			...
		}
		if (buf[i] == '\b' || buf[i] == 0x7f) {
			if (i > 0) {
				i--;
				printf("\b");
			}
			goto read_again;
		}
		...
	}
	...
}

不足与改进

  • 在当前的 shell 架构中,总是会 fork 一个子 shell 用于解析和执行命令,这给 shell 内部命令的实现带来了较大困难。如果后续需要实现 cd 命令,则需要将命令解析的工作交由父 shell 完成,需要进行较大规模的代码重构。

  • 目前实现的库函数较为匮乏,如遍历文件夹下所有文件的操作暂时没有相应的库函数支持。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值