返回介绍

第 1 节 C 语言实现 Linux Shell 命令解释器

发布于 2025-02-25 23:29:28 字数 18095 浏览 0 评论 0 收藏 0

一、实验简介

《C 语言实现 Linux Shell 命令解释器》项目可以培养 Linux 系统编程能力,尤其是在多进程方面。可以了解 forkexecvp 等重要的系统调用。另外还能深入到底层理解 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/shiyanlou");  /* 通常在登陆 shell 的时候通过查找配置文件(/etc/passwd 文件)
                 中的起始目录,初始化工作目录,这里默认为家目录 */
  while(1)
  {
    iter = 0;
    /* 捕捉信号,当信号发生时调用信号处理函数 sig_handle */
    signal(SIGINT, sig_handle); // SIGINT = 2, 用户键入 Ctrl-C
    signal(SIGQUIT, sig_handle); /* SIGQUIT = 3 用户键入 Ctr+\ */
    signal(SIGCHLD, sig_handle); //SIGCHLD = 17,一般在子进程中止时产生
    signal(SIGTSTP, SIG_IGN); // SIGTSTP =20,一般有 Ctrl-D 产生
    //用于输出提示符
    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, "&amp;") == 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(&amp;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_DFL); /* 接受 SIGTSTP 信号(由 Ctrl-Z 的时候产生 ),
                    采取系统默认动作,暂停进程 */
      execvp(arg[0], arg);  // execvp 用于在子进程中替换为另一段程序
    }
    else   //父进程
    {
      pid_t c;
      signal(SIGTSTP, SIG_DFL);  /* 接受 SIGTSTP 信号(由 Ctrl-Z 的时候产生 ),
                      采取系统默认动作,启动进程 */
      c = wait(&amp;pid);  //等待子进程结束
      dup2(current_out, 1); //还原默认的标准输出
      dup2(current_in, 0); //还原默认的标准输入
      return;
    }
  }

  else      // 后台执行
  {
    signal(SIGCHLD, bg_signal_handle);
    if((pid = fork()) < 0)
    {
      printf("*** ERROR: forking child process failed\n");
      return ;
    }
    else if(pid == 0)  // 子进程
    {
      int f;
      execvp(arg[0], arg);
    }
    else   // 父进程
    {
      bg_struct_handle(pid, arg, 0);
      dup2(current_out, 1);
      dup2(current_in, 0);
      return ;
    }

  }
}

四、实验总结

通过本实验,我们完成了一个支持输入输出重定向,支持后台运行的 Linux Shell 命令解释器。通过该项目的学习,可以更进一步的了解 Linux Shell 的工作机制。

  • 该项目的完整代码如下:
#include<stdio.h>
#include<string.h>
#include<signal.h>
#include<stdlib.h>   
#include<unistd.h>
#include<errno.h> 
#include<fcntl.h>
#include<sys/utsname.h>
#include<sys/stat.h>
#include<sys/wait.h>
#include<sys/ptrace.h>
#include<sys/types.h>
#define MAX 1024
#define MAX_COMM 100

// 全局声明
void print_prompt();
void bg_struct_handle(pid_t pid, char *arg[], int type);
char cwd[MAX];  //保存当前路径 
char *all[MAX]; //all 用来保存命令行字符串
int current_out = 4;
int current_in = 5;
int fd[4];

typedef struct proc{     //针对后台进程
  pid_t pid;
  int status;
  char *arg[MAX_COMM];
  struct proc *next;
}proc;
proc *start;

/* 信号处理函数 */
void sig_handle(int sig)
{

  if(sig == 2) // SIGINT = 2
  {
    printf("\nInstead of Ctrl-C type quit\n");
    print_prompt();
  }
  else if(sig == 3)  //SIGQUIT = 3
  {
    printf("\nType quit to exit\n");
    print_prompt();
  }
  signal(sig, sig_handle); //SIGCHLD = 17
}

/* 用于输出提示符 */
void print_prompt()
{
  // 调用 uname 获取系统信息
  struct utsname uname_ptr;
  uname(&amp;uname_ptr);

  //调用 getcwd 获取当前路径名,并存储在 cwd 指向的字符串
  getcwd(cwd, sizeof(cwd));
  setbuf(stdout, NULL);     //禁用 buffer, 直接将 printf 要输出的内容输出
  printf("<%s@%s:%s> ",uname_ptr.nodename, uname_ptr.sysname, cwd);
}

/* 扫描用户输入的命令行 */
void scan_command(char *command)
{
  int bytes_read;
  size_t nbytes = MAX;
  bytes_read = getline(&amp;command, &amp;nbytes, stdin); /*从标准输入中读入包含
                          多条命令行语句的字符串,并保存在 command 变量中 */
  bytes_read -= 1;
  command[bytes_read] = '\0';
}

/* 以空格为分解命令及其参数 */
void *parse(char *command, int time) 
{
  char *comm;   // 用于储存命令或命令的参数
  if(time ==0)
    comm = strtok(command, " ");
  else
    comm = strtok(NULL, " ");
  return comm;
}

/* 分割用户以分号分隔的多命令,如“ls;cd”*/
void parse_semicolon(char *command)
{
  int i ;
  for (i=0; i < MAX; i++)
    all[i] = (char *) malloc(MAX_COMM * sizeof(char));
  i = 0;
  all[i] = strtok(command, ";"); //注意 strtok 的用法,用来分割命令行
  while(1)
  {
    i += 1;
    all[i] = strtok(NULL, ";");
    if(all[i] == NULL)
      break;
  }
}

/* 内建命令 cd 的实现 */
void cd(char *arg)
{
  if(arg == NULL)
  {
    printf("insufficient arguments\n");
  }
  else
  {
    int cond;
    cond = chdir(arg);
    if(cond == -1)
    {
      printf("wrong path\n");
    }
  }
}

/* 调用 bg_struct_handle 对后台进程 proc 信息的处理 */
void bg_signal_handle()
{
  int status;
  pid_t pid;
  pid = waitpid(-1, &amp;status, WNOHANG);
  proc *iterate;
  iterate = start;
  while(iterate != NULL)
  {
    if(iterate -> pid == getpid())
    {
      bg_struct_handle(pid, NULL, 1); // 将结束的子进程从 proc 上删除相关信息(第三个参数为 1)
    }
  }
}

/* 处理 proc 链表(增、删、打印) */ 
void bg_struct_handle(pid_t pid, char *arg[], int type)
{
  proc *iterate, *new;
  if(type == 0)    // proc 链表上增加一个 proc 结构
  {
    if(start == NULL)
    {
      start = (proc *)malloc(sizeof(proc));
      start -> pid = pid;
      start -> status = 1;
      start -> next = NULL;
      int i = 0;
      while(arg[i] != NULL)
      {
        start -> arg[i] = malloc(MAX_COMM * sizeof(char));
        strcpy(start -> arg[i], arg[i]);
        i += 1;
      }
      start -> arg[i] = NULL;
    }
    else
    {
      new = (proc *)malloc(sizeof(proc));
      new -> pid = pid;
      new -> status = 1;
      new -> next = NULL;
      int i = 0;
      while(arg[i] != NULL)
      {
        new -> arg[i] = malloc(MAX_COMM * sizeof(char));
        strcpy(new -> arg[i], arg[i]);
        i += 1;
      }
      new -> arg[i] = NULL;
      iterate = start ;
      while(iterate -> next != NULL)
        iterate = iterate -> next;
      iterate -> next = new;
    }
  }
  else if(type == 1)  // proc 链表上删除一个 proc 结构
  {
    proc *preiter = NULL;
    iterate = start;
    while(iterate != NULL &amp;&amp; iterate -> pid != pid )
    {
      preiter = iterate;
      iterate = iterate -> next;
    }
    if(iterate == NULL)
    {
      printf("No Such Pid !\n");
      return ;
    }
    else if(iterate -> pid == pid)
    {
      if(preiter == NULL)
      {
        start = iterate -> next;
        free(iterate);
      }
      else
      {
        preiter -> next = iterate -> next;
        free(iterate);
      }
    }
  }

  else if(type == 2)  //迭代地打印 proc 链表的 proc 结构信息
  {
    int i = 1, a = 0;
    iterate = start;
    if (iterate == NULL)
    {
      printf("No Background jobs\n");
      return ;
    }
    while(iterate != NULL)
    {
      a = 0;
      setbuf(stdout, NULL);     
      printf("[%d] ",i);
      while(iterate -> arg[a] != NULL)
      {
        printf("%s ", iterate -> arg[a]);
        a += 1;
      }
      printf("[%d]\n", iterate -> pid);
      i += 1;
      iterate = iterate -> next;
    }
  }
  return ;
}

/* 创建子进程,调用 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_DFL); /* 接受 SIGTSTP 信号(由 Ctrl-Z 的时候产生 ),
                    采取系统默认动作,暂停进程 */
      execvp(arg[0], arg);  // execvp 用于在子进程中替换为另一段程序
    }
    else   //父进程
    {
      pid_t c;
      signal(SIGTSTP, SIG_DFL);  /* 接受 SIGTSTP 信号(由 Ctrl-Z 的时候产生 ),
                      采取系统默认动作,启动进程 */
      c = wait(&amp;pid);  //等待子进程结束
      dup2(current_out, 1); //还原默认的标准输出
      dup2(current_in, 0); //还原默认的标准输入
      return;
    }
  }

  else      // 后台执行
  {
    signal(SIGCHLD, bg_signal_handle);
    if((pid = fork()) < 0)
    {
      printf("*** ERROR: forking child process failed\n");
      return ;
    }
    else if(pid == 0)  // 子进程
    {
      int f;
      execvp(arg[0], arg);
    }
    else   // 父进程
    {
      bg_struct_handle(pid, arg, 0);
      dup2(current_out, 1);
      dup2(current_in, 0);
      return ;
    }

  }
}

/* 处理输出重定向文件的问题 */
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);
  }
}

/* 处理输入重定向文件的问题 */
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;
}


/* 执行单条命令行语句 */
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, "&amp;") == 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;
}

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

  chdir("/home/shiyanlou");  /* 通常在登陆 shell 的时候通过查找配置文件(/etc/passwd 文件)
                 中的起始目录,初始化工作目录,这里默认为家目录 */
  while(1)
  {
    iter = 0;
    /* 捕捉信号,当信号发生时调用信号处理函数 sig_handle */
    signal(SIGINT, sig_handle); // SIGINT = 2, 用户键入 Ctrl-C
    signal(SIGQUIT, sig_handle); /* SIGQUIT = 3 用户键入 Ctr+\ */
    signal(SIGCHLD, sig_handle); //SIGCHLD = 17,一般在子进程中止时产生
    signal(SIGTSTP, SIG_IGN);  // SIGTSTP =20,一般有 Ctrl-D 产生
    //用于输出提示符
    print_prompt();  

    //扫描多条命令语句
    scan_command(command);  

    // 基于分号解析出单条命令语句,并以字符串的形式存进全局变量 all 中
    parse_semicolon(command);

    // 迭代执行单条命令语句
    while(all[iter] != NULL)
    {
      execute(all[iter]); //这是 shell 解释器的核心函数
      iter += 1;
    }
  }
}

参考资料

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文