Linuxで簡単なシェルを自作してみた(5)|バックグラウンド実行
目次
Linuxで簡単なシェルを自作してみた(4)|組み込みコマンドのつづき。
バックグラウンド実行の機能を追加
バックグラウンド実行の機能を追加するには、ユーザーがコマンドの最後に &
を入力した場合、そのコマンドをバックグラウンドで実行するようにシェルを変更する。
バックグラウンドで実行する際、シェルは新しいプロセスを生成し、そのプロセスが終了するのを待機せずに次のコマンドを受け付けるようにする。
ただしバックグラウンドプロセスが終了したときに親プロセスが waitpid()
を呼び出す必要はある(放置したままだとゾンビプロセスとして残ってしまうため)。
バックグラウンド実行をサポートするために、シェルに以下の変更を加える:
- コマンドの末尾に
&
があるかどうかを確認。 &
があれば、コマンドをバックグラウンドで実行し、親プロセスはその終了を待たずに次のコマンドを実行可能とする。また、組み込みコマンドの場合は exec を使って置き換える必要はない。- 親プロセスはバックグラウンドプロセスの完了を待ってはいけない。ただし
waitpid()
を実行して消滅させる。 &
がなければ、通常通りフォアグラウンドで実行し、終了を待つ。
修正したシェルのソースコード
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#include <limits.h> // PATH_MAX
9
10#define MAX_CMD_LEN 1024
11#define MAX_ARGS 100
12
13// 組み込みコマンド関連の宣言
14int cd (char *argv[]);
15int pwd(char *argv[]);
16struct builtin {
17 char cmd[256]; // コマンド名
18 int (*func)(char *argv[]); // 関数へのポインタ
19};
20struct builtin blt_in[] = {{"cd",cd}, {"pwd",pwd}, {"",0}};
21
22// コマンドをスペースで分割する関数
23void parse_command(char *cmd, char *argv[]) {
24 char *token = strtok(cmd, " \n");
25 int i = 0;
26 while (token != NULL) {
27 argv[i++] = token;
28 token = strtok(NULL, " \n");
29 }
30 argv[i] = NULL; // 最後の引数をNULLで終わらせる
31}
32
33// 引用符を削除する関数
34void remove_quotes(char *str) {
35 char *src = str, *dst = str;
36 while (*src) {
37 if (*src == '"' || *src == '\'') {
38 src++; // 引用符をスキップ
39 } else {
40 *dst++ = *src++;
41 }
42 }
43 *dst = '\0'; // 文字列の終端を追加
44}
45
46// 引数リストにパス名展開を適用する関数
47int expand_arguments(char *argv[], char **expanded_argv) {
48 glob_t glob_result;
49 int argc = 0; // 展開後の引数数
50
51 for (int i = 0; argv[i] != NULL; i++) {
52 // 引用符が含まれている場合は展開しない
53 if ((strchr(argv[i], '*') || strchr(argv[i], '?') || strchr(argv[i], '[')) &&
54 (argv[i][0] != '"' && argv[i][0] != '\'')) { // 引用符外でパス名展開
55 if (glob(argv[i], GLOB_NOCHECK | GLOB_TILDE, NULL, &glob_result) != 0) {
56 perror("glob failed");
57 return -1;
58 }
59 // 展開された結果を追加
60 for (size_t j = 0; j < glob_result.gl_pathc; j++) {
61 if (argc >= MAX_ARGS - 1) {
62 fprintf(stderr, "Too many arguments after glob expansion\n");
63 globfree(&glob_result);
64 return -1;
65 }
66 expanded_argv[argc++] = strdup(glob_result.gl_pathv[j]);
67 }
68 globfree(&glob_result); // メモリを解放
69 } else {
70 // 引用符を取り除く
71 remove_quotes(argv[i]);
72
73 // パス名展開が不要な場合
74 if (argc >= MAX_ARGS - 1) {
75 fprintf(stderr, "Too many arguments\n");
76 return -1;
77 }
78 expanded_argv[argc++] = strdup(argv[i]);
79 }
80 }
81 expanded_argv[argc] = NULL; // 最後にNULLを追加
82 return argc;
83}
84
85// 組み込みコマンドの判定・実行
86int built_in(char *argv[], int bg){
87 struct builtin *p;
88 // 組み込みコマンド配列(blt_in)の走査
89 for (p = blt_in; *p->cmd != '\0'; p++) {
90 // 組み込みコマンドでない場合は次をチェック
91 if (strcmp(p->cmd, argv[0]) != 0) continue;
92 // バックグラウンド実行の場合
93 if (bg) {
94 pid_t pid = fork();
95 if (pid == -1) {
96 perror("fork failed");
97 exit(1);
98 }
99 // 子プロセス
100 if (pid == 0) exit ((p->func)(argv)); // 組み込みコマンドの関数を呼び出し
101 // 親プロセス
102 fprintf(stderr, "Background process started with PID: %d\n", pid);
103 return 0;
104 }
105 // バックグラウンド実行でない場合
106 // 組み込みコマンドの関数を呼び出し
107 (p->func)(argv);
108 return 0;
109 }
110 return -1;
111}
112
113// cd コマンド
114int cd(char *argv[]) {
115 if (argv[1] == NULL) {
116 // 引数なしの場合はホームディレクトリに移動
117 const char *home_dir = getenv("HOME");
118 if (home_dir == NULL) {
119 perror("cd: HOME not set");
120 return -1;
121 } else {
122 if (chdir(home_dir) != 0) {
123 perror("cd failed");
124 return -1;
125 }
126 }
127 } else {
128 // 引数が指定された場合、そのディレクトリに移動
129 if (chdir(argv[1]) != 0) {
130 perror("cd failed");
131 return -1;
132 }
133 }
134 return 0;
135}
136
137// pwd コマンド
138int pwd(char *argv[]){
139 char buf[PATH_MAX+1];
140 if (getcwd(buf, sizeof(buf)) == NULL) {
141 perror("getcwd failed");
142 return -1;
143 }
144 printf("%s\n", buf);
145 return 0;
146}
147
148// バックグラウンド実行判定
149// コマンドの末尾が '&' かどうかを確認し、'&' を取り除いて戻り値として返す
150int is_background_command(char *argv[]) {
151 int i = 0;
152 while (argv[i] != NULL) i++;
153 if (i > 0 && strcmp(argv[i - 1], "&") == 0) {
154 argv[i - 1] = NULL; // '&' をコマンド引数から削除
155 return 1;
156 }
157 return 0;
158}
159
160int main() {
161 char cmd[MAX_CMD_LEN]; // コマンド入力用のバッファ
162 char *argv[MAX_ARGS]; // コマンド引数
163 char *expanded_argv[MAX_ARGS]; // 展開後の引数
164 pid_t pid, wpid;
165 int status, bg;
166
167 // シェルのメインループ
168 while (1) {
169 // ゾンビを消滅させる
170 while ((wpid = waitpid(-1, &status, WNOHANG)) > 0)
171 fprintf(stderr, "process %d done\n", wpid);
172
173 // プロンプトを表示
174 fprintf(stderr, "mysh> ");
175
176 // コマンドを読み込む
177 if (fgets(cmd, sizeof(cmd), stdin) == NULL) {
178 if (feof(stdin)) {
179 break; // EOFが入力された場合、終了
180 }
181 perror("fgets failed");
182 continue;
183 }
184
185 // コマンドを解析
186 parse_command(cmd, argv);
187
188 // 空のコマンドの場合は次の入力へスキップ
189 if (argv[0] == NULL) {
190 continue;
191 }
192
193 // 終了コマンドの場合
194 if (strcmp(argv[0], "exit") == 0) {
195 break;
196 }
197
198 // バックグラウンド実行判定
199 bg = is_background_command(argv);
200
201 // 引数を展開
202 if (expand_arguments(argv, expanded_argv) == -1) {
203 fprintf(stderr, "Argument expansion failed\n");
204 continue;
205 }
206
207 // 組み込みコマンド判定・実行
208 if (built_in(expanded_argv, bg) == 0) {
209 continue;
210 }
211
212 // 子プロセスを作成してコマンドを実行
213 pid = fork();
214 if (pid == -1) {
215 perror("fork failed");
216 exit(1);
217 }
218
219 // 子プロセス
220 if (pid == 0) {
221 // execvpを使用してコマンドを実行
222 if (execvp(expanded_argv[0], expanded_argv) == -1) {
223 perror("exec failed");
224 exit(1);
225 }
226 }
227
228 // 親プロセス
229 while (1) {
230 // バックグラウンド実行の場合
231 if (bg) {
232 fprintf(stderr, "Background process started with PID: %d\n", pid);
233 break; // バックグラウンド実行の場合は待機しない
234 }
235 // バックグラウンド実行でない場合
236 if (waitpid(pid, &status, 0) == (pid_t)-1) { // 子プロセスの終了を待つ
237 if (errno == EINTR) {
238 continue; // 割り込み発生時はリトライする
239 } else {
240 perror("waitpid failed");
241 exit(1);
242 }
243 }
244 break;
245 }
246
247 // メモリを解放
248 for (int i = 0; expanded_argv[i] != NULL; i++) {
249 free(expanded_argv[i]);
250 }
251 }
252
253 printf("Exiting shell...\n");
254 return 0;
255}
Linuxで簡単なシェルを自作してみた(4)|組み込みコマンドのソースコードと 今回修正したソースコードの差異(diff)を、以下リンク先に示す。
修正前後の比較
主な変更点
main()
関数- 子プロセスを消滅させるため、ループ先頭で
waitpid()
を呼び出している。 全てのバックグラウンドプロセスをチェックする必要があるため、waitpid()
を繰り返し実行する。(wait(2) リファレンスより抜粋)
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *_Nullable wstatus, int options);
このシステムコールは呼び出し元プロセスの子プロセスにおける状態変化を待機し、子プロセスの終了など、状態変化した子プロセスに関する情報を取得するために使用される。 子プロセスが終了した場合、wait を実行することでシステムがその子プロセスに関連付けられたリソースを解放できる。 wait が実行されない場合、終了した子プロセスは「ゾンビ」状態のままとなる。waitpid()
システムコールは、第一引数 pid で指定された子プロセスが状態変化するまで、呼び出し元スレッドの実行を一時停止する。
pid 引数の値によって挙動が以下のように変わる:< -1
- : 指定された pid の絶対値と等しいプロセスグループIDを持つすべての子プロセスを待機する。
-1
- : 任意の子プロセスを待機する。
0
- : 呼び出し元プロセスと同じプロセスグループIDを持つ子プロセスを待機する。
> 0
- : 指定された pid に一致するプロセスIDを持つ子プロセスを待機する。
第二引数 wstatus が NULL でない場合、
waitpid()
は状態情報を指し示された int 型変数に格納する。 この整数はマクロを使用して調査することができる。(省略)
デフォルトではwaitpid()
は終了した子プロセスのみを待機するが、この動作は第三引数 options で変更可能。
options 引数の値は、以下の定数を0個以上論理和(OR)で組み合わせたもの:WNOHANG
:- 子プロセスが終了していない場合でも、すぐに戻る(待機しない)。
WUNTRACED
:- 子プロセスが停止している場合も戻る。追跡中の子プロセスが停止している場合は、このオプションを指定しなくても戻る。
WCONTINUED
(Linux 2.6.10以降) :SIGCONT
シグナルの受信により再開された停止中の子プロセスの状態も戻る。
成功時は状態変化した子プロセスのPID
(プロセスID)を返す。 失敗時は-1
を返す。 is_background_command()
の呼び出しを組み込みコマンド実行前に追加する必要がある。 また、展開処理の後に追加すると若干&
を探す処理が非効率となるため、その前に追加。- バックグラウンド実行の場合、親プロセスは
waitpid()
を呼ばず、すぐに次のコマンドを受け付ける。 また、バックグラウンドで実行されるプロセスの PID を表示する。
- 子プロセスを消滅させるため、ループ先頭で
is_background_command()
関数
コマンドの最後に&
があるかどうかを確認する。&
があれば、その部分を削除し、バックグラウンド指定有無の判定結果を返す。built_in()
関数
組み込みコマンドのバックグラウンド実行に対応するよう修正する。- 追加された引数を参照し、バッグクラウンド実行が指定されている場合は、 子プロセスを生成して組み込みコマンドの関数を呼び出す。 この時、 exec を使わない(組み込み機能を実行するのであって、外部に組み込みコマンドと同盟の実行形式ファイルがあっても exec で実行しない)。
- 子プロセスは、組み込み関数の実行後は即時でプロセスを終了する( exit )。
- 親プロセスは、子プロセスの
PID
を出力するのみで、他は何もせずリターンする。
使用方法
- 上記のコードをファイルに保存(例えば
myshell.c
)する。 - コンパイルする:
1gcc myshell.c -o myshell
- 実行する:
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>
8mysh>
9mysh>
10process 12345 done
11mysh>
12mysh> pwd &
13Background process started with PID: 12348
14mysh> /Users/dam
15
16process 12348 done
17mysh>
18mysh> exit
19Exiting shell...
上記の例では、sleep 5 &
をバックグラウンドで実行し、その後、すぐに次のコマンド(echo Hello, world!
)を実行している。
さらに数回Enterで空振りさせて、数秒後に子プロセスの実行完了メッセージが出ることを確認している。
バックグラウンドプロセスが終了するのをシェルが待つことなく、シェルは次のコマンドを受け付けることができる。
また、組み込みコマンド pwd
についても動作確認した。