Skip to content
/ shell Public

《C 语言实现 Linux Shell 命令解释器》项目可以培养 Linux 系统编程能力,尤其是在多进程方面。可以了解fork、execvp 等重要的系统调用。另外还能深入到底层理解 Linux Shell 的功能的实现手段。

Notifications You must be signed in to change notification settings

CalvinML/shell

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 

Repository files navigation

一、实验简介

《C 语言实现 Linux Shell 命令解释器》项目可以培养 Linux 系统编程能力,尤其是在多进程方面。可以了解fork、execvp 等重要的系统调用。另外还能深入到底层理解 Linux Shell 的功能的实现手段。为了测试方便,代码只放在一个文件中。

1.1 知识点

  • Shell 的基本概念
  • 进程控制相关的系统调用的使用(如 fork,exev)
  • 信号的概念及系统调用 signal的使用

1.2 效果截图

这里写图片描述

1.3 设计流程

这里写图片描述

二、main 函数的设计

首先,以自顶向下的方式探讨一下在 Linux Shell 的周期内主要做了哪些事: 初始化:在这一步,一个典型的 Shell 应该读取配置文件并执行配置功能。这样可以改变 Shell 的行为。 解释:接下来,Shell 将从标准输入(可以是交互的方式或者一个脚本文件)中读入命令,然后执行它。 终止:在命令被执行之后,Shell 执行关闭命令,释放内存,最后终止。 详细见代码注释:

/* 主函数入口 */
/* 主函数入口 */
int main()
{
    char *command;
    int iter = 0;
    command = (char *)malloc(MAX+1); //用于存储命令语句

    chdir("/home/");  /* 通常在登陆 shell 的时候通过查找配置文件(/etc/passwd 文件)
                               中的起始目录,初始化工作目录,这里默认为家目录 */
    while(1)
    {
        iter = 0;
		//定义信号处理方式
		signal_set();
        //用于输出提示符
        print_prompt();  
        //扫描多条命令语句
        scan_command(command);    
        // 基于分号解析出单条命令语句,并以字符串的形式存进全局变量 all 中
        parse_semicolon(command);

        // 迭代执行单条命令语句
        while(all[iter] != NULL)
        {
            execute(all[iter]); //这是 shell 解释器的核心函数
            iter += 1;
        }
    }
}
  • 注:print_prompt()、scan_command(command) 和 parse_semicolon(command) 三个函数是自定义函数,功能分别为:用于输出提示符,扫描多条命令语句,基于分号解析出单条命令语句,并以字符串的形式存进全局变量 all 中。概念简单,其详细代码在项目完整代码中。

三、程序的核心功能 execute 函数

execute函数完成的主要功能如下:

  • 判断用户执行的是否是 Shell 的内建命令,如果是则执行(这里为了强调概念,没有过多的添加内建命令)
  • 解析单条命令语句,即将命令名和命令参数解析并保存在字符串数组,以便调用 bf_exec命令。
  • 识别输入输出重定向符号,利用底层系统调用函数 dup2完成上层输入输出重定向
  • 识别后台运行符号,通过设置自定义函数 bf_exec的模式为1实现,这里要说明一点:所谓后台执行,无非就是让前台 Shell 程序以非阻塞的形式执行,并没有对后台程序做任何操作
  • 这部分的完整代码如下:
/* 执行单条命令行语句 */
void execute(char *command)
{
    char *arg[MAX_COMM];
    char *try;
    arg[0] = parse(command, 0);        //获得命令名称的字符串指针,如“ls”
    int t = 1;
    arg[t] = NULL;   
    if (strcmp(arg[0], "cd") == 0)     // 处理内嵌命令“cd”的情况
    {
        try = parse(command, 1);
        cd(try);
        return ;
    }
    if (strcmp(arg[0], "exit") == 0) // 为了方便用户推出shell
        exit(0);

    // 循环检测剩下的命令参数,即检测是否:重定向?管道?后台执行?普通命令参数?
    while (1)
    {
        try = parse(command, 1);
        if (try == NULL)
            break;
        else if (strcmp(try, ">") == 0)  // 重定向到一个文件的情况
        {
            try = parse(command, 1);   // 得到输出文件名
            file_out(arg, try, 0);        // 参数 0 代表覆盖的方式重定向
            return;
        }
        else if (strcmp(try, ">>") == 0)   // 追加重定向到一个文件
        {
            try = parse(command, 1);
            file_out(arg, try, 1);        // 参数 1 代表追加的方式重定向
            return;
        }
        else if (strcmp(try, "<") == 0)    // 标准输入重定向
        {
            try = parse(command, 1);      // 输入重定向的输入文件
            char *out_file = parse(command, 1);    // 输出重定向的输出文件
            if (out_file != NULL)
            {
                if (strcmp(out_file, ">") == 0)   // 输入输出文件给定
                {
                    out_file = parse(command, 1);
                    if (out_file == NULL)
                    {
                        printf("Syntax error !!\n");
                        return;
                    }
                    else
                        file_in(arg, try, out_file, 1);     // 参数 1 针对双重定向 < >
                }
                else if (strcmp(out_file, ">>") == 0)
                {
                    out_file = parse(command, 1);
                    if (out_file == NULL)
                    {
                        printf("Syntax error !!\n");
                        return;
                    }
                    else
                        file_in(arg, try, out_file, 2);       // 参数 2 针对双重定向 < >>
                }
            }
            else
            {
                file_in(arg, try, NULL, 0);        // 模式 0 针对单一的输入重定向 
            }
        }
        //处理后台进程
        else if (strcmp(try, "&") == 0)   // 后台进程
        {
            bf_exec(arg, 1);     // bf_exec 的第二个参数为 1 代表后台进程
            return;
        }
        else       //try是一个命令参数
        {
            arg[t] = try;
            t += 1;
            arg[t] = NULL;
        }
    }

    bf_exec(arg, 0);     // 参数 0 表示前台运行
    return;
}

3.1处理标准输出重定向问题的函数: file_out

本函数的定义为: void file_out(char *arg[], char *out_file, int type)。 arg 代表命令行参数。 out_file 代表重定向的文件名。 通过** dup2(f, 1)** 语句使得标准输出文件(文件描述符为1)重定向到制定的文件out_file(文件描述符f)。至于是追加重定向还是覆盖重定向取决于函数 open 的打开模式(是否是 O_APPEND)。 这部分的完整代码如下:

/* 处理输出重定向文件的问题 */
void file_out(char *arg[], char *out_file, int type)
{
    int f;
    current_out = dup(1);         
    if(type == 0)        //处理以覆盖的方式重定向“>”
    {
        f = open(out_file, O_WRONLY | O_CREAT, 0777); 
        dup2(f, 1); //复制文件描述符f,并指定为 1,也就是让标准输出重定向到指定的文件
        close(f);
        bf_exec(arg, 0);
    }
    else                 // 处理以追加的方式重定向“>>”
    {
        f = open(out_file, O_WRONLY | O_CREAT | O_APPEND , 0777); //以 O_APPEND 模式打开
        dup2(f, 1);
        close(f);
        bf_exec(arg, 0);
    }
}

3.2 处理标准输入重定向问题的函数: file_in

本函数的定义原型为 void file_in(char *arg[], char *in_file, char *out_file, int type) 类似于 file_out函数,该本分仍然采用 dup2(in, 0)来完成标准输入文件(文件描述符0)重定向到执行的文件 in_file(文件描述符为 in)。

  • 这部分的完整代码如下:
/* 处理输入重定向文件的问题 */
void file_in(char *arg[], char *in_file, char *out_file, int type)
{
    int in;
    in = open(in_file, O_RDONLY);
    current_in = dup(0); //
    dup2(in, 0);
    close(in);
    if(type == 0)    // 单一的输入重定向
    {
        printf("Going to execute bf_exec\n");          //debug remove it
        bf_exec(arg, 0);
    }
    else if(type == 1)            // 双重重定向 `< ... >` 
    {
        file_out(arg, out_file, 0);
    }
    else                         // 双重重定向 `< ... >>`
    {
        file_out(arg, out_file, 1);
    }
    return;
}

3.3 实现命令执行的最后一步: bf_exec

本函数定义的原型为:void bf_exec(char *arg[], int type), arg 指命令行,type标识前台还是后台运行。

  • 实现命令的执行必须在 Shell 父进程下 fork 一个子进程,并在子进程中调用 execvp 函数装载子进程执行的代码(所要执行的命令)。
  • 程序的前后台执行取决于父进程在子进程执行过程中是否是阻塞的。代码中,前台进程的父进程需要调用 wait(&pid) 阻塞,等待前台执行的命令执行完才能唤醒。
  • 这部分的完整代码如下:
/* 创建子进程,调用 execvp 函数执行命令程序(前后台)*/
void bf_exec(char *arg[], int type)
{
    pid_t pid;
    if(type == 0)    // 前台执行
    {
        if((pid = fork()) < 0)
        {
            printf("*** ERROR: forking child process failed\n");
            return ;
        }
        // 父子进程执行代码的分离
        else if(pid == 0)    //子进程
        {
            signal(SIGTSTP, SIG_IGN);
            signal(SIGINT,SIG_DFL);//用于杀死子进程
			signal(SIGQUIT, SIG_IGN);
            execvp(arg[0], arg);  // execvp 用于在子进程中替换为另一段程序
        }
        else     //父进程
        {
			signal(SIGCHLD, bg_signal_handle);
			signal(SIGINT, sig_handle); // SIGINT = 2, 用户键入Ctrl-C
			signal(SIGQUIT, sig_handle); /* SIGQUIT = 3 用户键入Ctr+\ */
			signal(SIGTSTP, sig_handle);  // SIGTSTP =20,一般有Ctrl-Z产生
			pid_t c;
            c = wait(&pid);  //等待子进程结束
            dup2(current_out, 1); //还原默认的标准输出
            dup2(current_in, 0); //还原默认的标准输入
            return;
        }
    }
    else            // 后台执行
    {
        if((pid = fork()) < 0)
        {
            printf("*** ERROR: forking child process failed\n");
            return ;
        }
        else if(pid == 0)    // 子进程
        {
			signal(SIGINT, SIG_IGN); 
			signal(SIGQUIT, SIG_IGN); 
			signal(SIGTSTP,SIG_IGN);
            execvp(arg[0], arg);
        }
        else     // 父进程
        {
			signal(SIGCHLD, bg_signal_handle);
			signal(SIGINT, sig_handle); // SIGINT = 2, 用户键入Ctrl-C
			signal(SIGQUIT, sig_handle); /* SIGQUIT = 3 用户键入Ctr+\ */
			signal(SIGTSTP, sig_handle);  // SIGTSTP =20,一般有Ctrl-Z产生
            bg_struct_handle(pid, arg, 0);
            dup2(current_out, 1);
            dup2(current_in, 0);
            return ;
        }

    }
}

四、实验总结

通过本实验,我们完成了一个支持输入输出重定向,支持后台运行的 Linux Shell 命令解释器。通过该项目的学习,可以更进一步的了解 Linux Shell 的工作机制。在使用重定向 > 和>> 时 建议将目录切换到自己的家目录,否则没权限建立重定向文件。 但是在本次实验中出现了一点bug还没有解决,重定< file1< file1 >> file2 的功能在运行时没有效果。希望各位童鞋能够改进代码,代码中的不足也希望大家能够指出,谢谢。

  • 该项目的完整代码见:github

五、参考资料

About

《C 语言实现 Linux Shell 命令解释器》项目可以培养 Linux 系统编程能力,尤其是在多进程方面。可以了解fork、execvp 等重要的系统调用。另外还能深入到底层理解 Linux Shell 的功能的实现手段。

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages