Linuxで簡単なシェルを自作してみた(3)|wait実行中の割り込みを回避する

目次

Linuxで簡単なシェルを自作してみた(2)|パス名展開と引用符削除のつづき。

fork() で子プロセスを生成し、 waitpid() で子プロセスの終了を待ち受けるわけだが、 デバッガーを使って細かな部分の動作確認をしていると、 waitpid() で割り込みエラーが発生することがある。 現状は waitpid() でエラーが発生するとシェルを終了させるように実装しているが、それだといちいち再起動するのが面倒だ。

以下の動画は、 waitpid() にブレークポイントを張ってステップ実行すると、割り込みが発生してしまい、エラーロジックに進む様子を示している。

そこで、 waitpid() で割り込みエラーが発生した場合は、waitpid() をリトライするよう修正する。

つまり、 waitpid() システムコールを呼び出してプロセスが待たされている間にシグナルが発生して プロセスがそれを補足すると、システムコールが割り込まれてエラーとして戻る。 このとき、 errno の値は EINTER になるので、その場合は再度 waitpid() を呼び出す。

修正後のソースコードと Linuxで簡単なシェルを自作してみた(2)|パス名展開と引用符削除のソースコードの差異(diff)を、以下リンク先に示す。
修正前後の比較

修正したシェルのソースコード:
  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#include <glob.h>     // glob
  7#include <errno.h>    // errno
  8
  9#define MAX_CMD_LEN 1024
 10#define MAX_ARGS 100
 11
 12// コマンドをスペースで分割する関数
 13void parse_command(char *cmd, char *argv[]) {
 14    char *token = strtok(cmd, " \n");
 15    int i = 0;
 16    while (token != NULL) {
 17        argv[i++] = token;
 18        token = strtok(NULL, " \n");
 19    }
 20    argv[i] = NULL; // 最後の引数をNULLで終わらせる
 21}
 22
 23// 引用符を削除する関数
 24void remove_quotes(char *str) {
 25    char *src = str, *dst = str;
 26    while (*src) {
 27        if (*src == '"' || *src == '\'') {
 28            src++;  // 引用符をスキップ
 29        } else {
 30            *dst++ = *src++;
 31        }
 32    }
 33    *dst = '\0';  // 文字列の終端を追加
 34}
 35
 36// 引数リストにパス名展開を適用する関数
 37int expand_arguments(char *argv[], char **expanded_argv) {
 38    glob_t glob_result;
 39    int argc = 0; // 展開後の引数数
 40
 41    for (int i = 0; argv[i] != NULL; i++) {
 42        // 引用符が含まれている場合は展開しない
 43        if ((strchr(argv[i], '*') || strchr(argv[i], '?') || strchr(argv[i], '[')) &&
 44            (argv[i][0] != '"' && argv[i][0] != '\'')) { // 引用符外でパス名展開
 45            if (glob(argv[i], GLOB_NOCHECK | GLOB_TILDE, NULL, &glob_result) != 0) {
 46                perror("glob failed");
 47                return -1;
 48            }
 49            // 展開された結果を追加
 50            for (size_t j = 0; j < glob_result.gl_pathc; j++) {
 51                if (argc >= MAX_ARGS - 1) {
 52                    fprintf(stderr, "Too many arguments after glob expansion\n");
 53                    globfree(&glob_result);
 54                    return -1;
 55                }
 56                expanded_argv[argc++] = strdup(glob_result.gl_pathv[j]);
 57            }
 58            globfree(&glob_result); // メモリを解放
 59        } else {
 60            // 引用符を取り除く
 61            remove_quotes(argv[i]);
 62
 63            // パス名展開が不要な場合
 64            if (argc >= MAX_ARGS - 1) {
 65                fprintf(stderr, "Too many arguments\n");
 66                return -1;
 67            }
 68            expanded_argv[argc++] = strdup(argv[i]);
 69        }
 70    }
 71    expanded_argv[argc] = NULL; // 最後にNULLを追加
 72    return argc;
 73}
 74
 75int main() {
 76    char cmd[MAX_CMD_LEN]; // コマンド入力用のバッファ
 77    char *argv[MAX_ARGS];  // コマンド引数
 78    char *expanded_argv[MAX_ARGS]; // 展開後の引数
 79    pid_t pid;
 80    int status;
 81
 82    // シェルのメインループ
 83    while (1) {
 84        // プロンプトを表示
 85        fprintf(stderr, "mysh> ");
 86
 87        // コマンドを読み込む
 88        if (fgets(cmd, sizeof(cmd), stdin) == NULL) {
 89            if (feof(stdin)) {
 90                break; // EOFが入力された場合、終了
 91            }
 92            perror("fgets failed");
 93            continue;
 94        }
 95
 96        // コマンドを解析
 97        parse_command(cmd, argv);
 98
 99        // 空のコマンドの場合は次の入力へスキップ
100        if (argv[0] == NULL) {
101            continue;
102        }
103
104        // 終了コマンドの場合
105        if (strcmp(argv[0], "exit") == 0) {
106            break;
107        }
108
109        // 引数を展開
110        if (expand_arguments(argv, expanded_argv) == -1) {
111            fprintf(stderr, "Argument expansion failed\n");
112            continue;
113        }
114
115        // 子プロセスを作成してコマンドを実行
116        pid = fork();
117        if (pid == -1) {
118            perror("fork failed");
119            exit(1);
120        }
121
122        // 子プロセス
123        if (pid == 0) {
124            // execvpを使用してコマンドを実行
125            if (execvp(expanded_argv[0], expanded_argv) == -1) {
126                perror("exec failed");
127                exit(1);
128            }
129        }
130
131        // 親プロセス
132        while (1) {
133            if (waitpid(pid, &status, 0) == (pid_t)-1) { // 子プロセスの終了を待つ
134                if (errno == EINTR) {
135                    continue; // 割り込み発生時はリトライする
136                } else {
137                    perror("waitpid failed");
138                    exit(1);
139                }
140            }
141            break;
142        }
143
144        // メモリを解放
145        for (int i = 0; expanded_argv[i] != NULL; i++) {
146            free(expanded_argv[i]);
147        }
148    }
149
150    printf("Exiting shell...\n");
151    return 0;
152}

関連ページ