第4章: プロセス実行モデルとコマンド実行
4.1 UNIXプロセスモデルの理論
プロセスの概念
プロセス(Process)は、実行中のプログラムのインスタンスである。Dennis RitchieとKen Thompsonは、"The UNIX Time-Sharing System"(Communications of the ACM, 1974)において、UNIXのプロセスモデルを定義した。
プロセスの構成要素:
- テキストセグメント: 実行可能コード
- データセグメント: 初期化されたグローバル変数
- BSSセグメント: 未初期化のグローバル変数
- ヒープ: 動的に割り当てられたメモリ
- スタック: 関数呼び出しとローカル変数
- プロセス制御ブロック(PCB): カーネルが管理するメタデータ
プロセスのメモリレイアウト:
高位アドレス
┌─────────────────┐
│ スタック │ ↓ 成長方向
├─────────────────┤
│ ↓ │
│ (空き領域) │
│ ↑ │
├─────────────────┤
│ ヒープ │ ↑ 成長方向
├─────────────────┤
│ BSS │
├─────────────────┤
│ データ │
├─────────────────┤
│ テキスト │
└─────────────────┘
低位アドレス
fork-execモデル
UNIXの革新的な設計の1つがfork-execモデルである。
fork() - プロセスの複製:
- 呼び出しプロセス(親)の完全なコピーを作成
- 親子は独立したアドレス空間を持つ
- 戻り値で親子を区別(親: 子のPID、子: 0)
exec() - プロセスイメージの置換:
- 現在のプロセスを新しいプログラムで置き換える
- PIDは変わらない
- 成功時は戻らない
Ken Thompsonはこの分離設計の理由を説明している:
> "The separation of fork and exec allows the shell to set up I/O redirection and pipelines between the fork and the exec." > (forkとexecの分離により、シェルはforkとexecの間でI/Oリダイレクションとパイプラインを設定できる)
コピーオンライト(Copy-on-Write)
初期のUNIXでは、fork()は親のメモリ全体を物理的にコピーしていた。これは非効率であった。
現代のUNIXはコピーオンライト(COW: Copy-on-Write)を実装する:
fork()時、親子はメモリページを共有- ページは読み取り専用としてマーク
- いずれかが書き込むと、そのページのみコピー
fork()直後:
親プロセス 子プロセス
ページテーブル ページテーブル
│ │
└────────┐ ┌──────────────┘
↓ ↓
┌────────────┐
│ 物理メモリ │ (共有)
└────────────┘
書き込み後:
親プロセス 子プロセス
ページテーブル ページテーブル
│ │
↓ ↓
┌────────┐ ┌────────┐
│ 物理A │ │ 物理B │
└────────┘ └────────┘
(変更されたページ) (コピー)
4.2 システムコールインターフェース
POSIX標準
POSIX.1(IEEE Std 1003.1)は、UNIXライクなシステムのためのポータブルなインターフェースを定義する。
プロセス管理に関連する主要なシステムコール:
| システムコール | 説明 |
|--------------|------|
| fork() | プロセスの複製 |
| execve() | プログラムの実行 |
| wait() | 子プロセスの終了を待つ |
| waitpid() | 特定の子プロセスを待つ |
| exit() | プロセスの終了 |
| getpid() | プロセスIDの取得 |
| getppid() | 親プロセスIDの取得 |
fork()の詳細
#include <unistd.h>
pid_t fork(void);
戻り値:
- 親プロセス:子のPID(正の整数)
- 子プロセス:0
- エラー:-1(
errnoが設定される)
fork()で継承されるもの:
- メモリ内容(COWにより共有)
- 開いているファイルディスクリプタ
- 環境変数
- シグナルハンドラ
- プロセスグループID
fork()で継承されないもの:
- PID(新しいPIDが割り当てられる)
- PPIDは呼び出し元のPIDになる
- ファイルロック
- ペンディングシグナル
execve()の詳細
#include <unistd.h>
int execve(const char *pathname, char *const argv[], char *const envp[]);
パラメータ:
pathname: 実行ファイルのパスargv: 引数配列(NULL終端)envp: 環境変数配列(NULL終端)
exec関数ファミリ:
| 関数 | パス | 引数 | 環境 |
|------|------|------|------|
| execl | パス | リスト | 継承 |
| execle | パス | リスト | 明示 |
| execlp | PATH検索 | リスト | 継承 |
| execv | パス | 配列 | 継承 |
| execve | パス | 配列 | 明示 |
| execvp | PATH検索 | 配列 | 継承 |
命名規則:
l: 引数をリスト(variadic)で指定v: 引数をベクタ(配列)で指定e: 環境変数を明示的に指定p: PATH環境変数を使用して検索
wait()とwaitpid()
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
waitpid()のpidパラメータ:
> 0: 指定されたPIDの子を待つ0: 同じプロセスグループの任意の子を待つ-1: 任意の子を待つ(wait()と等価)< -1: プロセスグループID == |pid| の任意の子を待つ
statusの解析マクロ:
WIFEXITED(status) /* 正常終了した場合に真 */
WEXITSTATUS(status) /* 終了ステータス(WIFEXITED時のみ有効) */
WIFSIGNALED(status) /* シグナルで終了した場合に真 */
WTERMSIG(status) /* 終了させたシグナル番号 */
WCOREDUMP(status) /* コアダンプした場合に真 */
WIFSTOPPED(status) /* 停止した場合に真 */
WSTOPSIG(status) /* 停止させたシグナル番号 */
4.3 ファイルディスクリプタとリダイレクション
ファイルディスクリプタの理論
ファイルディスクリプタ(File Descriptor)は、開いているファイルへの参照を表す非負整数である。
UNIXカーネルは3つのデータ構造を管理する:
プロセスA オープンファイルテーブル vノードテーブル
┌────────┐ ┌─────────────────┐ ┌───────────┐
│ fd 0 ──┼───────→│ オフセット: 100 │───────→│ ファイルA │
│ fd 1 ──┼──┐ │ フラグ: O_RDONLY│ └───────────┘
│ fd 2 ──┼─┐│ └─────────────────┘
└────────┘ ││ ┌─────────────────┐ ┌───────────┐
│└────→│ オフセット: 0 │───────→│ ファイルB │
│ │ フラグ: O_WRONLY│ └───────────┘
│ └─────────────────┘
│ ┌─────────────────┐ ┌───────────┐
└─────→│ オフセット: 0 │───────→│ ファイルC │
│ フラグ: O_WRONLY│ └───────────┘
└─────────────────┘
標準ファイルディスクリプタ
POSIXは3つの標準ファイルディスクリプタを定義:
dup()とdup2()
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
dup(): oldfdを複製し、最小の利用可能なfdを返す
dup2(): oldfdをnewfdに複製する
newfdが開いていれば先に閉じるoldfd == newfdの場合、何もしない
リダイレクションの実装に不可欠:
/* stdout をファイルにリダイレクト */
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO); /* fd 1 を fd に向ける */
close(fd); /* 元のfdは不要 */
4.4 PATH検索アルゴリズム
PATH環境変数
PATH環境変数は、実行可能ファイルを検索するディレクトリのリストを指定する:
PATH=/usr/local/bin:/usr/bin:/bin
検索アルゴリズム
POSIXが定義するコマンド検索手順:
/を含む場合:- コマンドが
/を含まない場合:
/**
* コマンドのパスを検索する
* @param cmd コマンド名
* @param env 環境変数リスト
* @return 実行可能ファイルのパス、見つからない場合はNULL
*/
char *find_command_path(const char *cmd, t_env *env)
{
char *path_env;
char **paths;
char *full_path;
int i;
/* 空のコマンドはエラー */
if (!cmd || !*cmd)
return (NULL);
/* 絶対パスまたは相対パス */
if (ft_strchr(cmd, '/'))
return (validate_path(cmd));
/* PATH環境変数を取得 */
path_env = env_get(env, "PATH");
if (!path_env)
return (NULL);
/* ':' で分割 */
paths = ft_split(path_env, ':');
if (!paths)
return (NULL);
/* 各ディレクトリを検索 */
i = 0;
while (paths[i])
{
full_path = path_join(paths[i], cmd);
if (access(full_path, X_OK) == 0)
{
free_array(paths);
return (full_path);
}
free(full_path);
i++;
}
free_array(paths);
return (NULL);
}
/**
* パスを検証する
*/
char *validate_path(const char *path)
{
struct stat st;
/* 存在確認 */
if (access(path, F_OK) != 0)
{
print_error(path, "No such file or directory");
return (NULL);
}
/* ディレクトリでないことを確認 */
if (stat(path, &st) == 0 && S_ISDIR(st.st_mode))
{
print_error(path, "Is a directory");
return (NULL);
}
/* 実行権限確認 */
if (access(path, X_OK) != 0)
{
print_error(path, "Permission denied");
return (NULL);
}
return (ft_strdup(path));
}
4.5 コマンド実行の実装
単純コマンドの実行
/**
* 単純コマンドを実行する
* @param ast コマンドのASTノード
* @param shell シェル状態
* @return 終了ステータス
*/
int execute_command(t_ast *ast, t_shell *shell)
{
pid_t pid;
int status;
char *path;
char **envp;
/* ビルトインコマンドの確認 */
if (is_builtin(ast->argv[0]))
return (execute_builtin(ast, shell));
/* コマンドパスの検索 */
path = find_command_path(ast->argv[0], shell->env);
if (!path)
return (127); /* command not found */
/* 環境変数配列の作成 */
envp = env_to_array(shell->env);
if (!envp)
{
free(path);
return (1);
}
/* 子プロセスを生成 */
pid = fork();
if (pid == -1)
{
perror("minishell: fork");
free(path);
free_array(envp);
return (1);
}
if (pid == 0)
execute_child(ast, path, envp);
else
status = wait_for_child(pid);
free(path);
free_array(envp);
return (status);
}
子プロセスの処理
/**
* 子プロセスでコマンドを実行する
* この関数は戻らない
*/
void execute_child(t_ast *ast, char *path, char **envp)
{
/* シグナルをデフォルトに戻す */
signal(SIGINT, SIG_DFL);
signal(SIGQUIT, SIG_DFL);
/* リダイレクトを設定 */
if (setup_redirections(ast->redirects) != 0)
exit(1);
/* コマンドを実行 */
execve(path, ast->argv, envp);
/* execveが失敗した場合 */
perror("minishell: execve");
exit(126);
}
/**
* 子プロセスの終了を待つ
*/
int wait_for_child(pid_t pid)
{
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status))
return (WEXITSTATUS(status));
else if (WIFSIGNALED(status))
{
/* シグナルで終了した場合、128 + シグナル番号 */
return (128 + WTERMSIG(status));
}
return (1);
}
リダイレクトの設定
/**
* リダイレクトを設定する
* @param redirects リダイレクトリスト
* @return 成功時0、失敗時-1
*/
int setup_redirections(t_redirect *redirects)
{
t_redirect *current;
current = redirects;
while (current)
{
if (apply_redirect(current) != 0)
return (-1);
current = current->next;
}
return (0);
}
/**
* 単一のリダイレクトを適用する
*/
int apply_redirect(t_redirect *redir)
{
int fd;
if (redir->type == REDIR_INPUT)
{
fd = open(redir->file, O_RDONLY);
if (fd == -1)
return (print_open_error(redir->file));
dup2(fd, STDIN_FILENO);
close(fd);
}
else if (redir->type == REDIR_OUTPUT)
{
fd = open(redir->file, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1)
return (print_open_error(redir->file));
dup2(fd, STDOUT_FILENO);
close(fd);
}
else if (redir->type == REDIR_APPEND)
{
fd = open(redir->file, O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd == -1)
return (print_open_error(redir->file));
dup2(fd, STDOUT_FILENO);
close(fd);
}
else if (redir->type == REDIR_HEREDOC)
{
return (setup_heredoc(redir->file));
}
return (0);
}
4.6 ヒアドキュメント
ヒアドキュメントの概念
ヒアドキュメント(Here Document)は、シェルスクリプトで複数行のテキストを標準入力として渡す機能である。
cat << EOF
Line 1
Line 2
EOF
POSIX規格では、ヒアドキュメントのデリミタがクォートされていない場合、内容の変数展開が行われる。
ヒアドキュメントの実装
/**
* ヒアドキュメントを設定する
* @param delimiter デリミタ文字列
* @return 成功時0、失敗時-1
*/
int setup_heredoc(const char *delimiter)
{
int pipefd[2];
char *line;
if (pipe(pipefd) == -1)
{
perror("minishell: pipe");
return (-1);
}
/* ヒアドキュメントの内容を読み取る */
while (1)
{
line = readline("> ");
if (!line)
{
/* Ctrl+D で終了 */
ft_putendl_fd(
"minishell: warning: here-document delimited by end-of-file",
STDERR_FILENO);
break;
}
if (ft_strcmp(line, delimiter) == 0)
{
free(line);
break;
}
/* パイプに書き込む */
ft_putendl_fd(line, pipefd[1]);
free(line);
}
close(pipefd[1]);
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
return (0);
}
ヒアドキュメントのシグナル処理
/**
* ヒアドキュメント読み取り中のシグナルハンドラ
*/
void heredoc_sigint_handler(int sig)
{
(void)sig;
/* 改行を出力してプロンプトに戻る */
write(STDOUT_FILENO, "\n", 1);
g_shell.heredoc_interrupted = 1;
/* readline を中断 */
close(STDIN_FILENO);
}
/**
* ヒアドキュメントを安全に読み取る
*/
int safe_heredoc(const char *delimiter, t_shell *shell)
{
struct sigaction sa;
struct sigaction old_sa;
int result;
/* ヒアドキュメント用シグナルハンドラを設定 */
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = heredoc_sigint_handler;
sigaction(SIGINT, &sa, &old_sa);
shell->heredoc_interrupted = 0;
result = setup_heredoc(delimiter);
/* シグナルハンドラを復元 */
sigaction(SIGINT, &old_sa, NULL);
if (shell->heredoc_interrupted)
return (-1);
return (result);
}
4.7 シグナル処理
シェルにおけるシグナル
シェルは複数のモードでシグナルを異なる方法で処理する:
| モード | SIGINT (Ctrl+C) | SIGQUIT (Ctrl+\) | |--------|-----------------|------------------| | インタラクティブ | 新しいプロンプト | 無視 | | コマンド実行中 | 子に転送 | 子に転送 | | ヒアドキュメント | 読み取り中断 | 無視 |
インタラクティブモードのシグナル
/**
* インタラクティブモードのシグナルを設定する
*/
void setup_interactive_signals(void)
{
struct sigaction sa;
/* SIGINT: Ctrl+C */
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = interactive_sigint_handler;
sigaction(SIGINT, &sa, NULL);
/* SIGQUIT: Ctrl+\ を無視 */
signal(SIGQUIT, SIG_IGN);
}
/**
* インタラクティブモードのSIGINTハンドラ
*/
void interactive_sigint_handler(int sig)
{
(void)sig;
write(STDOUT_FILENO, "\n", 1);
rl_on_new_line();
rl_replace_line("", 0);
rl_redisplay();
}
コマンド実行中のシグナル
/**
* コマンド実行中のシグナルを設定する
*/
void setup_execution_signals(void)
{
/* 親プロセスではシグナルを無視 */
signal(SIGINT, SIG_IGN);
signal(SIGQUIT, SIG_IGN);
}
/**
* 子プロセスのシグナルを設定する
*/
void setup_child_signals(void)
{
/* デフォルトの動作に戻す */
signal(SIGINT, SIG_DFL);
signal(SIGQUIT, SIG_DFL);
}
4.8 終了ステータス
終了ステータスの規約
POSIXは終了ステータスの範囲を0-255と定義:
| ステータス | 意味 | |-----------|------| | 0 | 成功 | | 1 | 一般的なエラー | | 2 | シェルコマンドの誤用 | | 126 | コマンドは見つかったが実行不可 | | 127 | コマンドが見つからない | | 128+n | シグナルnで終了 | | 130 | Ctrl+C (SIGINT = 2) | | 131 | Ctrl+\ (SIGQUIT = 3) |
終了ステータスの管理
/**
* 子プロセスの終了ステータスを取得する
*/
int get_exit_status(int wait_status)
{
if (WIFEXITED(wait_status))
return (WEXITSTATUS(wait_status));
else if (WIFSIGNALED(wait_status))
{
int sig = WTERMSIG(wait_status);
/* SIGINTの場合、改行を出力 */
if (sig == SIGINT)
write(STDOUT_FILENO, "\n", 1);
/* SIGQUITの場合、"Quit" を出力 */
else if (sig == SIGQUIT)
ft_putendl_fd("Quit (core dumped)", STDERR_FILENO);
return (128 + sig);
}
return (1);
}
/**
* $? 変数を更新する
*/
void update_exit_status(t_shell *shell, int status)
{
shell->last_status = status;
}
4.9 ビルトインコマンド
ビルトインの必要性
一部のコマンドは、シェル自身のプロセスで実行する必要がある:
- cd: 作業ディレクトリを変更
- export: 環境変数を設定
- unset: 環境変数を削除
- exit: シェルを終了
子プロセスでこれらを実行しても、親シェルには影響しない。
ビルトイン判定
/**
* コマンドがビルトインかどうかを判定する
*/
int is_builtin(const char *cmd)
{
static const char *builtins[] = {
"echo", "cd", "pwd", "export", "unset", "env", "exit", NULL
};
int i;
if (!cmd)
return (0);
i = 0;
while (builtins[i])
{
if (ft_strcmp(cmd, builtins[i]) == 0)
return (1);
i++;
}
return (0);
}
ビルトインの実行
/**
* ビルトインコマンドを実行する
*/
int execute_builtin(t_ast *ast, t_shell *shell)
{
const char *cmd;
int saved_stdin;
int saved_stdout;
int status;
cmd = ast->argv[0];
/* 標準入出力をバックアップ */
saved_stdin = dup(STDIN_FILENO);
saved_stdout = dup(STDOUT_FILENO);
/* リダイレクトを設定 */
if (setup_redirections(ast->redirects) != 0)
{
restore_stdio(saved_stdin, saved_stdout);
return (1);
}
/* ビルトインを実行 */
if (ft_strcmp(cmd, "echo") == 0)
status = builtin_echo(ast->argv);
else if (ft_strcmp(cmd, "cd") == 0)
status = builtin_cd(ast->argv, shell);
else if (ft_strcmp(cmd, "pwd") == 0)
status = builtin_pwd();
else if (ft_strcmp(cmd, "export") == 0)
status = builtin_export(ast->argv, shell);
else if (ft_strcmp(cmd, "unset") == 0)
status = builtin_unset(ast->argv, shell);
else if (ft_strcmp(cmd, "env") == 0)
status = builtin_env(shell->env);
else if (ft_strcmp(cmd, "exit") == 0)
status = builtin_exit(ast->argv, shell);
else
status = 1;
/* 標準入出力を復元 */
restore_stdio(saved_stdin, saved_stdout);
return (status);
}
/**
* 標準入出力を復元する
*/
void restore_stdio(int saved_stdin, int saved_stdout)
{
dup2(saved_stdin, STDIN_FILENO);
dup2(saved_stdout, STDOUT_FILENO);
close(saved_stdin);
close(saved_stdout);
}
4.10 環境変数の管理
環境変数配列の生成
/**
* 環境変数リストをexecve用の配列に変換する
*/
char **env_to_array(t_env *env)
{
char **envp;
t_env *current;
int count;
int i;
/* エントリ数をカウント */
count = 0;
current = env;
while (current)
{
count++;
current = current->next;
}
/* 配列を割り当て */
envp = malloc(sizeof(char *) * (count + 1));
if (!envp)
return (NULL);
/* "KEY=VALUE" 形式で格納 */
i = 0;
current = env;
while (current)
{
envp[i] = create_env_string(current->key, current->value);
if (!envp[i])
{
free_array(envp);
return (NULL);
}
i++;
current = current->next;
}
envp[i] = NULL;
return (envp);
}
/**
* "KEY=VALUE" 文字列を作成する
*/
char *create_env_string(const char *key, const char *value)
{
char *result;
size_t key_len;
size_t value_len;
key_len = ft_strlen(key);
value_len = value ? ft_strlen(value) : 0;
result = malloc(key_len + 1 + value_len + 1);
if (!result)
return (NULL);
ft_memcpy(result, key, key_len);
result[key_len] = '=';
if (value)
ft_memcpy(result + key_len + 1, value, value_len);
result[key_len + 1 + value_len] = '\0';
return (result);
}
4.11 エラー処理
エラーメッセージの形式
Bashに準じたエラーメッセージ形式:
minishell: <コマンド>: <エラーメッセージ>
エラー処理関数
/**
* エラーメッセージを出力する
*/
void print_error(const char *cmd, const char *msg)
{
ft_putstr_fd("minishell: ", STDERR_FILENO);
if (cmd)
{
ft_putstr_fd(cmd, STDERR_FILENO);
ft_putstr_fd(": ", STDERR_FILENO);
}
ft_putendl_fd(msg, STDERR_FILENO);
}
/**
* open()失敗時のエラー処理
*/
int print_open_error(const char *file)
{
print_error(file, strerror(errno));
return (-1);
}
/**
* コマンドが見つからない場合のエラー
*/
void print_command_not_found(const char *cmd)
{
print_error(cmd, "command not found");
}
4.12 実行エンジンの統合
メイン実行関数
/**
* ASTを実行する
* @param ast 抽象構文木
* @param shell シェル状態
* @return 終了ステータス
*/
int execute(t_ast *ast, t_shell *shell)
{
int status;
if (!ast)
return (0);
if (ast->type == NODE_PIPE)
status = execute_pipeline(ast, shell);
else if (ast->type == NODE_COMMAND)
status = execute_command(ast, shell);
else
status = 1;
shell->last_status = status;
return (status);
}
4.13 まとめ
本章では、プロセス実行モデルとコマンド実行を学んだ:
次章では、パイプラインの実装について学ぶ。複数のコマンドをパイプで接続し、データをストリームとして処理する方法を解説する。
---