计算机系统篇之异常控制流:利用 fork 和 execve 实现一个简易的 shell 程序

Posted csstormq

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了计算机系统篇之异常控制流:利用 fork 和 execve 实现一个简易的 shell 程序相关的知识,希望对你有一定的参考价值。

计算机系统篇之异常控制流(7):利用 fork 和 execve 实现一个简易的 shell 程序

Author: stormQ

Created: Saturday, 29. August 2020 01:11PM

Last Modified: Monday, 31. August 2020 06:39PM


什么是 shell

shell是一个交互型的应用级程序,它代表用户运行其他程序。

shell执行一系列的读 / 求值(read / evaluate)步骤,然后终止。读步骤读取自用户的一个命名行。求值步骤解析命令行,并代表用户运行程序。


如何在前台执行其他程序

step 1: 实现ReadCommand函数

ReadCommand函数用于读取用户一整行的输入(以换行符作为结束)。具体实现为:

std::string ReadCommand()

    std::printf("> ");
    std::string cmd;
    std::getline(std::cin, cmd);
    return cmd;

注:语句std::getline(std::cin, cmd)的作用为:从标准输入流std::cin中提取字符存储到std::string类型的变量cmd中,直到遇到换行符时停止提取。另外,std::getline()函数的其他版本可以通过第三个参数指定界定符。

注意: std::getline()函数提取的结果不包括界定符。

step 2: 实现PraseCommand函数

PraseCommand函数用于解析用户输入。即从用户输入中分割参数(以(一个或多个)空格作为参数分隔符),并初始化参数列表。

PraseCommand函数的第 1 个参数为用户输入的字符串,第 2 个参数指向参数列表的指针。参数列表为栈对象,并且参数数量限制在 128 个以内。

在初始化参数列表时,为了避免对参数列表中每个参数进行额外的内存分配、释放及拷贝开销,我们将用户输入字符串的所有空格(以空格作为参数的分隔符)都替换为 C 语言中字符串结束符\\0

由于std::string不会自动在字符串末尾插入\\0字符。所以,需要我们自己插入一个。

PraseCommand函数的具体实现为:

bool PraseCommand(std::string& input_cmd, char **output_cmd)

    if (input_cmd.empty())
    
        return false;
    
    input_cmd.insert(input_cmd.end(), 1, '\\0');
    std::memset(output_cmd, 0, sizeof(*output_cmd) * MAX_AEGS);

    const auto size = input_cmd.size();
    int first = 0, last = 0, k = 0;
    while (last < size)
    
        if (DELIMITER == input_cmd[last])
        
            input_cmd[last] = '\\0';
            if ('\\0' != input_cmd[first])
            
                output_cmd[k++] = &input_cmd[first];
            
            first = last + 1;
        
        last++;
    
    if (first < last && first < size && '\\0' != input_cmd[first])
    
        output_cmd[k] = &input_cmd[first];
    

    return true;

注:语句'\\0' != input_cmd[first]的作用:如果是空字符串(即用户原有输入中的一个或多个连续空格),则不作为参数。

step 3: 实现ExecCommand函数

ExecCommand函数用于创建子进程,并在子进程中执行用户命令,最后回收已终止的子进程。从而,达到在前台执行其他程序的效果。具体实现为:

void ExecCommand(char **argv, char **envp)

    const auto pid = fork();
    if (IS_CHILD(pid))
    
        if (-1 == execve(argv[0], argv, envp))
        
            std::printf("execve failed, args as followed:");
            PrintArgs(argv);
            exit(1);
        
    
    else
    
        Waitpid(pid, NULL, SUSPEND_CALLER);
    

这里,直接将父进程(即shell程序)的环境变量列表传递给子进程。如果子进程执行用户命令发生错误,则打印参数列表。

step 4: 完整程序

源码,proc18_main.cpp:

#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>

#include <errno.h>
#include <unistd.h>
#include <sys/wait.h>

#define MAX_AEGS 128
#define DELIMITER ' '
#define SUSPEND_CALLER 0
#define IS_CHILD(pid) (0 == pid)

std::string ReadCommand()

    std::printf("> ");
    std::string cmd;
    std::getline(std::cin, cmd);
    return cmd;


bool PraseCommand(std::string& input_cmd, char **output_cmd)

    if (input_cmd.empty())
    
        return false;
    
    input_cmd.insert(input_cmd.end(), 1, '\\0');
    std::memset(output_cmd, 0, sizeof(*output_cmd) * MAX_AEGS);

    const auto size = input_cmd.size();
    int first = 0, last = 0, k = 0;
    while (last < size)
    
        if (DELIMITER == input_cmd[last])
        
            input_cmd[last] = '\\0';
            if ('\\0' != input_cmd[first])
            
                output_cmd[k++] = &input_cmd[first];
            
            first = last + 1;
        
        last++;
    
    if (first < last && first < size && '\\0' != input_cmd[first])
    
        output_cmd[k] = &input_cmd[first];
    

    return true;


pid_t Waitpid(pid_t pid, int *statusp, int options)

    pid_t ret = 0;
    do
    
        ret = waitpid(pid, statusp, options);
     while (-1 == ret && EINTR == errno);
    return ret;


void PrintArgs(const char* const *argv)

    if (NULL == argv)
    
        std::printf("no any args\\n");
        return;
    

    for (int i = 0; argv[i] != NULL; i++)
    
        std::printf("arg[%d]: %s\\n", i, argv[i]);
    


void ExecCommand(char **argv, char **envp)

    const auto pid = fork();
    if (IS_CHILD(pid))
    
        if (-1 == execve(argv[0], argv, envp))
        
            std::printf("execve failed, args as followed:\\n");
            PrintArgs(argv);
            exit(1);
        
    
    else
    
        Waitpid(pid, NULL, SUSPEND_CALLER);
    


int main(int argc, char *argv[], char *envp[])

    while (true)
    
        auto input_cmd = ReadCommand();
        char *output_cmd[MAX_AEGS];
        if (PraseCommand(input_cmd, output_cmd))
        
            ExecCommand(output_cmd, envp);
        
    

    return 0;

编译:

$ g++ -o proc18_main proc18_main.cpp -g

运行:

$ ./proc18_main
> ls -lh
execve failed, args as followed:
arg[0]: ls
arg[1]: -lh
> /bin/ls -lh /usr
total 168K
drwxrwxr-x   3 root root 4.0K Nov 29  2018 3rdparty
drwxr-xr-x   5 root root 4.0K Nov 29  2018 aarch64-linux-gnu
drwxr-xr-x   2 root root  68K Jul 30 09:22 bin
drwxr-xr-x   2 root root 4.0K Dec  6  2018 games
drwxr-xr-x   4 root root 4.0K Mar 31 19:19 i686-w64-mingw32
drwxr-xr-x  62 root root  12K Jul 28 15:30 include
drwxr-xr-x 162 root root  20K Jul 28 15:30 lib
drwxr-xr-x   3 root root 4.0K Apr  2  2019 lib32
drwxr-xr-x   2 root root 4.0K Nov 29  2018 libx86_64-linux-gnu
drwxr-xr-x  13 root root 4.0K Sep 30  2019 local
drwxr-xr-x   3 root root 4.0K Mar  1  2018 locale
drwxr-xr-x   2 root root  12K Apr 16  2019 sbin
drwxr-xr-x 338 root root  12K Jul 30 09:22 share
drwxr-xr-x  20 root root 4.0K Apr 16  2019 src
drwxr-xr-x   4 root root 4.0K Mar 31 19:19 x86_64-w64-mingw32
> 

从上面结果中可以看出,目前实现的shell程序——proc18_main实现了代表用户运行其他程序(这里是/bin/ls -lh /usr)的基本功能。


如何在后台执行其他程序

在后台执行其他程序意味着shell不必等待后台子进程运行结束就可以执行下一个用户输入的命令。因此,要求shell能够非堵塞地回收后台子进程,具体原理参考 计算机系统篇之异常控制流(6):如何正确地回收子进程

在后台执行其他程序并回收后台子进程的具体实现步骤:

step 1: 捕获SIGCHLD信号,从而非堵塞地回收后台子进程

1)实现信号处理程序sigchld_hanlder

/**
 * @brief Provide a simple signal handler for the SIGCHLD signal with
 *        the following behaviors:
 *        1) Processing only the SIGCHLD signal.
 *        2) Save and restore errno value to avoid disturbing other parts
 *           of this program when returns from the signal handler.
 *        3) Waits for any child process to terminate without blocking.
 * @param sig Received signal number.
 */
void SigchldHanlder(int sig)

  if (SIGCHLD != sig)
  
    return;
  

  const auto old_errno = errno;
  while (WaitWithoutSuspend(nullptr) > 0)
  
  
  errno = old_errno;

其依赖函数WaitWithoutSuspend的实现为:

/**
 * @brief Waits for any child process to terminate without blocking.
 *        If no child has terminated, then this call returns immediately.
 * @param status The exit code of the terminated child. status is valid iff
 *               the child terminated normally, via a call to exit or returning
 *               from the main routine.
 * @return On success, if any child has terminated, returns the process ID of
 *         a terminated child. Otherwise, returns 0.
 *         On error, -1 is returned, and errno is set appropriately.
 */
int WaitWithoutSuspend(int *status)

  int pid = 0;
  do
  
    pid = waitpid(-1, status, WNOHANG);
   while (-1 == pid && EINTR == errno);

  if (nullptr != status && WIFEXITED(*status))
  
    *status = WEXITSTATUS(*status);
  
  return pid;

2)实现安装信号处理程序的包裹函数SetSignalHanlder

/**
 * @brief Install a signal handler function (handler) that is called
 *        asynchronously, interrupting the logical control flow,
 *        whenever the process receives a signal specified by sig.
 *        And provide behavior compatible with BSD signal semantics
 *        by making certain system calls restartable across signals.
 * @param sig Specifies the signal and can be any valid signal except
 *            SIGKILL and SIGSTOP.
 * @param hanlder The address of a user-defined function, called a signal
 *                handler, that will be called whenever the process receives
 *                a signal of type sig.
 * @return On success, return true.
 *         On error, false is returned, and errno is set appropriately.
 */
bool SetSignalHanlder(int sig, void (*hanlder)(int))

  struct sigaction sa;
  std::memset(&sa, 0, sizeof(sa));
  sa.sa_handler = hanlder;
  sa.sa_flags = SA_RESTART; // restart syscalls if possible
  return 0 == sigaction(sig, &sa, NULL);

3)安装SIGCHLD信号处理程序

在主程序main函数的第一行,添加如下内容:

SetSignalHanlder(SIGCHLD, SigchldHanlder);

step 2: 修改ExecCommand函数

1)实现IsBackground函数

IsBackground函数用于判断是否在后台执行用户输入的命令。具体实现为:

bool IsBackground(char **argv)

    char *last = NULL;
    while (argv && *argv)
    
        last = *argv;
        argv++;
    
    if (last && 0 == std::strcmp(last, BACKGROUND_FLAG))
    
        return true;
    
    return false;

2)实现DeleteLastArg函数

DeleteLastArg函数用于删除用户输入命令中的最后一个参数。该函数只有在后台执行程序时用到,即删除用户输入命令中的&。这里,采用&表示在后台执行。

void DeleteLastArg(char **argv)

    char **last = NULL;
    while (argv && *argv)
    
        last = argv;
        argv++;
    
    if (last)
    
        *last = NULL;
    

3)修改ExecCommand函数

修改ExecCommand函数:在执行前先判断用户命令是否要在后台执行。如果是,则将删除&后的参数列表传递给子进程;否则,在前台等待子进程结束。具体实现为:

void ExecCommand(char **argv, char **envp)

    const auto is_bg = IsBackground(argv);
    std::printf("is background:%s\\n", is_bg ? "true" : "false");
    if (is_bg)
    
        DeleteLastArg(argv);
    

    const auto pid = fork();
    if (IS_CHILD(pid))
    
        if (-1 == execve(argv[0], argv, envp))
        
            std::printf("execve failed, args as followed:\\n");
            PrintArgs(argv);
            exit(1);
        
    
    else
    
        if (!is_bg)
        
            Waitpid(pid, NULL, SUSPEND_CALLER);
        
    

4)程序的其他部分保持不变

step 3: 运行结果

$ ./proc18_main 
> /bin/ls -lh /usr &
is background:true
> total 168K
drwxrwxr-x   3 root root 4.0K Nov 29  2018 3rdparty
drwxr-xr-x   5 root root 4.0K Nov 29  2018 aarch64-linux-gnu
drwxr-xr-x   2 root root  68K Jul 30 09:22 bin
drwxr-xr-x   2 root root 4.0K Dec  6  2018 games
drwxr-xr-x   4 root root 4.0K Mar 31 19:19 i686-w64-mingw32
drwxr-xr-x  62 root root  12K Jul 28 15:30 include
drwxr-xr-x 162 root root  20K Jul 28 15:30 lib
drwxr-xr-x   3 root root 4.0K Apr  2  2019 lib32
drwxr-xr-x   2 root root 4.0K Nov 29  2018 libx86_64-linux-gnu
drwxr-xr-x  13 root root 4.0K Sep 30  2019 local
drwxr-xr-x   3 root root 4.0K Mar  1  2018 locale
drwxr-xr-x   2 root root  12K Apr 16  2019 sbin
drwxr-xr-x 338 root root  12K Jul 30 09:22 share
drwxr-xr-x  20 root root 4.0K Apr 16  2019 src
drwxr-xr-x   4 root root 4.0K Mar 31 19:19 x86_64-w64-mingw32

> 

如果你觉得本文对你有所帮助,欢迎关注公众号,支持一下!

以上是关于计算机系统篇之异常控制流:利用 fork 和 execve 实现一个简易的 shell 程序的主要内容,如果未能解决你的问题,请参考以下文章

计算机系统篇之异常控制流:异常控制流 FAQ

计算机系统篇之异常控制流:异常控制流 FAQ

计算机系统篇之异常控制流:如何正确地回收子进程

计算机系统篇之异常控制流:如何正确地回收子进程

计算机系统篇之异常控制流:如何正确地让调用线程休眠一段时间

计算机系统篇之异常控制流:如何正确地让调用线程休眠一段时间