操作系统实验——A Simple Custom Shell

实验要求

√1、模拟Linux Shell的运行样子
√2、可执行Linux文件系统中的命令(外部命令),如:ls, mkdir….
√3、可执行自定义的内置Shell命令,如: chdir, clear, exit
√4、支持命令后台运行,将尾部有&号的命令抛至后台执行
附加:
(待)1、实现Shell对管道的支持,如支持 ls | grep “pipe” 等命令
(待)2、实现Shell对输入输出重定向的支持,如支持 ls > result.txt

多啰嗦一句

虽然最终是要在Ubuntu上运行,也在Ubuntu上安装Codeblocks了,但是还是感觉各种难用。。。所以我决定在Xcode下写,搬到Ubuntu上运行+_+自虐了一晚上,才做出前四个,后面的实在有心无力了~ ~

结果图

技术分享

正文

源码

本文源码挂在github上,url:https://github.com/YiZhuoChen/MyShell,需要的可以自行下载。

原理

实验的基本思路是,不断从命令行接收用户输入,将用户输入的字符串分割(可能带有参数)。
——对于内置命令,分别判断然后到自定义函数中实现,有的可以调用Linux提供的函数,有的可以自己实现。
——对于外部命令,处理时需要判断是否要扔到后台、重定向,是否用到管道等。普通情况下,fork出子进程,然后在子进程中调用execvp函数处理,父进程中wait即可。
——字符串的分割:理论上应该自己写一个算法,对用户的输入进行合理性检验,同时分割字符串,方便起见,这里采用string提供的方法strtok简单处理,strtok是根据提供的字符集中所有字符对输入字符串进行分割,返回指向第一个字符串的指针,接下来还想对同一个字符串分割的话,第一个参数传入NULL即可。
——后台进程:父进程fork出子进程后,会返回子进程的pid,同时子进程的ppid置为父进程的pid(类比链表),这样父子进程就形成了关联。父进程中调用wait后就等待子进程结束返回的信号。所谓后台进程,就是让父进程不再关心子进程,将子进程托管给God Progress(即子进程的ppid = 1),由操作系统来管理。简单来说,就是父进程中不需要wait,也不需要处理子进程的返回信号了。
其他的代码中在详述。

全局变量与函数

首先是导入一些头文件,以及全局变量和函数的声明:

#include <stdio.h>
#include <string.h> //strcmp, strtok
#include <unistd.h> //getpid, chdir, _exit
#include <signal.h> //kill
#include <errno.h>  //errno, EINTR
#include <stdlib.h> //EXIT_FAILURE

#define MAX_SIZE 100

#pragma mark - Global Variables

char line[MAX_SIZE];    //get user input from command line
int flag;   //support relocation or not? ie. ls > result.txt
int back_flag;  //run the progress in the back?

#pragma mark - Functions Declaration

/**
 *  exit terminal
 */
void my_exit();

/**
 *  command "cd", go to target directory
 *
 *  @param target target directory
 */
void my_chdir(char *target);

/**
 *  other commands. Let linux to run it.
 */
void my_unix(char *command);

工程中用到的每个头文件中的函数或者变量已经写到头文件后的注释中了。
line用于接收用户从终端输入的命令。
flag用于标识重定向操作,接下来会对重定向进行简单的模拟。但并未实现。
back_flag用于标识是否进行后台操作。依据是用户输入的命令后缀是否有&符号。

my_exit()是自定义exit命令处理函数。
my_chdir(char *target)是自定义cd命令处理函数,target是目标目录
my_unix(char *command)是调用Linux文件系统中的命令,command为用户输入的命令,在该函数中同时处理重定向和后台操作。

main函数

#pragma mark - Main

int main(int argc, const char * argv[]) {

    //default no need to relocation
    flag = 0;
    //default not in the back
    back_flag = 0;

    char *command;



    while (1) {
        //#warning Not available in MacOS
//        char *userName = getlogin(); //user name
//        char *path = get_current_dir_name(); //currentPath
        //format: user@user-desktop:~path
//        printf("%s@%s-desktop:~%s$", userName, userName, path);

        fgets(line, MAX_SIZE, stdin);   //read from stdin
        //split user‘s input and get the first "string"
        //here use " ", "\n", "\t"... as the delimiter
        command = strtok(line, " \n\t();");
        if (command == NULL) {
            continue;       //if user input nothing, then ingore it.
        } else if (strcmp(command, "exit") == 0) {
            //if user input "exit" the exit the application.
            my_exit();
        } else if (strcmp(command, "cd") == 0) {
            my_chdir(command);
        } else {
            //let system handle other commands
            my_unix(command);
        }
    }

    return 0;
}

main函数中首先把两个flag置0,表示默认普通情况。
然后开一段死循环,在死循环中通过fgets从标准输入读一行命令。然后按照之前所述,通过strtok将命令分割,拿到第一段字符串(命令主体),根据不同的命令调用不同的函数。
注意:1、循环开始那块有个被注释掉的部分,这是Linux系统下的函数,MacOS下没有。
2、这段程序在Ubuntu,Codeblocks下运行得挺好,在Xcode下接收第二个输入开始就变成死循环,使用fgets、scanf都会这样,不知道为什么。

my_exit

#pragma mark - Other Functions

void my_exit() {
    //    printf("exit");
    pid_t pid = getpid();
    //SIGTERM: software termination signal from kill
    kill(pid, SIGTERM);
}

这段函数非常简单,将当前进程杀死即可。pid为进程的id号,标识该运行的进程。SIGTERM标识的意思是由软件引起的终止信号。

效果图:
技术分享

my_chdir()

void my_chdir(char *target) {
    //    printf("cd");

    int status;

    //in order to get next token and to continue with the same string.
    //NULL is passed as first argument
    target = strtok(NULL, " \n\t();");
    while (target) {
        status = chdir(target);
        //handle error
        if (status < 0) {
            fprintf(stderr, "Error in my_chdir(%s) : %s\n", target, strerror(errno));
            return;
        }
        target = strtok(NULL, " \n\t();");
    }
}

这个函数也比较容易,通过向strtok中第一个参数传入NULL来表明对当前字符串继续分割并拿到下一段。Linux提供了chdir函数来改变当前的工作路径。此时如果在Ubuntu下运行,并打开之前的注释,那么就可以看到类似命令行的开始了。

效果图:
技术分享

my_unix()

void my_unix(char *command) {
    //    printf("other");

    pid_t pid;
    char *args[25];
    int count = 0;
    FILE *file; //the destination of relocation
    int execvp_status;  //execvp success or not?
    int wait_status;    //wait success or not?
    int close_status;   //close file success or not?
    int wait_arg;       //the arg of wait();

    //add first part of command to args.
    args[0] = command;

    command = strtok(NULL, " \n\t();");
    while (command != NULL) {

        if (strcmp(command, ">") == 0) {
            flag = 1;   //relocation
        } else if (strcmp(command, "&") == 0) {
            back_flag = 1;
            command = strtok(NULL, " \n\t();");
        }

        count++;
        args[count] = command;
        command = strtok(NULL, " \n\t();");
    }

    //the end of command
    count++;
    args[count] = NULL;

    pid = fork();
    //handle error
    if (pid < 0) {
        fprintf(stderr, "Error in my_unix(%s) when fork child progress : %s", args[0], strerror(errno));
        _exit(0);
    }

    if (pid == 0) {
        //child progress

        //psu-relocate, just put commands to file.
        if (flag == 1) {
            //args[count - 1] is the last argument, which represent the file to relocate
            file = freopen(args[count - 1], "w+", stdout);
            if (file == NULL) {
                fprintf(stderr, "Error in my_unix(%s) when reopen file : %s", args[0], strerror(errno));
                _exit(0);
            }

            int i = 0;
            while (args[i] != NULL) {
                fprintf(file, "%s", args[i]);
                i++;
            }

            //cleanup
            close_status = fclose(file);
            if (close_status != 0) {
                fprintf(stderr, "Error in my_unix(%s) when close file : %s", args[0], strerror(errno));
                _exit(EXIT_FAILURE);
            }
            flag = 0;
            back_flag = 0;
            _exit(0);
        }

        execvp_status = execvp(args[0], args);
        if (execvp_status < 0) {
            fprintf(stderr, "Error in my_unix(%s) when execvp : %s", args[0], strerror(errno));
            _exit(EXIT_FAILURE);
        }

        _exit(0);
    } else {
        //parent progress

        //if child progress is in back, then parent is not necessary to wait.
        if (back_flag == 0) {
            wait_status = wait(&wait_arg);
            if (errno == EINTR) {
                return;
            }
            if (wait_status < 0) {
                fprintf(stderr, "Error in my_unix(%s) with wait() : %s", args[0], strerror(errno));
                _exit(EXIT_FAILURE);
            }

            flag = 0;
            back_flag = 0;
        } else {
            printf("Pid = %d\n", getpid());
        }
    }
}

这个函数比较长,让我们一部分一部分来看。首先是变量声明部分:


    pid_t pid;
    char *args[25];
    int count = 0;
    FILE *file; //the destination of relocation
    int execvp_status;  //execvp success or not?
    int wait_status;    //wait success or not?
    int close_status;   //close file success or not?
    int wait_arg;       //the arg of wait();

    //add first part of command to args.
    args[0] = command;

那几个status变量是用来标识某些方法的调用是否成功并做出错处理用的, args用于保存用户的输入命令分割后的每一段,count是总段数。FILE *变量用在模拟文件重定向功能。

字符串的分割部分:

command = strtok(NULL, " \n\t();");
    while (command != NULL) {

        if (strcmp(command, ">") == 0) {
            flag = 1;   //relocation
        } else if (strcmp(command, "&") == 0) {
            back_flag = 1;
            command = strtok(NULL, " \n\t();");
        }

        count++;
        args[count] = command;
        command = strtok(NULL, " \n\t();");
    }

    //the end of command
    count++;
    args[count] = NULL;

用count作为计数变量,将每一段输入命令保存到args中。同时检查是否有&和>字符,如果有,分别将后台操作的flag或重定向操作的flag置1。

子进程操作:

//child progress

        //psu-relocate, just put commands to file.
        if (flag == 1) {
            //args[count - 1] is the last argument, which represent the file to relocate
            file = freopen(args[count - 1], "w+", stdout);
            if (file == NULL) {
                fprintf(stderr, "Error in my_unix(%s) when reopen file : %s", args[0], strerror(errno));
                _exit(0);
            }

            int i = 0;
            while (args[i] != NULL) {
                fprintf(file, "%s", args[i]);
                i++;
            }

            //cleanup
            close_status = fclose(file);
            if (close_status != 0) {
                fprintf(stderr, "Error in my_unix(%s) when close file : %s", args[0], strerror(errno));
                _exit(EXIT_FAILURE);
            }
            flag = 0;
            back_flag = 0;
            _exit(0);
        }

        execvp_status = execvp(args[0], args);
        if (execvp_status < 0) {
            fprintf(stderr, "Error in my_unix(%s) when execvp : %s", args[0], strerror(errno));
            _exit(EXIT_FAILURE);
        }

        _exit(0);

中间if (flag == 1)那个代码段即为重定向的模拟,简单看下就好。最后一部分通过execvp将文件路径(args[0])和命令args传入,表明将子进程从父进程中脱离,单独执行。
注意:传入execvp的参数中,第二个参数表示用户输入的命令的字符数组必须以NULL结尾。

父进程:

        //parent progress

        //if child progress is in back, then parent is not necessary to wait.
        if (back_flag == 0) {
            wait_status = wait(&wait_arg);
            if (errno == EINTR) {
                return;
            }
            if (wait_status < 0) {
                fprintf(stderr, "Error in my_unix(%s) with wait() : %s", args[0], strerror(errno));
                _exit(EXIT_FAILURE);
            }

            flag = 0;
            back_flag = 0;
        } else {
            printf("Pid = %d\n", getpid());
        }

如果没有设置后台操作,则让父进程等待子进程执行完毕,否则跳过等待部分。
注意:关于EINTR
EINTR错误的描述是“当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误”,我们的wait就是这样一个“慢系统调用”,如果父进程阻塞在慢系统调用中的时候突然接收到一个信号,则内核会返回一个EINTR表示系统调用被中断,此时通过strerror打印errno的话会出现EINTR的字符描述:Interrupted system call。对此,我们只需要简单地忽略即可。

效果图:
技术分享

小结

真正的自定义shell应该是从头开始,包括用户输入有效性检验、部分命令的模仿实现等。这里核心功能都是调用的Linux提供的函数,算是属于练习fork、exec、wait等函数的使用。而且目前管道和重定向也没有实现,希望以后能有一天来填充这个空缺。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。