October 18, 2020
I've just hand in the homework of Linux Operating System about creating a basic shell today. The most of functions are done two weeks ago but the requirement about details such as blank ignorance and pipe force me to reconsider the design of code structure again and again.
今天刚刚提交了关于创建基本 Shell 的Linux 操作系统课程作业。大部分功能两周前就完成了,但对空白忽略和管道等细节的要求让我一次又一次地重新审视代码结构设计。
今日、基本的なシェルを作成するLinux Operating Systemの課題を提出しました。ほとんどの機能は2週間前に完成していましたが、空白の無視やパイプなどの細部の要件により、何度もコード構造の設計を見直すことになりました。
This tutorial, as a review of work, introduce how to use the fundamental Unix system call to build a useful shell with background, redirection, and pipe supporting. The source code is open at Github.
本教程作为对这项工作的回顾,介绍如何使用基础 Unix 系统调用来构建一个支持后台运行、重定向和管道的实用 Shell。源代码已开放在 Github 上。
このチュートリアルは作業のレビューとして、基本的なUnixシステムコールを使用してバックグラウンド実行、リダイレクト、パイプをサポートする実用的なシェルを構築する方法を紹介します。ソースコードはGithubで公開しています。
Why bother building a naive shell when there already are so many great shells like fish, bash and zsh? First, shell is a great window to learn about the fundamental Unix concepts, including process model, process communication, and file operation, and the usage of system call like fork(), exec(), wait(), pipe().
为什么要在 fish、bash、zsh 等优秀 Shell 已经存在的情况下,还要构建一个简单的 Shell 呢?第一,Shell 是学习基础 Unix 概念的绝佳窗口,包括进程模型、进程通信和文件操作,以及 fork()、exec()、wait()、pipe() 等系统调用的使用。
fish、bash、zshなどの優れたシェルがすでにある中、なぜ素朴なシェルを作るのでしょうか?第一に、シェルはUnixの基本概念、プロセスモデル、プロセス通信、ファイル操作、そしてfork()、exec()、wait()、pipe()などのシステムコールを学ぶ絶好の窓口です。
Second, in many cases there is not practical benefit to create a general purpose shell, but a command line user interface is very common, like the interactive command interpreter provided by Python, node.js, and gdb, all of them can be regarded as the domain specific shell.
第二,在很多情况下,构建一个通用 Shell 没有实际意义,但命令行用户界面非常常见,比如 Python、node.js 和 gdb 提供的交互式命令解释器,它们都可以被视为领域特定 Shell。
第二に、汎用シェルを作ることには実用的なメリットがない場合が多いですが、コマンドラインユーザーインターフェースは非常に一般的で、Python、node.js、gdbが提供するインタラクティブなコマンドインタープリタは、すべてドメイン固有シェルと見なすことができます。
In summary, learning about how to make a shell is quite useful theoretically and practically, so let's build it!
总之,学习如何构建 Shell 在理论和实践上都很有价值,让我们开始构建它吧!
まとめると、シェルの作り方を学ぶことは理論的にも実践的にも非常に有益です。では、作ってみましょう!
A shell continue to consume line by line from user until exit. Hence the framework of a shell must be a loop that keep processing inputs from standard input based on line:
Shell 持续逐行消费用户输入,直到退出。因此 Shell 的框架必须是一个持续处理来自标准输入的逐行输入的循环:
シェルは終了まで一行ずつユーザーからの入力を消費し続けます。そのため、シェルのフレームワークは標準入力からの行単位の入力を処理し続けるループでなければなりません:
#include <stdio.h>
int main() {
// We assume the input is less than 1024 chars.
char input[1024];
char* r;
while((r = gets(input)) != NULL) {
printf("%s\n", r);
}
return 0;
}
where we use function gets to read from standard input until it reaches the newline \n and store the string in input by removing \n beforehand. Try this by complie and run the executable:
其中使用 gets 函数从标准输入读取,直到遇到换行符 \n,并将去掉 \n 后的字符串存储在 input 中。编译并运行来试试:
ここでgets関数を使用して、改行\nに達するまで標準入力から読み取り、\nを除いた文字列をinputに格納します。コンパイルして実行してみましょう:
gcc sh.c -o sh
./sh
some input
some input
For now we receive input and print it out as the same. Not quite useful, of course. Next we might try to response to some simple command, like pwd that prints the current working directory and exit to quit, this is simple by invoking the getcwd system call and return:
目前我们只是接收输入并原样打印。当然没什么用。接下来可以尝试响应一些简单命令,比如打印当前工作目录的 pwd 和退出的 exit,通过调用 getcwd 系统调用和 return 即可实现:
今のところ、入力を受け取って同じように出力するだけです。もちろんあまり役に立ちません。次に、現在の作業ディレクトリを表示するpwdや終了するexitなどの簡単なコマンドに応答してみましょう。getcwdシステムコールとreturnを使えば簡単です:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(){
char input[1024];
char* r;
// use prompt to mark the executing program
printf("> ");
while((r = gets(input)) != NULL) {
if (strcmp(r, "pwd") == 0) {
char cwd[80];
printf("%s\n", getcwd(cwd, sizeof(cwd)));
}
if (strcmp(r, "exit") == 0) {
return 0;
}
printf("> ");
}
return 0;
}
This time we just simply ignore all other commands. We also added the prompts to mark a shell environment and distinguish the inputs from outputs (You can give your shell a name by placing it before > like resh > !).
这次我们只是简单地忽略所有其他命令。我们还添加了提示符来标识 Shell 环境,并区分输入和输出(你可以在 > 前加上你的 Shell 名称,比如 resh > !)。
今回は他のすべてのコマンドをシンプルに無視します。シェル環境を示し入出力を区別するためのプロンプトも追加しました(> の前にシェル名を付けることができます、例:resh > !)。
For now we are only able to compare the single command, while in real world there are multiple parameters passed into a command, hence a parser that split the input into parameters is required. To organize the codes, we extract the parsing and command execting process into seperate function, named parse() and route(), and store the parameters in params:
目前我们只能比较单个命令,而现实中一个命令会有多个参数,因此需要一个将输入分割为参数的解析器。为了整理代码,我们将解析和命令执行过程分别提取到 parse() 和 route() 函数中,并将参数存储在 params 中:
今のところ単一のコマンドしか比較できませんが、実際にはコマンドに複数のパラメータが渡されます。そのため、入力をパラメータに分割するパーサーが必要です。コードを整理するために、パースとコマンド実行プロセスをそれぞれparse()とroute()という別々の関数に抽出し、パラメータをparamsに格納します:
int main(){
char input[1024];
char* params[128];
char* r;
printf("> ");
while((r = gets(input)) != NULL) {
parse(input, params);
route(params);
printf("> ");
}
return 0;
}
We use strtok() in parser(), which is designed to split the string into tokens by calling repeatly and returns the start pointer of one token in each call. The first call requires the start pointer of string as the first argument, while NULL for latter calls. Refer to the manual entry of strtok() for more details.
我们在 parser() 中使用 strtok(),该函数通过重复调用将字符串分割为 token,每次调用返回一个 token 的起始指针。第一次调用需要提供字符串的起始指针作为第一个参数,后续调用传入 NULL。更多详情请参考 strtok() 的手册条目。
parser()ではstrtok()を使用します。これは繰り返し呼び出すことで文字列をトークンに分割し、各呼び出しで1つのトークンの開始ポインタを返します。最初の呼び出しでは文字列の開始ポインタを第1引数として指定し、以降の呼び出しではNULLを渡します。詳細はstrtok()のマニュアルを参照してください。
int parse(char* input, char** params){
int index = 0;
char* param;
params[index++] = strtok(input, " ");
while ((param = (strtok(NULL, " ")))){
params[index++] = param;
}
params[index] = NULL;
return index;
}
We print the parameters sequentially to build a naive echo:
我们按顺序打印参数来构建一个简单的 echo:
パラメータを順番に表示して、素朴なechoを構築します:
int route(char** params){
char* cmd = params[0];
if (strcmp(cmd, "echo") == 0) {
int index = 1;
while(params[index]) {
printf("%s ", params[index]);
index++;
}
printf("\n");
}
if (strcmp(cmd, "pwd") == 0) {
char cwd[80];
printf("%s\n", getcwd(cwd, sizeof(cwd)));
}
if (strcmp(cmd, "exit") == 0) {
// exit(0) to exit from the process,
// instead of just return to main function.
exit(0);
}
return 0;
}
Notice that our echo does not simply print the original string but parse it first, as a result any redundant blanks between parameters will be omitted:
注意我们的 echo 不是简单地打印原始字符串,而是先解析它,因此参数之间多余的空格会被省略:
私たちのechoは元の文字列をそのまま表示するのではなく、最初にパースするため、パラメータ間の余分なスペースは省略されることに注意してください:
gcc sh.c -o sh
./sh
> echo 1 2 3
1 2 3
The complete codes example can be found in parameters_parser.c.
完整的代码示例可以在 parameters_parser.c 中找到。
完全なコード例はparameters_parser.cにあります。
For users, it is more common to invoke some powerful programs instead of those shell built-in, such as find, grep. That is, we must empower our shell the ability to execute other programs, which require system calls fork() and exec(). We'll introduce these in next section, and equip our shell with the ability of all executables.
对于用户来说,更常见的是调用一些强大的程序而非 Shell 内置命令,比如 find、grep。因此,我们必须赋予 Shell 执行其他程序的能力,这需要使用系统调用 fork() 和 exec()。我们将在下一节介绍这些内容,并为我们的 Shell 装备执行所有可执行文件的能力。
ユーザーにとって、findやgrepなどのシェル組み込みコマンドではなく、強力なプログラムを呼び出すことの方が一般的です。そのため、シェルに他のプログラムを実行する能力を与える必要があります。これにはfork()とexec()システムコールが必要です。次のセクションでこれらを紹介し、シェルにすべての実行可能ファイルを実行する能力を装備します。
fork-exec Routinefork-exec 流程fork-exec ルーティンBesides invoking the system call directly, the more common case is to call another executable program to do the job, such as ls, grep, sed, and all the powerful tools. Unlike the Python's subprocess library or Go's os/exec package, which create a different new process, the Unix system call exec() behaves different: It does not create a new one but just change the current process to the target, and as a result, the shell will no longer exists if we just exec() to another program. So what should we do to both kept the shell but also run another program?
除了直接调用系统调用外,更常见的情况是调用其他可执行程序来完成工作,比如 ls、grep、sed 等强大的工具。与 Python 的 subprocess 库或 Go 的 os/exec 包(它们会创建一个新进程)不同,Unix 系统调用 exec() 的行为不同:它不会创建新进程,而只是将当前进程替换为目标程序,因此如果我们直接对另一个程序 exec(),Shell 就不存在了。那么我们如何既保留 Shell 又运行另一个程序呢?
ls、grep、sedなどの強力なツールのような別の実行可能プログラムを呼び出す方がより一般的です。PythonのsubprocessライブラリやGoのos/execパッケージ(新しいプロセスを作成する)とは異なり、Unixシステムコールexec()は異なる動作をします:新しいプロセスを作成するのではなく、現在のプロセスをターゲットに変更するため、別のプログラムにexec()するとシェルが消えてしまいます。では、シェルを維持しながら別のプログラムを実行するにはどうすればよいでしょうか?
The answer is we must call fork() to create a child process before we exec(), then we let the child process to exec(), by which the shell keeps running. The fork() command creates a child process that copys the parent process exactly, and starts at the next instruction right after fork(). To help the process distinguish which of process is executing, different return values are assigned to parent process (returns the process id of child) and child process (returns 0) of fork(). Here is a demo showing how it works:
答案是必须在 exec() 之前调用 fork() 创建一个子进程,然后让子进程 exec(),这样 Shell 就能保持运行。fork() 命令创建一个完全复制父进程的子进程,从 fork() 之后的下一条指令开始执行。为了帮助进程区分哪个进程在执行,fork() 对父进程(返回子进程 ID)和子进程(返回 0)赋予不同的返回值。下面是一个展示其工作原理的示例:
答えは、exec()の前にfork()を呼び出して子プロセスを作成し、子プロセスにexec()させることです。これによりシェルは実行し続けます。fork()コマンドは親プロセスを完全にコピーした子プロセスを作成し、fork()の直後の命令から開始します。どのプロセスが実行中かを区別するために、fork()は親プロセス(子のプロセスIDを返す)と子プロセス(0を返す)に異なる戻り値を割り当てます。動作を示すデモを以下に示します:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(){
int r = fork();
if (r < 0) {
perror("fork failed");
} else if (r == 0) {
printf("I'm the child with pid=%d, parent pid=%d\n", getpid(), getppid());
} else {
printf("I'm the parent with pid=%d\n", getpid());
wait(NULL);
}
}
Branch structure is created to make different behaviours of parent and child. Here system call wait(NULL) is used to wait until the child completes (try to remove it to see what happens!), we're going to introduce wait() (and the process status model) in detail since it plays important rule in the design of background mode.
分支结构用于实现父子进程的不同行为。这里使用系统调用 wait(NULL) 等待子进程完成(试试删除它看看会发生什么!),我们将详细介绍 wait()(以及进程状态模型),因为它在后台模式设计中起着重要作用。
分岐構造は親プロセスと子プロセスの異なる動作を実現するために作成されます。ここでシステムコールwait(NULL)を使用して子プロセスが完了するまで待機します(削除して何が起こるか試してみましょう!)。バックグラウンドモードの設計で重要な役割を果たすため、wait()(およびプロセスステータスモデル)を詳しく紹介します。
We can exec() in the child progress to execute other programs (there are several types of exec() in the child process with different parameters but their semantics are the same, we pick execvp() here, which pass the command and parameters in an array using NULL as mark of end):
我们可以在子进程中 exec() 来执行其他程序(有几种不同参数的 exec() 类型,但语义相同,这里我们选择 execvp(),它将命令和参数以数组方式传递,用 NULL 作为结束标记):
子プロセスでexec()して他のプログラムを実行できます(exec()には異なるパラメータを持つ複数の種類がありますが、セマンティクスは同じです。ここではexecvp()を選択し、コマンドとパラメータを配列で渡し、NULLを終端マーカーとして使用します):
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(){
int r = fork();
if (r < 0) {
perror("fork failed");
} else if (r == 0) {
printf("I'm the child with pid=%d, parent pid=%d\n", getpid(), getppid());
char* params[3] = {"/bin/ls", "-l", NULL};
execvp(params[0], params);
} else {
printf("I'm the parent with pid=%d\n", getpid());
wait(NULL);
}
}