Linuxで簡単なシェルを自作してみた(1)|外部コマンドの実行
目次
自作したと言っても初歩的な機能を実装しただけであり、このシェルを常用するつもりはない。
あくまでシェルを勉強したかった・深く理解したかっただけだ。
すでに世には bash などの優れたシェルが存在する。
これらのシェルを使う方が、私の作ったおもちゃのようなシェルを使うよりも、断然良い。
基本的なシェル(外部コマンドの実行)
シェルの基本機能として、最低限、以下が必要だ。
- 文字列を入力
- 入力された文字列をコマンドとして実行する
- コマンドの実行が終わったら、1. に戻る
基本的なシェルは、シェルとしてコマンドラインを解釈し、ユーザーから入力されたコマンドを実行する簡単なプログラムである。C 言語を使った簡単なシェルの例を示す。
基本的なシェルのソースコード (C言語)
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <unistd.h> // fork, execvp
5#include <sys/wait.h> // waitpid
6
7#define MAX_CMD_LEN 1024
8#define MAX_ARGS 100
9
10// コマンドをスペースで分割する関数
11void parse_command(char *cmd, char *argv[]) {
12 char *token = strtok(cmd, " \n");
13 int i = 0;
14 while (token != NULL) {
15 argv[i++] = token;
16 token = strtok(NULL, " \n");
17 }
18 argv[i] = NULL; // 最後の引数をNULLで終わらせる
19}
20
21int main() {
22 char cmd[MAX_CMD_LEN]; // コマンド入力用のバッファ
23 char *argv[MAX_ARGS]; // コマンド引数
24 pid_t pid;
25 int status;
26
27 // シェルのメインループ
28 while (1) {
29 // プロンプトを表示
30 fprintf(stderr, "mysh> ");
31
32 // コマンドを読み込む
33 if (fgets(cmd, sizeof(cmd), stdin) == NULL) {
34 if (feof(stdin)) {
35 break; // EOFが入力された場合、終了
36 }
37 perror("fgets failed");
38 continue;
39 }
40
41 // コマンドを解析
42 parse_command(cmd, argv);
43
44 // 空のコマンドの場合は次の入力へスキップ
45 if (argv[0] == NULL) {
46 continue;
47 }
48
49 // 終了コマンドの場合
50 if (strcmp(argv[0], "exit") == 0) {
51 break;
52 }
53
54 // 子プロセスを作成してコマンドを実行
55 pid = fork();
56 if (pid == -1) {
57 perror("fork failed");
58 exit(1);
59 }
60
61 // 子プロセス
62 if (pid == 0) {
63 // execvpを使用してコマンドを実行
64 if (execvp(argv[0], argv) == -1) {
65 perror("exec failed");
66 exit(1);
67 }
68 }
69
70 // 親プロセス
71 if (waitpid(pid, &status, 0) == (pid_t)-1) { // 子プロセスの終了を待つ
72 perror("waitpid failed");
73 exit(1);
74 }
75 }
76
77 printf("Exiting shell...\n");
78 return 0;
79}
説明
全体の処理の流れ
ソースコード内のexecvp
,waitpid
は、 図中のexec
,wait
に対応している。入力待ち
標準エラー出力に対してプロンプトの文字列 "mysh> " を出力して、入力を待ち受ける。 標準エラー出力はバッファリングが効かないので即時表示される。コマンドの入力
fgets()
を使ってユーザーの入力を受け取る。EOF (Ctrl + D) の入力でシェルを終了する。(リファレンスより抜粋)
#include <stdio.h>
int fgetc(FILE *stream);
fgetc()
は、ストリームから次の文字を読み取り、それをunsigned char
型にキャストしてからint
型として返す。 ファイルの終端またはエラー時にはEOF
を返す。コマンドの解析
入力された文字列(コマンド)を解析して、argv 配列を設定する。この処理は、後続のexecvp()
実行のための準備である。parse_command
(ユーザ定義関数) を呼び出すことで、cmd はスペースで分割され(スペースは\0で置き換えられる)、argv には分割された単語(トークン)の先頭位置へのポインタが設定される(下図参照)。シェルの終了
exit
が入力された場合、シェルは終了する。当シェルに組み込んだコマンドであるが、組み込みコマンドについては別途詳細に実装する。子プロセスの生成
fork()
を使って新しいプロセス(子プロセス)を生成する。(リファレンスより抜粋)
#include <unistd.h>
pid_t fork(void);
fork()
は、呼び出し元のプロセスを複製することで新しいプロセスを作成する。新しく作成されたプロセスは「子プロセス」と呼ばれ、呼び出し元のプロセスは「親プロセス」と呼ばれる。
戻り値:
成功した場合、親プロセスには子プロセスのPID
(プロセスID)が返され、子プロセスには0
が返される。 失敗した場合、親プロセスには-1
が返され、子プロセスは作成されず、エラーを示すためにerrno
が設定される。子プロセスで入力コマンドを実行
子プロセス内でexecvp()
を使って入力された文字列(コマンド)を実行する。execvp()
は、指定されたコマンドをシステムで実行するための関数で、引数リストを渡すことができる。(リファレンスより抜粋)
#include <unistd.h>
int execvp(const char *file, char *const argv[]);
exec()
ファミリーの関数は、現在のプロセスイメージを新しいプロセスイメージに置き換える。 第一引数 file は実行するファイルの名前、 第二引数 argv は新しいプログラムで使用可能な引数リスト(引数へのポインタ配列)。 各引数はヌル終端された文字列となっていること。 また、このポインタ配列の最後の要素は、ヌルポインタとしなければならない。
第一引数のファイル名にスラッシュ/
が含まれていない場合、実行可能ファイルを検索する際にPATH環境変数に設定されたディレクトリパス内で検索される。
戻り値:exec()
は、エラーが発生した場合にのみ戻り値を返す。戻り値は-1
であり、エラーを示すためにerrno
が設定される。子プロセスの終了待ち
親プロセスはwaitpid()
を使って子プロセスが終了するのを待つ。(リファレンスより抜粋)
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *_Nullable wstatus, int options);
このシステムコールは呼び出し元プロセスの子プロセスにおける状態変化を待機し、子プロセスの終了など、状態変化した子プロセスに関する情報を取得するために使用される。 子プロセスが終了した場合、wait を実行することでシステムがその子プロセスに関連付けられたリソースを解放できる。 wait が実行されない場合、終了した子プロセスは「ゾンビ」状態のままとなる。waitpid()
システムコールは、第一引数 pid で指定された子プロセスが状態変化するまで、呼び出し元スレッドの実行を一時停止する。
第二引数 wstatus が NULL でない場合、waitpid()
は状態情報を指し示された int 型変数に格納する。 この整数はマクロを使用して調査することができる。(省略)
デフォルトではwaitpid()
は終了した子プロセスのみを待機するが、この動作は第三引数 options で変更可能。(省略)
戻り値:
成功時は状態変化した子プロセスのPID
(プロセスID)を返す。 失敗時は-1
を返す。
使用方法
- 上記のコードをファイルに保存(例えば
myshell.c
)する。 - コンパイルする:
1gcc myshell.c -o myshell
- 実行する:
1./myshell
サンプル実行
1mysh> ls
2Desktop Documents Downloads myshell.c
3mysh> echo Hello, world!
4Hello, world!
5mysh> exit
6Exiting shell...
このシェルは、基本的なコマンド実行の仕組みを理解するのに役立つ。