Lab6-Challenge 实验报告
实验概述
在本实验中,我通过编写新函数,新的方法实现一些小组件以及附加功能,丰富了我们的 shell的功能,包括"后台运行",“一行多命令”,“简单引号支持”,“tree /mkdir /touch 命令”,“清屏”,“彩色输出”,“历史命令”,“环境变量”,“cd 命令"等任务。
各部分实现思路及代码
Easy部分
(1)实现后台运行
1、首先在getc.S中,修改汇编代码,以防止前台被阻塞,手动忽略输入。
LEAF(sys_cgetc)
1: lb t0, 0x90000000
//beqz t0, 1b
//nop
move v0,t0
jr ra
nop
END(sys_cgetc)
2、修改shell
在sh.c
中runcmd()
定义一个变量 hang 标记是否要后台运行,初始化为0。
case '&':
hang = 1;
break;
在运行命令的时候,若无&,正常等待程序运行结束,若有&,那么就调用fork()
生成子进程,同时要防止阻塞,并且在程序运行结束后,输出提示信息。
//无&,就等进程结束
if (!hang)
wait(r);
//有&,就fork一个子进程,等待新加载的程序运行完成,输出提示信息
else
{
pid = fork();
if (pid == 0) //创建cmd子进程
{
wait(r); //等待新加载的程序运行完
writef("\n[%d] DONE\t", r);//提示信息
for (i = 0; i < argc; ++i)
writef("%s ", argv[i]);
char curpath[MAXPATHLEN];
int envid = syscall_getenvid();
int shellid = syscall_getfaid(envid);
curpath_get(shellid, curpath);
writef("\n" LIGHT_BLUE(% s) " " BOLD_GREEN($) " ", curpath);
writef("\b \b");
exit();
}
}
(2)一行多命令
使用 fork 创建一个子进程执行已经识别的命令,cmd进程则继续解析下一条指令,需要注意的是,每一条指令运行前,都需要对相应的状态变量进行初始化,然后后一条指令在子进程运行结束后,再进行运行。
case ';':
if ((pid = fork()) == 0) //创建子进程
{
goto runit;
}
wait(pid);//等待子进程运行结束
//参数重新初始化
argc = 0;
hang = 0;
rightpipe = 0;
//父进程跳出switch, 继续解析下一个命令
file_input = -1;
file_output = -1;
break;
(3)引号支持
修改 token 的部分,识别到引号的时候一直继续读,直到遇到下一个引号。
if (*s == '\"')
{
*p1 = ++s;
while (*s && !(*s == '\"' && *(s - 1) != '\\'))
{
++s;
}
*s++ = 0;
*p2 = s;
return 'w';
}
(4)tree / mkdir /touch
-
tree
主函数
tree()
通过参数的解析来调用walk()
void tree(char *path, char *prefix) { struct Stat status; int r = stat(path, &status); if (r < 0) user_panic("stat %s: %e", path, r); walk(path, 0); }
walk()
函数实现内部的递归调用,传入路径和层级数量,遍历文件。对于嵌套的目录,使用递归的方式遍历。输出时借助于层级数量来格式化输出结果。
void walk(char *path, int len) { int dir_fd; struct File f; int i; char newpath[MAXPATHLEN] = {0}; dir_fd = open(path, O_RDONLY); if (dir_fd < 0) user_panic("failed: open %s: %e", *path*, dir_fd); while (readn(dir_fd, &f, sizeof (struct File)) == sizeof (struct File)) { if (f.f_name[0]) { struct File *dir = &f; //格式化输出 for (i = 0; i < len*4; i++) fwritef(1, " "); fwritef(1, "\n"); for (i = 0; i < len*4; i++) fwritef(1, " "); fwritef(1, "|-- "); // 输出当前文件或目录 if (dir->f_type == FTYPE_REG) { fwritef(1, YELLOW(%s), dir->f_name); } //递归输出子目录 else if (dir->f_type == FTYPE_DIR) { fwritef(1, PURPLE(%s), dir->f_name); //拼接子目录路径 strcat(newpath, path); strcat(newpath, dir->f_name); strcat(newpath, "/"); // writef("nxtpath:\n%s", npath);* walk(newpath, len + 1); } } } }
-
mkdir
-
为了简化操作,实现后续mkdir,增加系统调用来实现获得shell的id
u_int sys_get_ShellId(int *sysno*, u_int *envid*) { struct Env *e ; envid2env(*envid*, &e, 0); return e->env_parent_id; }
-
得到当前地址curpath,拼接得到完整地址
-
对O_MKDIR进行open
void mkdir(char *path, char *prefix) { char curpath[MAXPATHLEN] = {0}; // writef("mkdir :envid是 %d shellid是 %d\n", id, shellid); int r; //获取相应的id int id = syscall_getenvid(); int shellid = syscall_get_ShellId(syscall_get_ShellId(id)); if ((r = curpath_get(shellid, curpath) )< 0) user_panic("mkdir: Error: curpath get\n"); //path的拼接 if (path[0] == '/') { strcat(curpath, path); } else { if (curpath[strlen(curpath) - 1] != '/') strcat(curpath, "/"); strcat(curpath, path); } //调用open r = open(curpath, O_RDWR|O_MKDIR); if (r < 0) user_panic("Directory %s created error\n", curpath); fwritef(1, "%s created successfully\n", curpath); }
-
-
touch
类似于mkdir,只不过是open的时候使用O_CREAT参数。
void touch(char *path, char *prefix) { char curpath[MAXPATHLEN] = {0}; int r; //获取相应的id int id=syscall_getenvid(); int shell_id = syscall_get_ShellId(syscall_get_ShellId(id)); if ((r= curpath_get(shell_id, curpath)) < 0) { fwritef(1, "mkdir: Error: get curpath\n"); return; } //path拼接 if (path[0] == '/') { strcpy(curpath, path); } else { if (curpath[strlen(curpath) - 1] != '/') strcat(curpath, "/"); strcat(curpath, path); } r = open(curpath, O_CREAT|O_RDWR); if (r < 0) { fwritef(1, "mkdir: Error: create file\n"); return; } fwritef(1, "File %s created!", curpath); }
Normal部分
历史命令功能
-
首先创建读取/写入历史文件 API
void history_init(); void history_save(char *s); int history_read(char(*cmd)[128]);
初始化函数
void history_init() { int r; if ((r = open("/.history", O_CREAT | O_RDWR)) < 0) user_panic("His : Init Failed: %d.", r); }
-
将每一个输入执行的指令,利用
history_save()
保存进一个文件中,我创建的是.history
文件,每行存放一条指令。void history_save(char *s) { int r; if ((r = open("/.history", O_CREAT | O_RDWR | O_APPEND)) < 0) { user_panic("His : Open failed"); } fwritef(r, s); fwritef(r, "\n"); close(r); }
int history_read(char cmd[][128]) { int r, fd; char buf[128 * 128]; if ((fd = open("/.history", O_RDONLY)) < 0) { user_panic("His : Open failed"); } if ((r = read(fd, buf, sizeof(buf))) < 0) { user_panic("His : Read failed"); } close(fd); int i = 0, cmdi = 0; while (buf[i]) { int cmdj = 0; while (buf[i] && buf[i] != '\n') cmd[cmdi][cmdj++] = buf[i++]; if (!buf[i]) break; ++i; ++cmdi; } return cmdi; }
-
通过编写一个用户态程序
history.b
文件并写入磁盘中,使得每次调用history.b
时,能够将文件( .history )的内容全部输出。在 user/history.c 中 进行读取/.history文件#include "lib.h" void umain(int argc, char **argv) { if (argc != 1) { fwritef(1, "usage: history\n"); return; } int his = open(".history", O_RDONLY | O_CREAT); char ch; fwritef(1, "\x1b[33m-*- HISTORY -*-\x1b[0m\n"); fwritef(1, "\x1b[33m1\x1b[0m\t"); int cnt = 1; int fl = 0; while (read(his, &ch, 1) == 1) { if (fl) { fwritef(1, "\x1b[33m%d\x1b[0m\t", cnt); fl = 0; } fwritef(1, "%c", ch); if (ch == '\n') { cnt++; fl = 1; } } }
-
按键识别,键入上下键时,切换历史命令
-
经查阅资料,光标键对应的字符如下:
按键 常规模式 应用程序模式 ↑ ESC [ A ESC O A ↓ ESC [ B ESC O B -
步骤
- 获取缓冲区buf[]后三个字符
- 捕获到后,抵消光标移动操作
- 清空缓冲区,将光标移到左侧
- 打印出相应顺序的指令
-
代码分析
if (i >= 2 && buf[i - 2] == 27 && buf[i - 1] == 91 && buf[i] == 65)
{
writef("%c%c%c", 27, 91, 65);
for (i -= 2; i; --i)
writef("\b \b");
if (cmdi)
strcpy(buf, cmds[--cmdi]);
else
strcpy(buf, cmds[cmdi]);
writef("%s", buf);
i = strlen(buf) - 1;
}
else if (i >= 2 && buf[i - 2] == 27 && buf[i - 1] == 91 && buf[i] == 66)
{
if (cmdi < nn - 1)
{
for (i -= 2; i; --i)
writef("\b \b");
strcpy(buf, cmds[++cmdi]);
writef("%s", buf);
}
else
{
buf[i - 2] = buf[i - 1] = buf[i] = '\0';
}
i = strlen(buf) - 1;
}
Challenge部分
环境变量
在本部分,我与部分同学进行了进行了交流与探讨,最终结合助教的提示以及问题描述,得到了下面一种较为有效的解决方案。
-
首先我们需要自定一个新的结构体,来表示环境变量,其中需要有这些属性:环境变量拥有者的id、环境变量的名字 、环境变量的值、只读性、局部性、有效性,然后建立一个环境变量表,以达到多个程序使用的目的
struct Var { int envid; //环境变量拥有者的id char name[64]; //环境变量的名字 char val[128]; //环境变量的值 int readonly; //只读性 int local; //局部性 int ava; //有效性,为0代表还未生效,即不存在(狭义理解) };
-
新增系统调用,以实现对环境变量的增删改查
-
在
lib/syscall.S
:.word sys_get_var .word sys_set_var
-
user/syscall_lib.c
int syscall_get_var(u_int envid, char *name, char *val, int op) { return msyscall(SYS_get_var, envid, name, val, op, 0); } int syscall_set_var(u_int envid, char *name, char *val, int op, int perm) { return msyscall(SYS_set_var, envid, name, val, op, perm); }
-
增:
syscall_set_var(op=0)
——遍历环境变量表,找到第一个ava=0的Var,写入这个Var删:
syscall_set_var(op=1)
——遍历环境变量表,找到name对应的要删除的var,设置其ava=0改:
syscall_set_var(op=0)
——遍历环境变量表,找到name对应的要修改的var,改为参数中的这个var查:查询特定的有效的环境变量/全部有效的环境变量
- 特定的:
syscall_get_var(op=0)
——遍历环境变量表,找到所有ava=1的,且名字与查询条件相同的var - 全部的:
syscall_get_var(op=1)
——遍历环境变量表,找到所有ava=1的var
- 特定的:
-
具体代码实现,受限于篇幅,不完整贴上,需要注意的几点
-
设置set时的权限位 VAR_LOCAL , VAR_RDONLY 来控制访问权限,设置对应Var的local,readonly变量
-
新增了错误类型 E_VAR_NOT_FOUND , E_VAR_FULL , E_VAR_RDONLY
-
在进行后三者时,要注意额外判断当前维护变量是否为局部的以及其与当前shellid的关系,写一个简单的函数进行判断。
//shellid通过系统调用传入 int judge(u_int shellid, struct Var *var) { if (var->local == 0) { return 1; } else { if (shellid == var->envid) { return 1; } else { return 0; } } }
-
主要使用strcat,strcmp,strcpy进行字符串处理,比较。利用了比较傻的for循环。
-
然后比如说判断是否有对应name,构建了一个小函数,方便判断
int contain_name(char *name) { int i = 0; for (i = 0; i < NVARS; i++) { struct Var *v = &vars[i]; if (v->ava && strcmp(v->name, *name*) == 0) { return 1; } } return 0; }
-
注意权限位的设置
-
-
-
declare
-
在
umain
中,设置flag,以及permvoid umain(int argc, char **argv) { int i; ARGBEGIN { default: usage(); case 'r': flag[(u_char) ARGC()]++; case 'x': flag[(u_char) ARGC()]++; break; } ARGEND int perm = 0; if (flag['r']) { perm |= VAR_RDONLY; } if (flag['x']) { perm |= VAR_LOCAL; } char *arg2 = argv[1]; while(*arg2 == '=') { arg2++; } if (argc == 0) declare_nothing(); else if (argc == 1) set(argv[0], "", perm); else if (argc == 2) set(argv[0], arg2, perm); else writef(1, "too many args"); //writef("declare ok**"); }
-
在set函数中调用前述所写系统调用,实现declare,需要注意的是权限位的设定,以及传入参数
void set(char *name, char *value, int perm) { int r; int shellid = syscall_get_ShellId(syscall_get_ShellId(syscall_getenvid())); if ((r = syscall_set_var(shellid, name, value, 0, perm)) < 0) { if (r == -E_VAR_FULL) fwritef(1, "declare: vars are full\n", name); if (r == -E_VAR_RDONLY) fwritef(1, "declare: [%s] is readonly\n", name); return; } }
-
-
echo
在实现echo的时候,在umain解析的时候,需要对其进行修改
void umain(int argc, char **argv) { int i, nflag; nflag = 0; if (argc > 1 && strcmp(argv[1], "-n") == 0) { nflag = 1; argc--; argv++; } for (i = 1; i < argc; i++) { if (i > 1) write(1, " ", 1); if (argv[i][0] == '$') { char value[128]; syscall_env_var(&argv[i][1], value, 1); write(1, value, strlen(value)); } else { write(1, argv[i], strlen(argv[i])); } } if (!nflag) write(1, "\n", 1); }
-
unset
在
unset.c
中, 直接调用syscall_set_var 即可.
其他支持或者前置条件
(1)clear清屏
#define CLEAR() printf("\033[2J")
(2)彩色输出
定义在sh.h
中
用类似于\033[32m 实现绿色输出,类似可以实现其他颜色的输出。
用 \033[1m 可以实现加粗效果。
用 \033[m 表示颜色输出结束。
(3)cd命令
利用环境变量维护curpath(当前路径),利用系统调用可以获取和设置当前路径,并封装一套 API;
void curpath_init(int envid,char *path);
int curpath_get(int envid,char *path);
int curpath_set(int envid,char *path);
int curpath_get_parent(int envid,char *path);
当使用∶
-
cd.∶不改变路径
-
cd ..∶返回上一级目录(若为根目录,则不再返回)
-
cd folder ∶进入目录时
对所有的命令都重写,加入 curpath 的获取。如果传入的命令是一个以 /开头的路径,则为绝对路径;否则为相对路径。
int i, r;
int path[256];
int faid = syscall_get_ShellId(syscall_get_ShellId(syscall_getenvid()));
if (argc == 1)
{
fwritef(1, "cd: too few args\n");
return;
}
else
{
for (i = 1; i < argc; i++)
{
//显示当前目录
if (strcmp(argv[i], ".") == 0)
return;
//上一级目录
if (strcmp(argv[i], "..") == 0)
{
curpath_get_parent(faid, path);
curpath_set(faid, path);
fwritef(1, "cd(now_path): %s", path);
return;
}
else
//进入某一个目录
{
if ((r = curpath_get(faid, path)) < 0)
{
fwritef(1, "cd: get environment var failed");
return;
}
strcat(path, argv[i]);
int len = strlen(path);
if (path[len - 1] != '/')
strcat(path, "/");
struct Stat st;
r = stat(path, &st);
if (r == -E_VAR_NOT_FOUND)
fwritef(1, "cd: %s not found\n", path);
else if (r < 0)
fwritef(1, "cd: cannot cd %s\n", path);
else if (!st.st_isdir)
fwritef(1, "cd: %s is not directory\n", path);
else
{
if ((r = curpath_set(faid, path)) < 0)
fwritef(1, "Environment var not found");
fwritef(1, "curpath: %s", path);
}
return;
}
}
}
(4)rm命令
//类似于创建文件,有小改动
if ((r = remove(curpath)) < 0)
{
fwritef(1, "File %s Not Exists!\n", curpath);
return;
}
(5)open修改
在open()中,首先需要加入APPEND,MKDIR,CREAT代码。
#define O_APPEND 0x1000
user/file.c
中的open()
中加入O_APPEND打开方式扩充代码
if ((mode & O_APPEND) != 0) {
ffd->f_fd.fd_offset = size;
}
fs/serv.c
中的serve_open()
中加入CREAT, MKDIR打开方式扩充.
if ((r = file_open((char *) path, &f)) < 0) {
if (r == -E_NOT_FOUND && (rq->req_omode & O_CREAT)) {
if ((r = file_create((char *)path, &f)) < 0) {
return;
}
f->f_type = FTYPE_REG;
} else if (r == -E_NOT_FOUND && (rq->req_omode & O_MKDIR)) {
if ((r = file_create((char *)path, &f)) < 0) {
return;
}
f->f_type = FTYPE_DIR;
} else {
ipc_send(envid, r, 0, 0);
return;
}
}
(6)环境变量的获取,当前地址的操作
void curpath_init(int envid, char *path) {
int r;
// set op 0 declare with perm
if ((r = syscall_set_var(envid, CURPATH_KEY, path, 0, 0)) < 0) user_panic("Init curpath failed: %u.", r);
return ;
}
int curpath_get(int envid, char *path) {
int r;
//op1 get one
//writef("get from env : %d", envid);
if ((r = syscall_get_var(envid, CURPATH_KEY, path, 1)) < 0) return r;
return 0;
}
int curpath_set(int envid, char *path) {
int r;
// op0 declare with perm
if ((r = syscall_set_var(envid, CURPATH_KEY, path, 0, 0)) < 0) return r;
return r;
}
int curpath_get_parent(int envid, char *path) {
int r, i;
if ((r = curpath_get(envid, path)) < 0) return r;
if (strlen(path) == 1) return 0;
for (i = strlen(path) - 2; path[i - 1] != '/'; i--);
path[i] = 0;
return r;
}
实验难点及总结
在本实验中,我通过编写新函数,新的方法实现一些小组件以及附加功能,丰富了我们的 shell的功能,包括"后台运行”,“一行多命令”,“简单引号支持”,“tree /mkdir /touch 命令”,“清屏”,“彩色输出”,“历史命令”,“环境变量”,“cd 命令"等任务。
我认为实验的主要难点在于
- 修改若干的指令,使之成为内置的指令
- 在历史命令中,如何实现识别键位(通过查阅资料得知),如何实现历史命令保存,输出
- 以及在challenge部分的环境变量的实现(新定义结构体,并且书写相应的系统调用,以及增加相应的open()参数,错误码,并加以合理运用)
- 一些字符串处理方法的使用,以及各个细节的注意以及逐个击破。
终于度过了难捱的一学期学习,在做完了lab6挑战性任务以后,我对os的学习体会有了进一步的升华,也对课程内容有了更深入的理解,感谢os课程组对我们的磨练以及提升!