shell实现

相对路径实现

  • 在进程控制块中添加工作目录域char env_work_dir[1024];,并通过以下两个系统调用进行读取与修改

    1
    2
    3
    int syscall_read_work_dir(void * dstva);
    int syscall_write_work_dir(void* srcva);
    // 工作目录初值为"/",并且fork出子进程继承父进程工作目录
  • 实现相对路径与绝对路径的转换函数void walk_realtive_path(char* work_dir, char* cd_arg, char* ans); 无论传入的是相对路径还是绝对路径最终都转换为绝对路径输出

    传入当前工作目录(系统调用获得), 传入的路径参数,最终转换为的绝对路径写入ans中;

    (一个难写的字符串替换函数)

  • 实现open的相对路径支持

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int open(const char *path_0, int mode) {

    char path[1024]; // absolute path
    char cur_work_dir[1024];
    char path_00[1024];
    strcpy(path_00,path_0);
    syscall_read_work_dir(cur_work_dir);
    walk_realtive_path(cur_work_dir, path_00, path);
    //...
    }
  • int cd(int argc, char **argv);函数中实现内建指令cd的功能,其在main函数中被调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    ...
    int r = open(argv[1], O_RDONLY); // 尝试打开对应文件或者目录
    if(r < 0){ // 如果文件或者目录不存在,直接返回
    printf("cd: The directory '%s' does not exist\n", argv[1]);
    return 1;
    }

    char work_dir[1024] ;
    syscall_read_work_dir(work_dir);
    walk_realtive_path(work_dir, argv[1], ans); // 相对路径转换为绝对路径
    struct Stat st;
    stat(ans, &st);
    if(st.st_isdir){ // 判断是否为目录
    syscall_write_work_dir(ans);
    close(r);
    // printf("the new dir is %s\n", ans);
    return 0;
    }
    ...
  • int pwd(int argc, char **argv);中实现pwd内建指令的功能;但是最后为了顺利完成第四题,我最后将其改为了一般指令

更多指令实现

  • mkdir指令实现

    • O_MKDIR文件打开模式的实现

      1
      2
      3
      4
      5
      6
      //在fs/serve.c/serve_open函数中
      if ((rq->req_omode & O_MKDIR) && (r = file_create(rq->req_path,FTYPE_DIR ,&f)) < 0 &&
      r != -E_FILE_EXISTS) {
      ipc_send(envid, r, 0, 0);
      return;
      }
    • 如果没有-p参数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      if((fd = open(path, O_RDONLY)) >= 0){  //若存在直接返回
      close(fd);
      user_panic("mkdir: cannot create directory '%s': File exists\n", path);
      return -1;
      }
      fd = open(path, O_MKDIR); //以mkdir形式打开文件
      if (fd == -10) {
      user_panic("mkdir: cannot create directory '%s': No such file or directory\n", path);
      } else if (fd < 0) {
      user_panic("other error when mkdir %s, error code is %d\n", path, fd);
      } else {
      close(fd);
      }
    • 如果存在-p参数

      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
      if((fd = open(path, O_RDONLY)) >= 0){
      close(fd);
      return;
      }

      int i = 0;
      char str[1024];
      for (; path[i] != '\0'; ++i) { //尝试打开或者创建逐级目录
      if (path[i] == '/') {
      str[i] = '\0';
      fd = open(str, O_MKDIR);
      if (fd >= 0) {
      close(fd);
      } else {
      user_panic("other error when mkdir %s, error code is %d\n", path, fd);
      }
      }
      str[i] = path[i];
      }
      str[i] = '\0';
      fd = open(str, O_MKDIR);
      if (fd >= 0) {
      close(fd);
      } else {
      user_panic("other error when mkdir %s, error code is %d\n", path, fd);
      }
  • touch指令实现;

    1
    fd = open(path, O_CREAT);   
  • rm指令的实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    void rm(char *path){
    int fd;
    // case1 : no exist
    if((fd = open(path, O_RDONLY)) < 0){
    if(f_mode == 0){
    user_panic("rm: cannot remove '%s': No such file or directory\n", path);
    }
    return ;
    }
    close(fd);

    struct Stat st;
    stat(path, &st);
    if(st.st_isdir && r_mode == 0){
    user_panic("rm: cannot remove '%s': Is a directory\n", path);
    }

    remove(path);
    }

环境变量管理

  • env.h中定义环境变量结构体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct Var{
    char name[VAR_NAME_LEN]; // 环境变量名
    char value[VAR_VALUE_LEN]; // 环境变量值

    unsigned int is_global; // 1=全局变量(可以被子shell继承), 0=局部变量
    unsigned int is_readOnly; // 1=只读变量,不可以被重新赋值或者修改

    u_int shell_id; // 拥有该环境变量的shell的id

    int valid; // 为1代表有效
    };

我采用数组存储所有环境变量

1
2
3
struct Var var_table[VAR_NUM];   // 环境变量存储表
static int var_num = 0; // 当前所有进程存在的var个数
static int shell_num = 0; // 当前出现过的shell的进程个数 --- 主要用于为每个进程创建新的shell_id使用
  • 实现以下5个系统调用

    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
    /**
    * 在shell进程启动时候调用,继承父shell的全局变量
    * 返回当前shell的shell_id
    */
    int syscall_init_curenv_var(){
    return init_curenv_var();
    }

    /**
    * 修改(若存在且非只读)或者创建单个环境变量
    */
    int syscall_declare_write(int shell_id, void * name, void *value, int is_global, int is_readOnly){
    return declare_write(shell_id, name, value, is_global, is_readOnly);
    }

    /**
    * 删除环境变量(检查只读权限);-1:只读不可删 或者 0:正常删除
    */
    int syscall_declare_unset(int shell_id, void*name){
    return declare_unset(shell_id, name);
    }

    /**
    * 查询环境变量name的值,将其写入value
    */
    int syscall_declare_get(int shell_id, void*name, void *value){
    return declare_get(shell_id, name, value);
    }

    /**
    * 获取当前shell的所有变量到var_array中
    * 返回当前shell的环境变量个数
    */

    int syscall_delcare_getall(int shell_id, char var_array[1024][50]){
    return delcare_getall(shell_id, var_array);
    }
  • sh.c中的调用

    • main函数一开始便调用int syscall_init_curenv_var();并将返回的当前进程的shell_id保存下来

    • void expand_variables(char *arg);该函数主要对于parse_cmd解析得到的每个参数进行检查,如果引用了环境变量则进行替换

    • int declare_cmd(int argc, char **argv) 在该函数中实现declare指令的功能:环境变量添加或者修改或者打印输出所有环境变量;调用方式与cd相同

    • int unset_cmd(int argc, char **argv); unset内置指令的实现函数,调用方式同上。

输入指令优化

输入指令优化主要是包括上下左右键、BackSpace键,ctrl快捷键等

主要是在readline中实现

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
...
read(0,&temp,1);
switch(temp) {
case '\r':
case '\n': //回车处理
save_command(buf); // 将当前指令保存入.mos_history中
break;
case '\b': //'\b'与0x7f均为BackSpace键
case 0x7f:
if (i <= 0) { break; }
for(int j = --i; j <= len - 1;j++){
buf[j] = buf[j + 1]; // 移动删除buf中对应的字符
}
buf[--len] = 0;
printf("\033[%dD%s \033[%dD", (i + 1), buf, (len - i + 1)); //重新打印输出
break;
case '\033': //上下左右键
if(read(0, &temp, 1) != 1){
printf("read arrow error\n");
break;
}
if (temp == '['){
read(0, &temp, 1);
if (temp == 'C') { // right arrow
if (i < len) { // have space for cursor to move right
i += 1;
} else {
printf("\033[D");
}
}
//...
}

}

历史指令以及上下回显

  • 实现文件的追加模式打开

    1
    2
    3
    4
    if(o->o_mode & O_APPEND){
    struct Fd *fff = (struct Fd*) ff;
    fff->fd_offset = f->f_size; // 修改追加模式打开文件的指针位置到文件末尾
    }
  • 指令保存:在readline中读取到回车键时候则当前指令输入完毕,需要将当前指令保存到.mos_history

    以追加模式打开,每次都将新指令追加写到最后,并在其中补充’\n’;利用换行符分隔各条指令

  • history指令实现

    history指令中默认只保存并输出20条历史指令,但是我们的实现方式中是保存了所有指令;

    sh.c中维护全局静态变量static int hiscount, curline;hiscount存储已经出现保存了多少条历史指令,curline代表当前缓冲区所处理指令是第几条指令;

    通过hiscount-20可以知道从第几条指令开始是最后20条指令,输出即可;

    读取到一个回车代表读取了一条指令

  • 上下键指令回显,以上箭头为例

    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
                  } else if (temp == 'A') { // up arrow
    printf("\033[B");
    // 如果存在上一条指令
    if(curline != 0 && curline > hiscount - 20){
    buf[len] = '\0';
    //如果缓冲显示区显示的是正在输入的行,则将其保存到curIn里面
    if(curline == hiscount) {
    strcpy(curIn, buf);
    }

    // 清空当前行
    if (i != 0) { printf("\033[%dD", i); }
    for(int j = 0; j < len; j++) { printf(" ");}
    if (len != 0) { printf("\033[%dD", len); }

    // 读取curline上一行指令到buf里面并打印输出
    if((r = readHistoryLine(--curline, buf)) < 0) {
    printf("can't read targetLine\n");
    break;
    }
    printf("%s", buf);
    i = strlen(buf);
    len = strlen(buf);
    }
    }

追加重定向、条件执行、反引号

追加重定向

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 append_redir :
if (gettoken(0, &t) != 'w') {
debugf("syntax error: >> not followed by word\n");
exit();
}
if((fd = open(t, O_RDONLY)) < 0){
fd = open(t, O_CREAT);
if(fd < 0){
debugf("failed to open '%s'\n", t);
exit();
}
}
close(fd);
// 重新以追加模式打开
fd = open(t, O_APPEND | O_WRONLY);
if(fd < 0){
debugf("failed to open '%s'\n", t);
exit();
}
// 此处是否需要判断dup不成功
dup(fd, 1);
close(fd);
break;

反引号

其主要通过以下两个函数实现主体功能:

  • int replaceBackquoteCommands(char* cmd);处理cmd字符串中的反引号,其通过递归查找反引号组,利用下面函数运行反应号内指令,并用输出结果替换原来的反引号
  • int executeCommandAndCaptureOutput(char *cmd, char *output, int maxLen);执行cmd对应的指令,并将指令运行结果保存到output

sh.c中的main函数对读取的字符串调用replaceBackquoteCommands()函数

条件执行

  • parsecmd中解析到||或者&&时候,fork出一个子进程先执行第一部分指令

  • fork出来的子进程通过spawn装载并运行,并在进程结束前将指令运行结果通过ipc发送到fork出来子进程

  • fork子进程根据当前指令是否为条件指令选择是否发送ipc消息到父进程;如果是条件执行,则将运行结果发送到父进程

  • 父进程根据子进程的ipc消息判断是否需要执行后续指令

    此处需要判断后续是否会出现连续的&&或者||

    在user_panic与libmain中实现指令运行结果向shell发送

实验难点

  • 在实现指令优化时候,对于屏幕输出管理较为困难;需要了解ESC转义序列等,且需要确保屏幕输出缓冲区的完全清空与输出
  • 在反引号处理与条件执行时候,涉及到新开子进程;对于新开的子进程,实现进程间消息通信与输入输出重定向较为复杂;需要对dup函数理解到位,避免控制台输入输出不正确

实验体会

这次实验不仅加深我对shell的实现原理的理解,更重要的是回顾复习从文件系统到进程管理,以及终端控制的相关知识,每个模块的实现都加深了我对操作系统各组件协同工作的理解,提升了对复杂系统问题的分析能力和处理能力。