Linuxで簡単なシェルを自作してみた|外部コマンド、リダイレクション、パイプラインの実装方法

自作したLinuxシェルのソースコードを通じて、シェルの基本的な動作から、外部コマンドの実行、バックグラウンド処理、リダイレクション、パイプラインの実装方法までを説明する。

基本的なシェル(外部コマンドの実行)

基本的なシェルのソースコードは、シェルとしてコマンドラインを解釈し、ユーザーから入力されたコマンドを実行する簡単なプログラムである。ここでは、C 言語を使った簡単なシェルの例を示す。

このシェルは、ユーザーからコマンドを読み込み、システムの fork() と exec() を使ってそのコマンドを実行する。

基本的なシェルのソースコード (C言語)

 1#include <stdio.h>
 2#include <stdlib.h>
 3#include <string.h>
 4#include <unistd.h>
 5#include <sys/wait.h>
 6
 7#define MAX_CMD_LEN 1024
 8#define MAX_ARGS 100
 9
10// コマンドをスペースで分割する関数
11void parse_command(char *cmd, char **args) {
12    char *token = strtok(cmd, " \n");
13    int i = 0;
14    while (token != NULL) {
15        args[i++] = token;
16        token = strtok(NULL, " \n");
17    }
18    args[i] = NULL; // 最後の引数をNULLで終わらせる
19}
20
21int main() {
22    char cmd[MAX_CMD_LEN]; // コマンド入力用のバッファ
23    char *args[MAX_ARGS];   // コマンド引数
24    pid_t pid;
25    int status;
26
27    // シェルのメインループ
28    while (1) {
29        // プロンプトを表示
30        printf("mysh> ");
31        fflush(stdout);
32
33        // コマンドを読み込む
34        if (fgets(cmd, sizeof(cmd), stdin) == NULL) {
35            if (feof(stdin)) {
36                break; // EOFが入力された場合、終了
37            }
38            perror("fgets failed");
39            continue;
40        }
41
42        // コマンドを解析
43        parse_command(cmd, args);
44
45        // 終了コマンドの場合
46        if (args[0] != NULL && strcmp(args[0], "exit") == 0) {
47            break;
48        }
49
50        // 子プロセスを作成してコマンドを実行
51        pid = fork();
52        if (pid == -1) {
53            perror("fork failed");
54            exit(1);
55        }
56
57        if (pid == 0) { // 子プロセス
58            // execvpを使用してコマンドを実行
59            if (execvp(args[0], args) == -1) {
60                perror("exec failed");
61                exit(1);
62            }
63        } else { // 親プロセス
64            waitpid(pid, &status, 0); // 子プロセスの終了を待つ
65        }
66    }
67
68    printf("Exiting shell...\n");
69    return 0;
70}

説明

  • コマンドの入力と解析
    fgets() 関数を使ってユーザーの入力を受け取り、その後、strtok() 関数でコマンドをスペースで分割して引数に変換する。

  • fork()execvp()
    fork() を使って新しいプロセス(子プロセス)を作成する。子プロセス内で execvp() を使って実際のコマンドを実行する。execvp() は、指定されたコマンドをシステムで実行するための関数で、引数リストを渡すことができる。

  • waitpid()
    親プロセスは waitpid() を使って子プロセスが終了するのを待つ。

  • exit コマンド
    exit コマンドが入力された場合、シェルは終了する。

使用方法

  1. 上記のコードをファイルに保存(例えば myshell.c)する。
  2. コンパイルする:
    1gcc myshell.c -o myshell
    
  3. 実行する:
    1./myshell
    

サンプル実行

1mysh> ls
2Desktop  Documents  Downloads  myshell.c
3mysh> echo Hello, world!
4Hello, world!
5mysh> exit
6Exiting shell...

このシェルは、シンプルですが、基本的なコマンド実行の仕組みを理解するのに役立つ。

バックグラウンド実行の機能を追加

バックグラウンド実行の機能を追加するには、ユーザーがコマンドの最後に & を入力した場合、そのコマンドをバックグラウンドで実行するようにシェルを変更する。 バックグラウンドで実行する際、シェルは新しいプロセスを生成し、そのプロセスが終了するのを待機せずに次のコマンドを受け付けるようにする。 ただしバックグラウンドプロセスが終了したときに親プロセスが waitpid() を呼び出す必要はある(放置したままだとゾンビプロセスとして残ってしまうため)。 SIGCHLD シグナルをハンドリングして、親プロセスがバックグラウンドプロセスの終了の通知を受け取り、waitpid() を呼び出すようにする。

バックグラウンド実行をサポートするために、シェルに以下の変更を加える:

  1. コマンドの末尾に & があるかどうかを確認。
  2. バックグラウンド実行したプロセスから SIGCHLD シグナルを受け取り、バックグラウンドプロセスに対して waitpid() を実行して消滅させる。
  3. & があれば、プロセスをバックグラウンドで実行し、親プロセスはその終了を待たずに次のコマンドを実行可能とする。
  4. & がなければ、通常通りフォアグラウンドで実行し、終了を待つ。

修正したシェルのコード

  1#include <stdio.h>
  2#include <stdlib.h>
  3#include <string.h>
  4#include <unistd.h>
  5#include <sys/wait.h>
  6#include <signal.h>
  7
  8#define MAX_CMD_LEN 1024
  9#define MAX_ARGS 100
 10
 11// コマンドをスペースで分割する関数
 12void parse_command(char *cmd, char **args) {
 13    char *token = strtok(cmd, " \n");
 14    int i = 0;
 15    while (token != NULL) {
 16        args[i++] = token;
 17        token = strtok(NULL, " \n");
 18    }
 19    args[i] = NULL; // 最後の引数をNULLで終わらせる
 20}
 21
 22// コマンドの末尾が '&' かどうかを確認し、'&' を取り除いて戻り値として返す
 23int is_background_command(char **args) {
 24    int i = 0;
 25    while (args[i] != NULL) {
 26        i++;
 27    }
 28    if (i > 0 && strcmp(args[i - 1], "&") == 0) {
 29        args[i - 1] = NULL; // '&' をコマンド引数から削除
 30        return 1; // バックグラウンド実行フラグを返す
 31    }
 32    return 0;
 33}
 34
 35// SIGCHLD シグナルハンドラ(バックグラウンドプロセスの終了を処理)
 36void sigchld_handler(int signo) {
 37    int status;
 38    // 終了した子プロセスの状態を取得
 39    while (waitpid(-1, &status, WNOHANG) > 0) {
 40        // 子プロセスがシグナルで終了した場合はそのシグナル番号を出力
 41        if (WIFSIGNALED(status)) {
 42            printf("Child process terminated by signal %d\n", WTERMSIG(status));
 43        }
 44    }
 45}
 46
 47int main() {
 48    char cmd[MAX_CMD_LEN]; // コマンド入力用のバッファ
 49    char *args[MAX_ARGS];   // コマンド引数
 50    pid_t pid;
 51    int status;
 52
 53    // SIGCHLD シグナルのハンドラを設定
 54    signal(SIGCHLD, sigchld_handler);
 55
 56    // シェルのメインループ
 57    while (1) {
 58        // プロンプトを表示
 59        printf("mysh> ");
 60        fflush(stdout);
 61
 62        // コマンドを読み込む
 63        if (fgets(cmd, sizeof(cmd), stdin) == NULL) {
 64            if (feof(stdin)) {
 65                break; // EOFが入力された場合、終了
 66            }
 67            perror("fgets failed");
 68            continue;
 69        }
 70
 71        // コマンドを解析
 72        parse_command(cmd, args);
 73
 74        // 空のコマンドの場合は次の入力へスキップ
 75        if (args[0] == NULL) {
 76            continue;  // 空コマンドは何もせず次に進む
 77        }
 78
 79        // 終了コマンドの場合
 80        if (args[0] != NULL && strcmp(args[0], "exit") == 0) {
 81            break;
 82        }
 83
 84        // バックグラウンド実行かどうかを判定
 85        int background = is_background_command(args);
 86
 87        // 子プロセスを作成してコマンドを実行
 88        pid = fork();
 89        if (pid == -1) {
 90            perror("fork failed");
 91            exit(1);
 92        }
 93
 94        if (pid == 0) { // 子プロセス
 95            // execvpを使用してコマンドを実行
 96            if (execvp(args[0], args) == -1) {
 97                perror("exec failed");
 98                exit(1);
 99            }
100        } else { // 親プロセス
101            if (!background) {
102                // バックグラウンドでなければ、親は子プロセスの終了を待つ
103                waitpid(pid, &status, 0);
104            } else {
105                // バックグラウンドの場合、親は終了を待たずに次のコマンドを受け付ける
106                printf("Background process started with PID: %d\n", pid);
107            }
108        }
109    }
110
111    printf("Exiting shell...\n");
112    return 0;
113}

主な変更点

  1. バックグラウンド実行の判定 is_background_command() 関数を追加し、コマンドの最後に & があるかどうかを確認する。& があれば、その部分を削除し、コマンドをバックグラウンドで実行するフラグを返す。

  2. **SIGCHLD シグナルハンドラの追加 signal() 関数を追加し、SIGCHLD シグナルを受け取ったら sigchld_handler 関数(シグナルハンドラ)を呼び出す。シグナルハンドラでは子プロセス(バックグラウンドプロセス)に対して waitpid() を実行して、子プロセスを消滅させる。

  3. バックグラウンド実行の処理 バックグラウンドで実行する場合、親プロセスは waitpid() を呼ばず、すぐに次のコマンドを受け付ける。また、バックグラウンドで実行されるプロセスの PID を表示する。

  4. バックグラウンド実行の判定 コマンドがバックグラウンドで実行される場合(& がコマンドの最後にある場合)は、親プロセスは waitpid() を使わずに、次のコマンドをすぐに受け付ける。

使用方法

  1. 上記のコードをファイルに保存(例えば myshell.c)する。
  2. コンパイルする:
    1gcc myshell.c -o myshell
    
  3. 実行する:
    1./myshell
    

サンプル実行

1mysh> ls
2Desktop  Documents  Downloads  myshell.c
3mysh> sleep 5 &
4Background process started with PID: 12345
5mysh> echo Hello, world!
6Hello, world!
7mysh> exit
8Exiting shell...

上記の例では、sleep 5 & をバックグラウンドで実行し、その後、すぐに次のコマンド(echo Hello, world!)を実行することができる。 バックグラウンドプロセスが終了するのをシェルが待つことなく、シェルは次のコマンドを受け付けることができる。

組み込みコマンド cd を追加

組み込みコマンド cd の機能をシェルに追加するには、cd コマンドが呼ばれたときに、プロセスを新たにフォークして実行するのではなく、シェル自身のプロセスでディレクトリを変更する必要がある。 このためには、chdir() システムコールを使用してカレントディレクトリを変更する。

cd コマンドの特徴

  • cd コマンドは、シェルプロセス内で実行され、シェルのカレントディレクトリを変更する。新たなプロセスを作成して実行するのではなく、現在実行中のシェルのプロセスでディレクトリ変更を行う。
  • 引数に指定されたパスに移動する。引数がない場合は、通常ホームディレクトリ (~) に移動する。
  • 引数が無効な場合(例えば、存在しないディレクトリを指定した場合)は、エラーメッセージを表示する。

修正したシェルのコード

以下に、cd コマンドを組み込んだシェルのコードを示す。

  1#include <stdio.h>
  2#include <stdlib.h>
  3#include <string.h>
  4#include <unistd.h>
  5#include <sys/wait.h>
  6#include <signal.h>
  7
  8#define MAX_CMD_LEN 1024
  9#define MAX_ARGS 100
 10
 11// コマンドをスペースで分割する関数
 12void parse_command(char *cmd, char **args) {
 13    char *token = strtok(cmd, " \n");
 14    int i = 0;
 15    while (token != NULL) {
 16        args[i++] = token;
 17        token = strtok(NULL, " \n");
 18    }
 19    args[i] = NULL; // 最後の引数をNULLで終わらせる
 20}
 21
 22// コマンドの末尾が '&' かどうかを確認し、'&' を取り除いて戻り値として返す
 23int is_background_command(char **args) {
 24    int i = 0;
 25    while (args[i] != NULL) {
 26        i++;
 27    }
 28    if (i > 0 && strcmp(args[i - 1], "&") == 0) {
 29        args[i - 1] = NULL; // '&' をコマンド引数から削除
 30        return 1; // バックグラウンド実行フラグを返す
 31    }
 32    return 0;
 33}
 34
 35// SIGCHLD シグナルハンドラ(バックグラウンドプロセスの終了を処理)
 36void sigchld_handler(int signo) {
 37    int status;
 38    // 終了した子プロセスの状態を取得
 39    while (waitpid(-1, &status, WNOHANG) > 0) {
 40        // 子プロセスがシグナルで終了した場合はそのシグナル番号を出力
 41        if (WIFSIGNALED(status)) {
 42            printf("Child process terminated by signal %d\n", WTERMSIG(status));
 43        }
 44    }
 45}
 46
 47// 組み込みコマンド cd を処理する関数
 48void execute_cd(char **args) {
 49    if (args[1] == NULL) {
 50        // 引数なしの場合はホームディレクトリに移動
 51        const char *home_dir = getenv("HOME");
 52        if (home_dir == NULL) {
 53            perror("cd: HOME not set");
 54        } else {
 55            if (chdir(home_dir) != 0) {
 56                perror("cd failed");
 57            }
 58        }
 59    } else {
 60        // 引数が指定された場合、そのディレクトリに移動
 61        if (chdir(args[1]) != 0) {
 62            perror("cd failed");
 63        }
 64    }
 65}
 66
 67int main() {
 68    char cmd[MAX_CMD_LEN]; // コマンド入力用のバッファ
 69    char *args[MAX_ARGS];   // コマンド引数
 70    pid_t pid;
 71    int status;
 72
 73    // SIGCHLD シグナルのハンドラを設定
 74    signal(SIGCHLD, sigchld_handler);
 75
 76    // シェルのメインループ
 77    while (1) {
 78        // プロンプトを表示
 79        printf("mysh> ");
 80        fflush(stdout);
 81
 82        // コマンドを読み込む
 83        if (fgets(cmd, sizeof(cmd), stdin) == NULL) {
 84            if (feof(stdin)) {
 85                break; // EOFが入力された場合、終了
 86            }
 87            perror("fgets failed");
 88            continue;
 89        }
 90
 91        // コマンドを解析
 92        parse_command(cmd, args);
 93
 94        // 空のコマンドの場合は次の入力へスキップ
 95        if (args[0] == NULL) {
 96            continue;  // 空コマンドは何もせず次に進む
 97        }
 98
 99        // 終了コマンドの場合
100        if (args[0] != NULL && strcmp(args[0], "exit") == 0) {
101            break;
102        }
103
104        // cd コマンドの場合、組み込みで処理
105        if (args[0] != NULL && strcmp(args[0], "cd") == 0) {
106            execute_cd(args);
107            continue;
108        }
109
110        // バックグラウンド実行かどうかを判定
111        int background = is_background_command(args);
112
113        // 子プロセスを作成してコマンドを実行
114        pid = fork();
115        if (pid == -1) {
116            perror("fork failed");
117            exit(1);
118        }
119
120        if (pid == 0) { // 子プロセス
121            // execvpを使用してコマンドを実行
122            if (execvp(args[0], args) == -1) {
123                perror("exec failed");
124                exit(1);
125            }
126        } else { // 親プロセス
127            if (!background) {
128                // バックグラウンドでなければ、親は子プロセスの終了を待つ
129                waitpid(pid, &status, 0);
130            } else {
131                // バックグラウンドの場合、親は終了を待たずに次のコマンドを受け付ける
132                printf("Background process started with PID: %d\n", pid);
133            }
134        }
135    }
136
137    printf("Exiting shell...\n");
138    return 0;
139}

主な変更点

  1. cd コマンドの処理

    • execute_cd() 関数を追加した。この関数は、引数がない場合はホームディレクトリに移動し、引数が指定されている場合はそのディレクトリに移動する。ディレクトリ変更には chdir() システムコールを使用する。
    • ホームディレクトリは getenv("HOME") を使って取得します。
  2. cd コマンドの判定

    • ユーザーが cd を入力した場合、シェルは execute_cd() を呼び出してディレクトリを変更する。cd コマンドが実行された場合、fork()execvp() の処理はスキップされる。
  3. ディレクトリ変更のエラーハンドリング

    • 指定されたディレクトリが存在しない場合や、HOME 環境変数が設定されていない場合にエラーメッセージを表示する。

使用方法

  1. 上記のコードをファイルに保存(例えば myshell.c)する。
  2. コンパイルする:
    1gcc myshell.c -o myshell
    
  3. 実行する:
    1./myshell
    

サンプル実行

1mysh> cd /home/user
2mysh> pwd
3/home/user
4mysh> cd
5mysh> cd /nonexistent
6cd failed: No such file or directory
7mysh> exit
8Exiting shell...

このように、cd コマンドはシェル内で動作し、引数に指定されたディレクトリに移動する。引数なしの場合はホームディレクトリに移動し、無効なディレクトリが指定された場合はエラーメッセージを表示する。

リダイレクション機能を追加

リダイレクション(入力リダイレクション、出力リダイレクション)の機能をシェルに追加することで、ユーザーがコマンドの入力や出力をファイルにリダイレクトできるようになる。 ここでは、シェルに以下のリダイレクション機能を追加する。

追加するリダイレクション機能

  1. 出力リダイレクション (>): コマンドの標準出力を指定したファイルに書き込む。
  2. 入力リダイレクション (<): コマンドの標準入力を指定したファイルから読み込む。
  3. 追記リダイレクション (>>): コマンドの標準出力を指定したファイルに追記する。

実装の概要

  1. コマンド引数の解析 コマンドを解析して、リダイレクション記号(>, <, >>)が含まれているかを確認する。これらの記号が見つかれば、対応するファイルの開き方を決定する。

  2. fork()execvp() の使用 fork() で子プロセスを作成し、リダイレクションが指定された場合、子プロセス内で標準入力や標準出力をファイルにリダイレクトする。

  3. リダイレクションの実装

    • 出力リダイレクションは freopen() または open() を使用してファイルにリダイレクトする。
    • 入力リダイレクションは freopen() または open() を使用してファイルから入力をリダイレクトする。

修正したシェルのコード

以下に、リダイレクション機能を追加したシェルのコードを示す。

  1#include <stdio.h>
  2#include <stdlib.h>
  3#include <string.h>
  4#include <unistd.h>
  5#include <sys/wait.h>
  6#include <signal.h>
  7#include <fcntl.h>
  8
  9#define MAX_CMD_LEN 1024
 10#define MAX_ARGS 100
 11
 12// コマンドをスペースで分割する関数
 13void parse_command(char *cmd, char **args) {
 14    char *token = strtok(cmd, " \n");
 15    int i = 0;
 16    while (token != NULL) {
 17        args[i++] = token;
 18        token = strtok(NULL, " \n");
 19    }
 20    args[i] = NULL; // 最後の引数をNULLで終わらせる
 21}
 22
 23// コマンドの末尾が '&' かどうかを確認し、'&' を取り除いて戻り値として返す
 24int is_background_command(char **args) {
 25    int i = 0;
 26    while (args[i] != NULL) {
 27        i++;
 28    }
 29    if (i > 0 && strcmp(args[i - 1], "&") == 0) {
 30        args[i - 1] = NULL; // '&' をコマンド引数から削除
 31        return 1; // バックグラウンド実行フラグを返す
 32    }
 33    return 0;
 34}
 35
 36// SIGCHLD シグナルハンドラ(バックグラウンドプロセスの終了を処理)
 37void sigchld_handler(int signo) {
 38    int status;
 39    // 終了した子プロセスの状態を取得
 40    while (waitpid(-1, &status, WNOHANG) > 0) {
 41        // 子プロセスがシグナルで終了した場合はそのシグナル番号を出力
 42        if (WIFSIGNALED(status)) {
 43            printf("Child process terminated by signal %d\n", WTERMSIG(status));
 44        }
 45    }
 46}
 47
 48// 組み込みコマンド cd を処理する関数
 49void execute_cd(char **args) {
 50    if (args[1] == NULL) {
 51        // 引数なしの場合はホームディレクトリに移動
 52        const char *home_dir = getenv("HOME");
 53        if (home_dir == NULL) {
 54            perror("cd: HOME not set");
 55        } else {
 56            if (chdir(home_dir) != 0) {
 57                perror("cd failed");
 58            }
 59        }
 60    } else {
 61        // 引数が指定された場合、そのディレクトリに移動
 62        if (chdir(args[1]) != 0) {
 63            perror("cd failed");
 64        }
 65    }
 66}
 67
 68// リダイレクションのための関数
 69void handle_redirection(char **args) {
 70    int i = 0;
 71    int input_redirect = 0;
 72    int output_redirect = 0;
 73    int append_redirect = 0;
 74    char *input_file = NULL;
 75    char *output_file = NULL;
 76
 77    // リダイレクションを探す
 78    while (args[i] != NULL) {
 79        if (strcmp(args[i], "<") == 0) {
 80            input_redirect = 1;
 81            input_file = args[i + 1];
 82            args[i] = NULL; // リダイレクション記号を取り除く
 83        } else if (strcmp(args[i], ">") == 0) {
 84            output_redirect = 1;
 85            output_file = args[i + 1];
 86            args[i] = NULL;
 87        } else if (strcmp(args[i], ">>") == 0) {
 88            append_redirect = 1;
 89            output_file = args[i + 1];
 90            args[i] = NULL;
 91        }
 92        i++;
 93    }
 94
 95    // 入力リダイレクション
 96    if (input_redirect) {
 97        int fd = open(input_file, O_RDONLY);
 98        if (fd == -1) {
 99            perror("Input redirection failed");
100            exit(1);
101        }
102        dup2(fd, STDIN_FILENO); // 標準入力をファイルにリダイレクト
103        close(fd);
104    }
105
106    // 出力リダイレクション(上書き)
107    if (output_redirect) {
108        int fd = open(output_file, O_WRONLY | O_CREAT | O_TRUNC, 0644);
109        if (fd == -1) {
110            perror("Output redirection failed");
111            exit(1);
112        }
113        dup2(fd, STDOUT_FILENO); // 標準出力をファイルにリダイレクト
114        close(fd);
115    }
116
117    // 出力リダイレクション(追記)
118    if (append_redirect) {
119        int fd = open(output_file, O_WRONLY | O_CREAT | O_APPEND, 0644);
120        if (fd == -1) {
121            perror("Append redirection failed");
122            exit(1);
123        }
124        dup2(fd, STDOUT_FILENO); // 標準出力をファイルに追記リダイレクト
125        close(fd);
126    }
127}
128
129int main() {
130    char cmd[MAX_CMD_LEN]; // コマンド入力用のバッファ
131    char *args[MAX_ARGS];   // コマンド引数
132    pid_t pid;
133    int status;
134
135    // SIGCHLD シグナルのハンドラを設定
136    signal(SIGCHLD, sigchld_handler);
137
138    // シェルのメインループ
139    while (1) {
140        // プロンプトを表示
141        printf("mysh> ");
142        fflush(stdout);
143
144        // コマンドを読み込む
145        if (fgets(cmd, sizeof(cmd), stdin) == NULL) {
146            if (feof(stdin)) {
147                break; // EOFが入力された場合、終了
148            }
149            perror("fgets failed");
150            continue;
151        }
152
153        // コマンドを解析
154        parse_command(cmd, args);
155
156        // 空のコマンドの場合は次の入力へスキップ
157        if (args[0] == NULL) {
158            continue;  // 空コマンドは何もせず次に進む
159        }
160
161        // 終了コマンドの場合
162        if (args[0] != NULL && strcmp(args[0], "exit") == 0) {
163            break;
164        }
165
166        // cd コマンドの場合、組み込みで処理
167        if (args[0] != NULL && strcmp(args[0], "cd") == 0) {
168            execute_cd(args);
169            continue;
170        }
171
172        // バックグラウンド実行かどうかを判定
173        int background = is_background_command(args);
174
175        // 子プロセスを作成してコマンドを実行
176        pid = fork();
177        if (pid == -1) {
178            perror("fork failed");
179            exit(1);
180        }
181
182        if (pid == 0) { // 子プロセス
183            // リダイレクションの処理
184            handle_redirection(args);
185
186            // execvpを使用してコマンドを実行
187            if (execvp(args[0], args) == -1) {
188                perror("exec failed");
189                exit(1);
190            }
191        } else { // 親プロセス
192            if (!background) {
193                // バックグラウンドでなければ、親は子プロセスの終了を待つ
194                waitpid(pid, &status, 0);
195            } else {
196                // バックグラウンドの場合、親は終了を待たずに次のコマンドを受け付ける
197                printf("Background process started with PID: %d\n", pid);
198            }
199        }
200    }
201
202    printf("Exiting shell...\n");
203    return 0;
204}

主な変更点

  1. handle_redirection() 関数の追加

    • リダイレクション(入力、出力、追記)を処理するための関数である。
    • 引数として渡されたコマンドにリダイレクション記号(<, >, >>)が含まれているかをチェックし、該当する処理を行う。
    • dup2() を使用して標準入力または標準出力をファイルにリダイレクトする。
  2. 入力リダイレクション (<): ファイルから標準入力を読み込みむ。

    • open() でファイルを読み込み専用で開き、dup2() で標準入力に設定する。
  3. 出力リダイレクション (>): 標準出力を指定したファイルに書き込む。

    • open() でファイルを開き、dup2() で標準出力に設定する。
  4. 追記リダイレクション (>>): 標準出力をファイルに追記する。

    • open() でファイルを追記モードで開き、dup2() で標準出力に設定する。

使用方法

  1. 上記のコードをファイルに保存(例えば myshell.c)する。
  2. コンパイルする:
    1gcc myshell.c -o myshell
    
  3. 実行する:
    1./myshell
    

サンプル実行

 1mysh> echo Hello, world! > output.txt
 2mysh> cat output.txt
 3Hello, world!
 4mysh> echo Another line >> output.txt
 5mysh> cat output.txt
 6Hello, world!
 7Another line
 8mysh> cat < input.txt
 9< input.txt の内容が表示される >
10mysh> exit
11Exiting shell...

リダイレクションを使うことで、コマンドの入力や出力をファイルにリダイレクトしたり、ファイルから入力を受け取ったりすることができる。

パイプライン機能を追加

パイプライン機能をシェルに追加することで、複数のコマンドを | でつなげて、前のコマンドの標準出力を次のコマンドの標準入力として渡すことができるようになる。 シェル内でパイプラインを処理するには、次のようなステップが必要である:

パイプラインの基本的な処理

  1. コマンドの解析 ユーザーが入力したコマンドに含まれるパイプライン(|)を探し、コマンドを分割して各コマンドを独立して実行できるようにする。

  2. pipe()fork() の使用 パイプラインで複数のコマンドを接続するために、pipe() を使用してパイプを作成し、fork() で複数の子プロセスを生成する。各子プロセスは、自分の標準入力または標準出力をパイプに接続する。

  3. プロセス間でのデータ転送 最初のコマンドの標準出力をパイプの書き込み端に接続し、次のコマンドの標準入力をパイプの読み取り端に接続する。これにより、コマンド間でデータを渡すことができる。

パイプライン機能を追加したシェルのコード

以下に、パイプライン機能を追加したシェルのコードを示す。

  1#include <stdio.h>
  2#include <stdlib.h>
  3#include <string.h>
  4#include <unistd.h>
  5#include <sys/wait.h>
  6#include <signal.h>
  7#include <fcntl.h>
  8
  9#define MAX_CMD_LEN 1024
 10#define MAX_ARGS 100
 11
 12// コマンドをスペースで分割する関数
 13void parse_command(char *cmd, char **args) {
 14    int i = 0;
 15    char *token = strtok(cmd, " \n");
 16
 17    // トークン化して args 配列に格納
 18    while (token != NULL) {
 19        args[i++] = token;
 20        token = strtok(NULL, " \n");
 21    }
 22    args[i] = NULL; // 最後の引数をNULLで終わらせる
 23}
 24
 25// コマンドをパイプで分割する関数
 26int parse_pipeline(char *cmd, char **cmds) {
 27    char *token = strtok(cmd, "|");
 28    int i = 0;
 29    while (token != NULL) {
 30        cmds[i++] = token;
 31        token = strtok(NULL, "|");
 32    }
 33    cmds[i] = NULL; // 最後をNULLで終端
 34    return i;       // パイプの数を返す
 35}
 36
 37// コマンドの末尾が '&' かどうかを確認し、'&' を取り除いて戻り値として返す
 38int is_background_command(char **args) {
 39    int i = 0;
 40    while (args[i] != NULL) {
 41        i++;
 42    }
 43    if (i > 0 && strcmp(args[i - 1], "&") == 0) {
 44        args[i - 1] = NULL; // '&' をコマンド引数から削除
 45        return 1; // バックグラウンド実行フラグを返す
 46    }
 47    return 0;
 48}
 49
 50// SIGCHLD シグナルハンドラ(バックグラウンドプロセスの終了を処理)
 51void sigchld_handler(int signo) {
 52    int status;
 53    // 終了した子プロセスの状態を取得
 54    while (waitpid(-1, &status, WNOHANG) > 0) {
 55        // 子プロセスがシグナルで終了した場合はそのシグナル番号を出力
 56        if (WIFSIGNALED(status)) {
 57            printf("Child process terminated by signal %d\n", WTERMSIG(status));
 58        }
 59    }
 60}
 61
 62// 組み込みコマンド cd を処理する関数
 63void execute_cd(char **args) {
 64    if (args[1] == NULL) {
 65        // 引数なしの場合はホームディレクトリに移動
 66        const char *home_dir = getenv("HOME");
 67        if (home_dir == NULL) {
 68            perror("cd: HOME not set");
 69        } else {
 70            if (chdir(home_dir) != 0) {
 71                perror("cd failed");
 72            }
 73        }
 74    } else {
 75        // 引数が指定された場合、そのディレクトリに移動
 76        if (chdir(args[1]) != 0) {
 77            perror("cd failed");
 78        }
 79    }
 80}
 81
 82// リダイレクションのための関数
 83void handle_redirection(char **args) {
 84    int i = 0;
 85    while (args[i] != NULL) {
 86        if (strcmp(args[i], "<") == 0) {
 87            int fd = open(args[i + 1], O_RDONLY);
 88            if (fd == -1) {
 89                perror("Input redirection failed");
 90                exit(1);
 91            }
 92            dup2(fd, STDIN_FILENO);
 93            close(fd);
 94            args[i] = NULL; // リダイレクション記号を取り除く
 95        } else if (strcmp(args[i], ">") == 0) {
 96            int fd = open(args[i + 1], O_WRONLY | O_CREAT | O_TRUNC, 0644);
 97            if (fd == -1) {
 98                perror("Output redirection failed");
 99                exit(1);
100            }
101            dup2(fd, STDOUT_FILENO);
102            close(fd);
103            args[i] = NULL; // リダイレクション記号を取り除く
104        } else if (strcmp(args[i], ">>") == 0) {
105            int fd = open(args[i + 1], O_WRONLY | O_CREAT | O_APPEND, 0644);
106            if (fd == -1) {
107                perror("Append redirection failed");
108                exit(1);
109            }
110            dup2(fd, STDOUT_FILENO);
111            close(fd);
112            args[i] = NULL; // リダイレクション記号を取り除く
113        }
114        i++;
115    }
116}
117
118int main() {
119    char cmd[MAX_CMD_LEN]; // コマンド入力用のバッファ
120    char *cmds[MAX_ARGS]; // パイプで分割されたコマンド用
121    char *args[MAX_ARGS];   // コマンド引数
122    pid_t pid;
123    int status;
124
125    // SIGCHLD シグナルのハンドラを設定
126    signal(SIGCHLD, sigchld_handler);
127
128    // シェルのメインループ
129    while (1) {
130        // プロンプトを表示
131        printf("mysh> ");
132        fflush(stdout);
133
134        // コマンドを読み込む
135        if (fgets(cmd, sizeof(cmd), stdin) == NULL) {
136            if (feof(stdin)) break; // EOFが入力された場合、終了
137            perror("fgets failed");
138            continue;
139        }
140
141        // パイプで分割
142        int num_cmds = parse_pipeline(cmd, cmds);
143
144        if (num_cmds > 1) { // パイプが含まれる場合
145            int pipes[num_cmds - 1][2];
146            pid_t pids[num_cmds];
147
148            // 必要なパイプを作成
149            for (int i = 0; i < num_cmds - 1; i++) {
150                if (pipe(pipes[i]) == -1) {
151                    perror("pipe failed");
152                    exit(1);
153                }
154            }
155
156            for (int i = 0; i < num_cmds; i++) {
157                parse_command(cmds[i], args);
158
159                if ((pids[i] = fork()) == -1) {
160                    perror("fork failed");
161                    exit(1);
162                }
163
164                if (pids[i] == 0) { // 子プロセス
165                    if (i > 0) { // パイプの入力
166                        dup2(pipes[i - 1][0], STDIN_FILENO);
167                    }
168                    if (i < num_cmds - 1) { // パイプの出力
169                        dup2(pipes[i][1], STDOUT_FILENO);
170                    }
171                    for (int j = 0; j < num_cmds - 1; j++) { // 全てのパイプを閉じる
172                        close(pipes[j][0]);
173                        close(pipes[j][1]);
174                    }
175
176                    handle_redirection(args); // リダイレクション処理
177
178                    if (execvp(args[0], args) == -1) {
179                        perror("exec failed");
180                        exit(1);
181                    }
182                }
183            }
184
185            // 親プロセスでパイプを閉じる
186            for (int i = 0; i < num_cmds - 1; i++) {
187                close(pipes[i][0]);
188                close(pipes[i][1]);
189            }
190
191            // 全ての子プロセスの終了を待つ
192            for (int i = 0; i < num_cmds; i++) {
193                waitpid(pids[i], &status, 0);
194            }
195        } else { // パイプが含まれない通常のコマンド
196            parse_command(cmds[0], args);
197
198            // 空のコマンドの場合は次の入力へスキップ
199            if (args[0] == NULL) continue;
200
201            // 終了コマンドの場合
202            if (strcmp(args[0], "exit") == 0) break;
203
204            // cd コマンドの場合、組み込みで処理
205            if (strcmp(args[0], "cd") == 0) {
206                execute_cd(args);
207                continue;
208            }
209
210            // バックグラウンド実行かどうかを判定
211            int background = is_background_command(args);
212
213            // 子プロセスを作成してコマンドを実行
214            pid = fork();
215            if (pid == -1) {
216                perror("fork failed");
217                exit(1);
218            }
219
220            if (pid == 0) { // 子プロセス
221                // リダイレクションの処理
222                handle_redirection(args);
223
224                // execvpを使用してコマンドを実行
225                if (execvp(args[0], args) == -1) {
226                    perror("exec failed");
227                    exit(1);
228                }
229            } else { // 親プロセス
230                if (!background) {
231                    // バックグラウンドでなければ、親は子プロセスの終了を待つ
232                    waitpid(pid, &status, 0);
233                } else {
234                    // バックグラウンドの場合、親は終了を待たずに次のコマンドを受け付ける
235                    printf("Background process started with PID: %d\n", pid);
236                }
237            }
238        }
239    }
240
241    printf("Exiting shell...\n");
242    return 0;
243}

主な変更点

  1. parse_pipeline() 関数

    • コマンドを | で分割するための関数。
  2. プロセス間のパイプ接続

    • pipe() を使ってパイプを作成する。
    • 各コマンドを実行する子プロセスを fork() で生成し、標準入力や標準出力をパイプにリダイレクトする。
  3. 複数コマンドのパイプライン処理

    • 複数のコマンドがパイプでつながれた場合、順番に実行され、前のコマンドの標準出力が次のコマンドの標準入力として渡される。

使用例

 1mysh> cat sample.txt
 2aaa
 3aaa
 4aaa
 5bbb
 6aaa
 7ccc
 8aaa
 9mysh> cat sample.txt | grep aaa | wc -l
10       5

このコードでは、複数のコマンドをパイプでつなげて実行することができる。 最初のコマンドの出力が次のコマンドの入力として渡され、最終的な結果が表示される。

関連ページ