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}