计算机系统篇之异常控制流(7):利用 fork 和 execve 实现一个简易的 shell 程序
Author: stormQ
Created: Saturday, 29. August 2020 01:11PM
Last Modified: Monday, 31. August 2020 06:39PM
本文描述了一个简易 shell 的完整实现过程(你可以利用它在前台或后台执行程序),从而理解 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
>