Linux で動く 最小限のシェル を C で実装しながら、
- シェルがどうやってコマンドを実行しているのか
-
execvpは何をしているのか -
$PATHはどう使われているのか -
perrorは何のためにあるのか
を整理していきます。
シェルの基本構造
Unix 系 OS のシェルは、基本的に次の流れで動いています。
- ユーザから 1 行入力を読む
- コマンドと引数に分解する
-
fork()で子プロセスを作る - 子プロセスで
exec()して別のプログラムに置き換える - 親プロセスは
wait()で終了を待つ
最小構成のシェル実装例
以下は Linux 前提の、非常にシンプルなシェルです。
機能
- プロンプト表示
- コマンド実行
-
cd(ビルトイン) -
exit(ビルトイン)
simple_shell.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define MAX_LINE 1024
#define MAX_ARGS 64
void parse_line(char *line, char **argv) {
int i = 0;
char *token = strtok(line, " \t\n");
while (token != NULL && i < MAX_ARGS - 1) {
argv[i++] = token;
token = strtok(NULL, " \t\n");
}
argv[i] = NULL;
}
int main() {
char line[MAX_LINE];
char *argv[MAX_ARGS];
while (1) {
printf("myshell> ");
fflush(stdout);
if (fgets(line, sizeof(line), stdin) == NULL) {
printf("\n");
break;
}
parse_line(line, argv);
if (argv[0] == NULL) {
continue;
}
/* exit */
if (strcmp(argv[0], "exit") == 0) {
break;
}
/* cd */
if (strcmp(argv[0], "cd") == 0) {
if (argv[1] == NULL) {
fprintf(stderr, "cd: missing argument\n");
} else if (chdir(argv[1]) != 0) {
perror("cd");
}
continue;
}
pid_t pid = fork();
if (pid == 0) {
execvp(argv[0], argv);
perror("execvp");
exit(1);
} else if (pid > 0) {
int status;
waitpid(pid, &status, 0);
} else {
perror("fork");
}
}
return 0;
}
execvp とは何か?
int execvp(const char *file, char *const argv[]);
execvpは現在のプロセスを別のプログラムに置き換える関数です。
- 成功すると 戻ってこない
- 失敗したときだけ -1 を返す
$PATHは自動で検索される?
される。
execvpのpはPATH検索を意味しています。
ただし、コマンド名に/が含まれている場合、その限りではありません(PATHからは検索されません)。
| 関数名 | PATH検索 | 引数形式 |
|---|---|---|
| execl | ❌ | 可変長 |
| execv | ❌ | 配列 |
| execvp | ✅ | 配列 |
| execve | ❌ | 配列(env指定可) |
実体は execve で、他はラッパーです。
perrorとは?
void perror(const char *s);
- 直前に失敗したシステムコールの理由を表示
- 内部で errno を参照
- stderr に出力される