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.cruncmd()定义一个变量 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部分

历史命令功能

  1. 首先创建读取/写入历史文件 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);
    }
    
  2. 将每一个输入执行的指令,利用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;
    }
    
  3. 通过编写一个用户态程序 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;
    		}
    	}
    }
    
  4. 按键识别,键入上下键时,切换历史命令

  • 经查阅资料,光标键对应的字符如下:

    按键 常规模式 应用程序模式
    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部分

环境变量

在本部分,我与部分同学进行了进行了交流与探讨,最终结合助教的提示以及问题描述,得到了下面一种较为有效的解决方案。

  1. 首先我们需要自定一个新的结构体,来表示环境变量,其中需要有这些属性:环境变量拥有者的id、环境变量的名字 、环境变量的值、只读性、局部性、有效性,然后建立一个环境变量表,以达到多个程序使用的目的

    struct Var {
    	int envid;  //环境变量拥有者的id
       	char name[64]; //环境变量的名字 
    	char val[128]; //环境变量的值
    	int readonly; //只读性
    	int local; //局部性
    	int ava; //有效性,为0代表还未生效,即不存在(狭义理解)
    }; 
    
  2. 新增系统调用,以实现对环境变量的增删改查

    • 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; 
        }
        
      • 注意权限位的设置

  3. declare

    • umain中,设置flag,以及perm

      void
      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;
          }
      }
      
  4. 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);
    }
    
  5. 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课程组对我们的磨练以及提升!