OS实训1——Minix3 shell的简单实现

shell实现要求

Shell主体为一个while循环,对用户键盘输入命令进行操作并给出反馈。
该实验中,我们主要实现以下几个命令:

  1. cd:shell也是一个程序,启动时迷你型会给它分配一个当前工作目录,利用chdir系统调用可以移动Shell的工作目录。
  2. history n:保存每次输入的命令,打印最近n条命令。
  3. exit:退出Shell的while循环,结束Shell的main函数。
  4. mytop:参考Minix终端输入top命令的输出信息,最终输出总体内存大小、空闲内存大小、缓存大小和总体CPU使用占比。
  5. 后台运行:对末尾包含&参数的命令,通过将子进程的标准输入、输出映射成 /dev/null来屏蔽键盘和控制台,并调用signal(SIGCHLD,SIG_IGN),使minix接管此进程,shell不等待进程结束,直接返回。
  6. 重定向:利用dup2函数,将某个打开文件的文件描述符fd映射到标准输入or输出,实现覆盖写“>”、追加写“>>”和文件输入“<”。
  7. 管道:利用fork及dup2实现进程间同步。

shell实现代码

对输入命令,Shell先将其分解成单词序列,再根据命令名称分为两类分别处理。
一类是shell内置命令(如cd,history,exit),识别后,执行对应操作。
另一类是program命令(如/bin/ 目录下的ls,grep ),识别后,创建一个新进程执行命令,并等待进程结束。

参数设置

#define M 256
#define SIZE 256
  	char path[M], input[M], *command[M], *temp, history[M][M];		//save path, input command, processes command, a temporary char, historical command 
    int i, j, n_com, n_his = 0;		//n_com: the number of blocks of a command, n_his:the number of all commands

    int k, h, flag_back, flag_out, flag_add, flag_in, flag_pipe, pid, pid2, status, status2, fd1, fd2;		//flag: 分别标识后台运行、覆盖写、追加写、输入、管道状态 
    char file_out[M], file_in[M], *command2[M];

   char buffer[SIZE] = "", *pagesize, *total, *freepage, *cached, *temp1;        //*pagesize不要赋为pagesize[SIZE],否则后期无法用 atoi()转为数字 
   int count = 0, fd;

Shell主体

int main(int argc, char *argv[]) {
	 while (1) {
	       //get path 
	       getcwd(path, sizeof(path) - 1);
	       printf("shell:%s%% ", path);
	
	       //initialize parameter
	       memset(input, 0, sizeof(input));
	       memset(command, 0, sizeof(command));
	       n_com = 0;
	       flag_back = 0;
	       flag_out = 0;
	       flag_add = 0;
	       flag_in = 0;
	       flag_pipe = 0;
	       memset(command2, 0, sizeof(command2));
	
	       gets(input);        //get command
	
	       /* save command 
	* ——!!! The code should follow "gets(input)", or we can only get command[0] (it was cut by "strtok").  
	        */
	       for (i = 0; i < M; ++i) {
	           history[n_his][i] = input[i];            //notice:history[0] can't be empty
	      }
	       ++n_his;
	
	       //break command to get parameter 
	       temp = strtok(input, " ");        //char *strtok(char *s, char *delim):以delim中的字符为分界符,将s切分成一个个子串
	       while (temp != NULL) {
	           command[n_com] = temp;
	           n_com++;
	           temp = strtok(NULL, " ");     //将strtok第一个参数赋为空值NULL,继续分解字符串
	      }
	      /* 命令相关操作 */
      }
      return 0}

该部分是在进行解析命令之前做的准备工作,将输入的字符串命令分割成块并依次存入command中。在此之前,注意要将输入的命令存入history数组中作为命令的记录。

三个较简单命令

//parse the command
if (!strcmp(command[0], "cd")) {
	if (n_com != 1) {
        chdir(command[1]);
    }
} else if (!strcmp(command[0], "exit")) {
    if (n_com == 1) {
        break;
    }
} else if (!strcmp(input, "\000")) {
    continue;
} 

如上实现的是对cd、exit和无输入情况的实现,其中对n_com != 1进行判断以确保命令只有1块。

history n

else if (!strcmp(command[0], "history")) {
   if (n_com != 1) {
       int t = atoi(command[1]);            // notice: command[1] is a string, not a char! 

       for (j = n_his - t; j < n_his; ++j) {
           printf("%d ", j + 1);
           puts(history[j]);
      }
  }
}

该命令接上一部分的if-else语句,因为command[1]是字符串,故需用atoi()函数来确定需要的最近n条命令,并用for循环将其打印到屏幕。

mytop

else if (!strcmp(command[0], "mytop")) {
	if (n_com == 1) {
    // method 1: 内存 
		int memory_sum=0,memory_free=0,cache_size=0;
		
     	fd = open("/proc/meminfo",O_RDONLY);
        count=read(fd,buffer,SIZE);
         
        temp1=strtok(buffer," ");
        pagesize=temp1;
        temp1=strtok(NULL," ");
        total=temp1;
        temp1=strtok(NULL," ");
        freepage=temp1;
        temp1=strtok(NULL," ");
        temp1=strtok(NULL," ");
        cached=temp1;
        close(fd);
        
        memory_sum=atoi(pagesize)*atoi(total)*1.0/1024;
        memory_free=atoi(pagesize)*atoi(freepage)/1024;
        cache_size=atoi(pagesize)*atoi(cached)/1024;
        printf("main memory: %dK total, %dK free, %dK cached\n",memory_sum,memory_free,cache_size);
          }
}

这是一种输出总体内存大小、空闲内存大小和缓存大小的方法。

在/proc/meminfo中,可查看内存信息。文件中的每个参数对应含义依次是页面大小pagesize, 总页数量total , 空闲页数量free ,最大页数量largest ,缓存页数量cached 。
计算内存大小公式: (pagesize * total)/1024

输出结果为:
在这里插入图片描述
*
接下来第二种方法,是参考top源码的代码实现的,有兴趣的话可以在此处https://github.com/0xffea/MINIX3/blob/master/usr.bin/top/top.c查看top源码

else if (!strcmp(command[0], "mytop")) {
   if (n_com == 1) {
       //method 2 ——reference source of top
       int r, c, s = 0;
       int cputimemode = 1;    /* bitmap. */

       if (chdir(_PATH_PROC) != 0) {
           perror("chdir to " _PATH_PROC);
           return 1;
      }

       system_hz = (u32_t) sysconf(_SC_CLK_TCK);

       getkinfo();

       while ((c = getopt(argc, argv, "s:B")) != EOF) {
           switch (c) {
               case 's':
                   s = atoi(optarg);
                   break;
               case 'B':
                   blockedverbose = 1;
                   break;
               default:
                   fprintf(stderr,
                           "Usage: %s [-s<secdelay>] [-B]\n",
                           argv[0]);
                   return 1;
          }
      }

       if (s < 1)
           s = 2;

       /* Catch window size changes so display is updated properly
                * right away.
                */
       signal(SIGWINCH, sigwinch);


       /* while循环控制无限动态刷新,直到 Ctrl-C 
		* 或用for循环控制刷新次数,便于退出回到shell 
		*/
       for (j=0;j<10;j++){
//               while (1) { 
           slot = 0;            // notice: slot should be reset before each dynamic refresh ——注意每次动态刷新都要重置为0 

           //控制动态更新间隔所需参数
           fd_set fds; 
           int ns; 
           struct timeval tv; 

           showtop(cputimemode, r); //打印结果 
           tv.tv_sec = s;
           tv.tv_usec = 0;

           FD_ZERO(&fds);
           FD_SET(STDIN_FILENO, &fds);

           if ((ns = select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv)) < 0 //设置动态更新间隔sleep 
               && errno != EINTR) {
               perror("select");
               sleep(1);
          }

           if (ns > 0 && FD_ISSET(STDIN_FILENO, &fds)) {
               char c;
               if (read(STDIN_FILENO, &c, 1) == 1) {
                   switch (c) {
                       case 'q':
                           putchar('\r');
                           return 0;
                           break;
                       case 'o':
                           order++;
                           if (order > ORDER_HIGHEST)
                               order = 0;
                           break;
                       case TIMECYCLEKEY:
                           cputimemode++;
                           if (cputimemode >= (1L << CPUTIMENAMES))
                               cputimemode = 1;
                           break;
                  }
              }
          }
      }
  }

其中,用while或for循环控制其动态更新,而此时若不注意重置slot的话,则会导致slot最终大于nr_total,使屏幕一直输出:“top: unreasonable endpoint number xxx”。

后台运行&

if (!strcmp(command[n_com - 1], "&")) {
   flag_back = 1;
}
if ((pid = fork()) < 0) {
   printf("fork error\n");
   return -1;
}
if (pid == 0) {
   if (flag_back) {
//    fd1=open("/dev/null",O_RDWR | O_CREAT | O_TRUNC, 0644);
       fd1 = open("/dev/null", O_RDONLY);
       dup2(fd1, 0);
       dup2(fd1, 1);
       dup2(fd1, 2);
       signal(SIGCHLD, SIG_IGN);
  }
}
else {
   if (flag_back) {
       printf("[process id %d]\n", pid);        //若为后台程序,则输出进程号
       continue;
  } else {
       if (waitpid(pid, &status, 0) == -1) {

      }
  }
}

在该实验中,若为&后台运行命令,则&必定出现在输入命令的最后一部分,故检验command[n_com-1]位来确定是否为&命令,以标识flag_back的状态。

之后,需fork进程,并在子进程中将标准输入、输出、错误都重定向到“/dev/null”,最后用 signal(SIGCHLD, SIG_IGN)来使Minix接管此进程。

最后,在父进程中附加代码printf("[process id %d]\n", pid),使得有后台进程时,输出其进程号。

重定向、管道

状态确定:

for (k = 0; k < n_com; k++) {
   if (!strcmp(command[k], ">")) {
       flag_out = 1;
       strcpy(file_out, command[k + 1]); 	//">" 后面即使重定向指向的文件 
//     for (h = k; h < n_com - 2; h++) { 		//在该实验中不需要
//         command[h] = command[h + 2];
//     }
       command[n_com - 2] = NULL;
       n_com -= 2;
       k--;
  } else if (!strcmp(command[k], ">>")) {
       flag_add = 1;
       strcpy(file_out, command[k + 1]); 
       command[n_com - 2] = NULL;
       n_com -= 2;
       k--;
  } else if (!strcmp(command[k], "<")) {
       flag_in = 1;
       strcpy(file_in, command[k + 1]); 
       command[n_com - 2] = NULL;
       n_com -= 2;
       k--;
  } else if (!strcmp(command[k], "|")) {
       flag_pipe = 1;
       for (h = 0; h < k; h++) {
           command2[h] = command[h]; 	//将 "|" 之前的命令移至command2中 
      }
       command2[k] = NULL;
       for (h = 0; h < n_com - k - 1; h++) {
           command[h] = command[h + k + 1]; 	//重置command保存 "|" 之后的命令
      }
       command[n_com - k - 1] = NULL;
       break;
  }

此处,由于这些命令的标识符可能出现在输入命令中的任意一处,故需用一个for循环来确认其位置,而后设置它们的状态flag_xxx为1。而标识符“>”、“>>”、“<”之后一位则是重定向指向的文件,故将它们赋为file_out、file_in的值,便于之后重定向操作处理。

对管道的操作稍复杂,要将“|”之前的命令移到command[2]中保存备用,再更新command数组存“|”之后的命令备用。

命令执行:

if ((pid = fork()) < 0) {
   printf("fork error\n");
   return -1;
}
if (pid == 0) {
   if (flag_out)            // >
  {
       fd1 = open(file_out, O_RDWR | O_CREAT | O_TRUNC, 0644);
       if (fd1 < 0) {
           printf("open error!\n");
           return 0;
      }
       dup2(fd1, 1);
  }
   if (flag_add)        // >> : O_APPEND
  {
       fd1 = open(file_out, O_RDWR | O_CREAT | O_APPEND, 0644);
       if (fd1 < 0) {
           printf("open error!\n");
           return 0;
      }
       dup2(fd1, 1);
  }
   if (flag_in)            // <
  {
       fd1 = open(file_in, O_RDONLY);
       //                   fd1 = open(file_in,O_WRONLY|O_CREAT|O_TRUNC, 0644);
       if (fd1 < 0) {
           printf("open error!\n");
           return 0;
      }
       dup2(fd1, 0);
  }
   if (flag_pipe)            // |
  {
       if ((pid2 = fork()) < 0) {
           printf("fork2 error\n");
           return -1;
      } else if (pid2 == 0)        //子子进程中以写方式打开pipe 
      {
           fd2 = open("/tmp/tempfile", O_WRONLY | O_CREAT | O_TRUNC, 0644);        //新建一个临时文件,后删除 
           dup2(fd2, 1);
           execvp(command2[0], command2);
           exit(0);
      }
       if (waitpid(pid2, &status2, 0) == -1)        //等待子子进程结束以回收 
      {

      }
       fd2 = open("/tmp/tempfile", O_RDONLY);        //子进程中以读方式打开管道 
       dup2(fd2, 0);
  }
   execvp(command[0], command);        //执行命令
   remove("/tmp/tempfile");
   exit(0);
} else {
   if (waitpid(pid, &status, 0) == -1) {

  }
}

同&命令一样,fork一个进程(它们和&在同一个fork的进程中,只不过此处省略了&的相关操作)。根据各flag_xxx的状态来确定要执行哪一个操作。

在重定向中,用fd1 = open(file_xxx, XXX);打开所需文件,在“>”中需用到可读可写O_RDWR | O_CREAT | O_TRUNC, 0644 ,在“>>”中需将O_TRUNC 改为O_APPEND 追加写模式,而“<”则用只读O_RDONLY。之后用dup2()函数分别重定向它们的标准输入、输出即可。

在管道中,由于涉及到“|”前后两个文件的协调控制问题,故需在子进程中再fork一个进程,在子子进程中重定向前一个文件的标准输出,让其写入一个新建的临时文件“/tempfile”中。而后,等待该子子进程结束并将其回收后,在重定向后一个文件的标注输入,将前一个文件写入临时文件“/tempfile”中的内容传给后一个文件,以达到管道的控制目的。

在对以上操作定义完后,用execvp(command[0], command)将命令执行,且对管道命令,还要讲中途新建的文件“/tempfile”删除,执行完成后退出子进程。最后在父进程中等待子进程结束并将其回收。

至此,shell的全部内容已完成。

实验结果

  1. cd
    在这里插入图片描述
  2. ls –a –l
    在这里插入图片描述
  3. ls –a –l > result.txt
    在这里插入图片描述
  4. vi result.txt
    在这里插入图片描述
  5. grep a < text.txt (事先ls –a –l > text.txt)
    在这里插入图片描述
  6. ls –a –l | grep a
    在这里插入图片描述
  7. vi result.txt &
    在这里插入图片描述
  8. history 9
    在这里插入图片描述
  9. mytop (实际运行时是动态刷新的)
    在这里插入图片描述
  10. exit
    在这里插入图片描述
  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值