操作系统实验——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: [email protected]:~path
//        printf("%[email protected]%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等函数的使用。而且目前管道和重定向也没有实现,希望以后能有一天来填充这个空缺。

时间: 2024-10-05 05:04:57

操作系统实验——A Simple Custom Shell的相关文章

操作系统实验指导书(完整版)

操作系统实验指导书 烟台大学计算机学院 操作系统课程组 2008-9-20 第一部分  操作系统上机指导   Linux操作系统环境: RedHat Enterprise Linux ES release 3 (Taroon Update 1) (2.4.21-9.EL) Red Flag Linux release 4.0 (HOT) (2.4)   登录到系统 常用命令练习: 用root账号(超级用户)注册,口令为computer(注意大小写).注册成功出现#号(超级用户系统提示符,普通用户

[操作系统实验lab3]实验报告

[感受]: 这次操作系统实验感觉还是比较难的,除了因为助教老师笔误引发的2个错误外,还有一些关键性的理解的地方感觉还没有很到位,这些天一直在不断地消化.理解Lab3里的内容,到现在感觉比Lab2里面所蕴含的内容丰富很多,也算是有所收获,和大家分享一下我个人的一些看法与思路,如果有错误的话请指正. [关键函数理解]: 首先第一部分我觉得比较关键的是对于一些非常关键的函数的理解与把握,这些函数是我们本次实验的精华所在,虽然好几个实验都不需要我们自己实现,但是这些函数真的是非常厉害!有多厉害,呆会就知

[Ubuntu]操作系统实验笔记

前些日子为了更新Ubuntu到14.04这个LTS版本,连带着把Windows也重新安装了一遍.懒得再安装虚拟机了,尝试一下在Ubuntu14.04这个64位系统里做操作系统实验咯. 1.安装交叉编译器 第一个要解决的问题就是交叉编译器,材料里提供的是x86平台上的交叉编译器.按道理来说64位系统应该是支持32程序的呢.试一下. 先不吐槽说说明文档里面的代码了.首先要解决的是各种权限问题.sudo su似乎不能全部搞定. 经过一堆权限不够的提示后我对安装已经基本没有信心了. 2.安装gxemul

0311 了解和熟悉操作系统实验

实验0.了解和熟悉操作系统实验 专业:商业软件工程2班   姓名:王俊杰  学号:201406114252 一.        实验目的 (1)掌握操作系统的定义和概念: (2)了解各类操作系统的发展历史: 二.        实验内容和要求 使用网络搜索了解各类计算机操作系统的知识,并整理成一篇文档. 实验方法.步骤及结果测试 了解和掌握内容包括: 计算机操作系统的定义和概念: 操作系统是方便用户.管理和控制计算机软硬件资源的系统软件(或程序集合). 从用户角度看,操作系统可以看成是对计算机硬

实验0、了解和熟悉操作系统实验

实验0.了解和熟悉操作系统实验 专业:商业软件工程2班   姓名:韩麒麟  学号:201406114253 一.        实验目的 (1)掌握操作系统的定义和概念: (2)了解各类操作系统的发展历史: 二.        实验内容和要求 使用网络搜索了解各类计算机操作系统的知识,并整理成一篇文档. 三.实验方法.步骤及结果测试 了解和掌握内容包括: 1.计算机操作系统的定义和概念: 操作系统(Operating System,简称OS)是管理电脑硬件与软件资源的程序,同时也是计算机系统的内

0311 实验0、了解和熟悉操作系统实验

实验0.了解和熟悉操作系统实验 专业:商软(2)班   姓名:列志华  学号:201406114254 一.        实验目的 (1)掌握操作系统的定义和概念: (2)了解各类操作系统的发展历史: 二.        实验内容和要求 使用网络搜索了解各类计算机操作系统的知识,并整理成一篇文档. 实验方法.步骤及结果测试 了解和掌握内容包括: 一.  计算机操作系统的定义和概念: 操作系统是现代计算机系统中不可缺少的系统软件,是其他所有系统软件和应用软件的运行基础.操作系统控制和管理整个计算

操作系统实验报告二

  操作系统实验报告二 姓名:许恺 学号:2014011329 日期:10月14日 题目1:编写线程池 关键代码如下: 1.Thread.h #pragma once #ifndef __THREAD_H #define __THREAD_H #include <vector> #include <string> #include <pthread.h> #pragma comment(lib,"x86/pthreadVC2.lib") using

[操作系统实验lab4]实验报告

昨天跟老师建议了OS实验改革的事情,感觉助教老师给的指导书挺坑哈,代码注释也不全.我也算沦落到看别人家的源码了... 我参考的源码注释是:https://github.com/benwei/MIT-JOS/ 这个源码质量暂且不评价,但这个注释质量真心不错!!!良心注释啊!!! 本不想去找源码注释啥来看的,毕竟可能一不小心就抄袭了源码的思想?不知道HT和WLM是怎么写的,他们做的都好快啊=.=难道只有我一个人做OS实验的周期是1~2周吗... 哎,不吐槽了,这篇文章留着慢慢更,不着急.感觉在鸣神的

操作系统实验报告四

操作系统实验4 题目1:编写页面内存的LRU替换算法 在实验3基础上考虑,如果当前分配的内存或保存页面的数据项已经被用完,这时再有新的网页请求,需要对已在内存中的网页数据进行替换,本实验内容需要使用LRU算法来对内存中的网页数据进行替换 题目2:编写页面内存的LFU替换算法 实现LFU(最少访问频率的页面替换)算法来管理内存页面 实验报告要求: 实验报告封面如下页所示. 按照题目要求,完成相关实验题目. 2.1报告中要包含完成此题目所查阅的一些关键技术材料.例如内存结构的设计.分配管理.回收方法