文章 35
评论 44
浏览 111155
杭电操作系统实验三模拟shell

杭电操作系统实验三模拟shell

实验三:linux进程管理

1)实现一个模拟的shell

问题:编写三个cmd1.ccmd2.ccmd3.c程序,功能自定。需要一个仿shell的程序和实现类似执行grepfind等命令的程序。通过在仿shell中输入命令(字符串和参数),创建子进程来执行相应的命令程序,父进程需要等待子进程处理结束后,才能再去查看是否有新的命令。输入“exit”退出shell程序。输入无效命令则显示“Command not found”,等待下一个新命令。

前说下,刚开始把问题看复杂了,除了要求之外还写了cdpwdexitmkdir,所以可以自行删改(不麻烦),或者有兴趣可以加更多的命令玩玩 : )

处理方法:

myshell.c:

  • 提供循环输入命令模块
  • 解析命令
  • 判断命令是内置命令还是外置命令(可以不用)
  • 内置命令直接调用函数,外置命令用fork()来创建一个子进程, 在进程中执行命令程序采用exec族的函数来调用函数执行。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdarg.h>
#include <sys/syscall.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#define MAX_BUF 80
#define MAX_CMD 80
#define MAX_ARG_NR 16
int cmd_len;                    //输入的命令长度(包含换行符)
//////////////////////////////////////////////////////////////////////////
int main()
{
    char buf[MAX_BUF];
    char command[MAX_CMD+1];
    system("clear");
    printf("Songshell: support cd/pwd/mkdir/exit/find/grep\n");
    while(1){
        getcwd(buf, sizeof(buf));
        printf("➜ [%s] ", buf);
        fflush(stdout);         //输出上一行内容
        memset(command,0, sizeof(command));
        if((cmd_len = read(0, command, MAX_CMD)) < 2)   //仅有回车,cmd_len=1
            printf("Command Error! Pleace Try Again\n");
        else{
            ToProcess(command);
        }
    }
    return 0;
}

ssize_t read(int fd, void *buf, size_t count);

read(0, command, MAX_CMD))从标准输入文件中读取最大长度为MAX_CMD并带有回车的字符串写入command,当除了回车还有其他字符时,调用ToProcess(command)

//执行命令
int ToProcess(const char *command)
{
    char *argv[MAX_CMD];
    char buf[MAX_CMD];
    strcpy(buf, command);
    buf[cmd_len-1] = ' ';
    buf[cmd_len] = '\n'; 
    ParsePalam(buf, argv);
    if(argv[0] == NULL)     		//命令为空
        return 0;
    if(!JudgeBuildin(argv)){
        //不是内置命令
        pid_t child = fork();
        if(child > 0){
            int status = 0;
            wait(&status);
        }
        else if(child == 0){         //子进程
            if(!strcmp(argv[0], "mkdir"))
                execl("/home/exp3/myshell/mkdir_cmd", argv[1], NULL);
            else if(!strcmp(argv[0], "command1"))
                execl("/home/exp3/myshell/command1", argv[0], NULL);
            else if(!strcmp(argv[0], "command2"))
                execl("/home/exp3/myshell/command2", argv[0], NULL);
            else if(!strcmp(argv[0], "command3"))
                execl("/home/exp3/myshell/command3", argv[0], NULL);
            else if(!strcmp(argv[0], "find"))
                execv("/usr/bin/find",argv);
            else if(!strcmp(argv[0], "grep"))
                execv("/usr/bin/grep", argv);
        }
        else if(child == -1){
            printf("failed: no child process is created!\n");
        }
    }
    return 0;
}

P7-8:为了让后面ParsePalam()函数更好解析输入的命令参数。在最后一个参数的后面添加空格, 和其他参数结构保持一致(这里不加,解析会出错)。

P9:解析完参数,包括命令及参数都按输入顺序存入char *argv[]指针数组中,调用形式为argv[0],argv[1]..

P12:一般而言,在一个脚本里执行一个外部命令(普通的可执行文件)时,shell 会 fork 出一个子进程,然后再用 exec族函数来执行这个程序;但是,bash shell 的内置命令(builtin)却不会这样,它们是直接执行的。所以,等价的内置命令的执行速度会比执行外部命令要来的快。

可以通过type或者which命令来查看命令类型:提到builtin就是内置,打印出目录如/usr/bin/xxxx这样的就是外置命令

image-20201203170855621

如果不是内置命令就要涉及创建子进程和执行外部程序的实现了

通过终端输入man 2 fork查看fork的编程手册。

P15:child>0,返回的是子进程的PID号,说明当前是父进程,按要求,父进程不需要做什么事,等着就完事了。man 2 wait,看下头文件,函数用最简单的wait(int *wstatus)

P19:child == 0,说明是子进程,于是要调用外部程序了,man exec查看exec族的相关信息。exec族博客

image-20201203164609486

拿P21的execl("/home/exp3/myshell/mkdir_cmd", argv[1], NULL);举例,execl的参数依次是,调用程序的路径名, 参数0,参数1,.....,NULL结尾。这里的argv[1]就是要创建的目录名

mkdir_cmd.c

#include <unistd.h>
#include <sys/stat.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
    if(mkdir(argv[0], 0755) == -1){
        printf("mkdir failed\n");
    }
    return 0;
}

提醒,main参数一定别忘记了argc,血泪教训,参数就是顺序写入到argv中,我只传入了一个参数。

P29:execv("/usr/bin/find",argv);,execv不像execl要一个一个传参数,而是直接提供指针数组就行了。因为find命令可以带很多参数就用这个了

P33:child == -1fork失败

注意:因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。所以创建子进程来提供exec执行命令程序的空间。(如下图可以看到,execl调用成功后不会打印success)

image-20201203170427313

image-20201203170401160

//解析命令及参数
int ParsePalam(char *buf, char** argv)
{
    int argv_idx, argc;
    argv_idx = argc = 0;
    while(argv_idx < MAX_ARG_NR)    //初始化
        argv[argv_idx++] = NULL;
    while(1){
        while(*buf == ' ')          //跳过空格
            buf++;
        if(*buf == '\n')            //碰到回车循环结束
            break;
        argv[argc] = buf;           //记录参数首地址
        while(*buf != ' ' && *buf)  //确定参数结束地址
            buf++;
        *buf++ = '\0';              //添加字符串结束符
        if(argc > MAX_ARG_NR){      //超过最大参数数量error
            printf("argc is too large!\n");
            return -1;
        }
        argc++;
    }
    return argc;    //返回参数数量
}

解析参数的方法是用《操作系统真相还原》里的方法。

主要思路是argv[i]记录第i个参数的首地址,并把参数后面的空格换成结束符

//判断是否是内置命令
int JudgeBuildin(char **argv)
{
    if(!strcmp(argv[0], "cd")){
        if(chdir(argv[1]))      //return -1 means error
            printf("dir isn't exist\n");
        return 1;
    }
    else if(!strcmp(argv[0], "pwd")){
        char buf[MAX_BUF];
        getcwd(buf, sizeof(buf));
        printf("%s\n", buf);
        return 1;
    }
    else if(!strcmp(argv[0], "exit") || !strcmp(argv[0], "q") ){
        exit(0);
    }
    return 0;
}

没啥难度,也不是实验要求。


标题:杭电操作系统实验三模拟shell
作者:abandon
地址:HTTPS://www.songsci.com/articles/2021/09/08/1645759080340.html

Life Is Like A Boat

取消