第6章: ビルトインコマンドとシグナル処理の理論
6.1 ビルトインコマンドの理論
なぜビルトインが必要か
UNIXシェルにおいて、一部のコマンドはビルトイン(Built-in)として実装される必要がある。これは、プロセスモデルの本質的な制約に起因する。
プロセスアドレス空間の独立性:
親プロセス(シェル) 子プロセス
┌─────────────────────┐ ┌─────────────────────┐
│ 環境変数 │ │ 環境変数(コピー) │
│ カレントディレクトリ│ │ カレントディレクトリ│
│ シェル状態 │ │ 独立した状態 │
└─────────────────────┘ └─────────────────────┘
↑ ↑
│ │
変更が永続化 exit時に破棄
子プロセスでchdir()を実行しても、親プロセスのカレントディレクトリには影響しない。これはfork()によりアドレス空間がコピーされるためである。
POSIXビルトイン要件
POSIX.1-2017(IEEE Std 1003.1-2017)では、シェルが実装すべきビルトインを規定している:
Special Built-In Utilities(特殊ビルトイン):
break, :, continue, ., eval, exec, exit, export,
readonly, return, set, shift, times, trap, unset
Regular Built-In Utilities(通常ビルトイン):
alias, bg, cd, command, false, fc, fg, getopts,
hash, jobs, kill, newgrp, pwd, read, true,
umask, unalias, wait
minishellで実装するビルトイン:
echo: 引数の出力cd: ディレクトリ変更pwd: 現在のディレクトリ表示export: 環境変数の設定unset: 環境変数の削除env: 環境変数の一覧exit: シェルの終了
ビルトインの実行フロー
コマンド入力
│
↓
┌─────────────────┐
│ ビルトインか? │
└─────────────────┘
│ │
YES NO
│ │
↓ ↓
┌─────────┐ ┌─────────┐
│ 親で実行│ │ fork() │
│ │ │ execve()│
└─────────┘ └─────────┘
6.2 環境変数の理論
環境変数の歴史
環境変数(Environment Variables)は、Version 7 UNIX(1979年)で導入された。Bourneシェルの設計者Stephen Bourneが、プロセス間で設定を継承するメカニズムとして考案した。
環境変数の構造
POSIX規定の環境変数形式:
NAME=value
制約:
- NAMEは英字またはアンダースコアで始まる
- NAMEは英数字とアンダースコアのみを含む
- 大文字小文字を区別する
- valueは任意の文字列(NULLを除く)
プロセスと環境変数
execve()システムコールは環境変数を受け取る:
int execve(const char *pathname, char *const argv[], char *const envp[]);
envpは"KEY=value"形式の文字列配列で、NULLで終端される。
execve時の環境変数:
envp[0] → "PATH=/usr/bin:/bin"
envp[1] → "HOME=/home/user"
envp[2] → "USER=username"
envp[3] → "PWD=/current/dir"
envp[4] → NULL
環境変数のデータ構造
typedef struct s_env
{
char *key; /* 変数名 */
char *value; /* 値 */
int exported; /* export済みフラグ */
struct s_env *next; /* 次のノード */
} t_env;
連結リストを使用する理由:
- 動的な追加・削除が容易
- サイズの変更が柔軟
- 順序の維持が可能
環境変数の操作
/* 環境変数の取得 */
char *get_env_value(t_env *env, const char *key)
{
while (env)
{
if (ft_strcmp(env->key, key) == 0)
return (env->value);
env = env->next;
}
return (NULL);
}
/* 環境変数の設定 */
int set_env_value(t_env **env, const char *key, const char *value)
{
t_env *current;
t_env *new_node;
/* 既存の変数を探す */
current = *env;
while (current)
{
if (ft_strcmp(current->key, key) == 0)
{
free(current->value);
current->value = ft_strdup(value);
return (0);
}
current = current->next;
}
/* 新しい変数を追加 */
new_node = create_env_node(key, value);
if (!new_node)
return (-1);
add_env_node(env, new_node);
return (0);
}
6.3 echoコマンドの実装
POSIX仕様
POSIXではechoの動作を以下のように規定している:
echo [-n] [string ...]
-n: 末尾の改行を出力しない
注意: POSIXでは-nの動作は実装依存としている。bashでは-nを認識するが、一部のシステムでは文字列として扱う。
実装
int builtin_echo(char **argv)
{
int i;
int newline;
int j;
newline = 1;
i = 1;
/* -nオプションの処理(複数可: -n -nn -nnn等) */
while (argv[i] && argv[i][0] == '-')
{
j = 1;
while (argv[i][j] == 'n')
j++;
/* -nの後に他の文字がなければオプション */
if (argv[i][j] == '\0' && j > 1)
{
newline = 0;
i++;
}
else
break;
}
/* 引数を出力 */
while (argv[i])
{
ft_putstr_fd(argv[i], STDOUT_FILENO);
if (argv[i + 1])
ft_putchar_fd(' ', STDOUT_FILENO);
i++;
}
/* 改行(-nがなければ) */
if (newline)
ft_putchar_fd('\n', STDOUT_FILENO);
return (0); /* echoは常に成功 */
}
6.4 cdコマンドの実装
cdの理論
cd(change directory)は、プロセスのカレントディレクトリを変更する。内部的にはchdir()システムコールを使用する。
#include <unistd.h>
int chdir(const char *path);
POSIX仕様
cd [-L|-P] [directory]
-L: シンボリックリンクを論理的にたどる(デフォルト)
-P: シンボリックリンクを物理的にたどる
特殊なケース:
cd:$HOMEに移動cd -:$OLDPWDに移動し、そのパスを出力cd ~:$HOMEに移動
実装
int builtin_cd(char **argv, t_shell *shell)
{
char *path;
char *oldpwd;
char *newpwd;
/* 引数なし: HOMEへ移動 */
if (!argv[1])
{
path = get_env_value(shell->env, "HOME");
if (!path)
{
print_error("cd", "HOME not set");
return (1);
}
}
/* cd -: OLDPWDへ移動 */
else if (ft_strcmp(argv[1], "-") == 0)
{
path = get_env_value(shell->env, "OLDPWD");
if (!path)
{
print_error("cd", "OLDPWD not set");
return (1);
}
/* パスを表示 */
ft_putendl_fd(path, STDOUT_FILENO);
}
/* 通常のパス */
else
{
path = argv[1];
}
/* 現在のディレクトリを保存 */
oldpwd = getcwd(NULL, 0);
/* ディレクトリ変更 */
if (chdir(path) == -1)
{
print_error_with_arg("cd", path, strerror(errno));
free(oldpwd);
return (1);
}
/* OLDPWD更新 */
if (oldpwd)
{
set_env_value(&shell->env, "OLDPWD", oldpwd);
free(oldpwd);
}
/* PWD更新 */
newpwd = getcwd(NULL, 0);
if (newpwd)
{
set_env_value(&shell->env, "PWD", newpwd);
free(newpwd);
}
return (0);
}
引数の検証
int validate_cd_args(char **argv)
{
struct stat st;
/* 引数が多すぎる */
if (argv[1] && argv[2])
{
print_error("cd", "too many arguments");
return (1);
}
if (argv[1] && ft_strcmp(argv[1], "-") != 0)
{
/* パスの存在確認 */
if (stat(argv[1], &st) == -1)
{
print_error_with_arg("cd", argv[1], strerror(errno));
return (1);
}
/* ディレクトリかどうか */
if (!S_ISDIR(st.st_mode))
{
print_error_with_arg("cd", argv[1], "Not a directory");
return (1);
}
/* 実行権限(検索権限)の確認 */
if (access(argv[1], X_OK) == -1)
{
print_error_with_arg("cd", argv[1], strerror(errno));
return (1);
}
}
return (0);
}
6.5 pwdコマンドの実装
getcwd()システムコール
#include <unistd.h>
char *getcwd(char *buf, size_t size);
bufがNULLの場合、getcwd()は必要なサイズのメモリを動的に確保する(POSIX.1-2001拡張)。
実装
int builtin_pwd(void)
{
char *cwd;
cwd = getcwd(NULL, 0);
if (!cwd)
{
perror("pwd");
return (1);
}
ft_putendl_fd(cwd, STDOUT_FILENO);
free(cwd);
return (0);
}
6.6 exportコマンドの実装
動作仕様
export [name[=value] ...]
引数なし: 全てのexport済み変数を表示
name=value: 変数を設定しexport
name: 変数をexport(値なし)
変数名の検証
POSIX規定の識別子規則:
int is_valid_identifier(const char *str)
{
int i;
if (!str || !*str)
return (0);
/* 最初の文字: 英字またはアンダースコア */
if (!ft_isalpha(str[0]) && str[0] != '_')
return (0);
/* 残りの文字: 英数字またはアンダースコア */
i = 1;
while (str[i] && str[i] != '=')
{
if (!ft_isalnum(str[i]) && str[i] != '_')
return (0);
i++;
}
return (1);
}
実装
int builtin_export(char **argv, t_shell *shell)
{
int i;
int status;
/* 引数なし: 環境変数を表示 */
if (!argv[1])
{
print_exported_vars(shell->env);
return (0);
}
status = 0;
i = 1;
while (argv[i])
{
if (!is_valid_identifier(argv[i]))
{
print_export_error(argv[i]);
status = 1;
}
else
{
export_variable(shell, argv[i]);
}
i++;
}
return (status);
}
void export_variable(t_shell *shell, char *arg)
{
char *key;
char *value;
char *equals;
equals = ft_strchr(arg, '=');
if (equals)
{
/* KEY=value形式 */
key = ft_substr(arg, 0, equals - arg);
value = ft_strdup(equals + 1);
set_env_value(&shell->env, key, value);
mark_exported(shell->env, key);
free(key);
free(value);
}
else
{
/* KEYのみ(exportマークを付ける) */
mark_exported(shell->env, arg);
}
}
export時の表示
void print_exported_vars(t_env *env)
{
t_env **sorted;
int count;
int i;
/* 環境変数をソート */
count = count_env_vars(env);
sorted = sort_env_vars(env, count);
if (!sorted)
return;
/* 表示 */
i = 0;
while (i < count)
{
ft_putstr_fd("declare -x ", STDOUT_FILENO);
ft_putstr_fd(sorted[i]->key, STDOUT_FILENO);
if (sorted[i]->value)
{
ft_putstr_fd("=\"", STDOUT_FILENO);
ft_putstr_fd(sorted[i]->value, STDOUT_FILENO);
ft_putchar_fd('"', STDOUT_FILENO);
}
ft_putchar_fd('\n', STDOUT_FILENO);
i++;
}
free(sorted);
}
6.7 unsetコマンドの実装
動作仕様
unset [name ...]
指定された変数を削除する。
実装
int builtin_unset(char **argv, t_shell *shell)
{
int i;
int status;
if (!argv[1])
return (0);
status = 0;
i = 1;
while (argv[i])
{
if (!is_valid_env_name(argv[i]))
{
print_unset_error(argv[i]);
status = 1;
}
else
{
remove_env_var(&shell->env, argv[i]);
}
i++;
}
return (status);
}
void remove_env_var(t_env **env, const char *key)
{
t_env *current;
t_env *prev;
current = *env;
prev = NULL;
while (current)
{
if (ft_strcmp(current->key, key) == 0)
{
/* ノードを削除 */
if (prev)
prev->next = current->next;
else
*env = current->next;
free(current->key);
free(current->value);
free(current);
return;
}
prev = current;
current = current->next;
}
}
6.8 envコマンドの実装
動作仕様
env
値が設定されている全ての環境変数を表示する。
実装
int builtin_env(t_env *env)
{
while (env)
{
/* 値がある変数のみ表示 */
if (env->value && env->exported)
{
ft_putstr_fd(env->key, STDOUT_FILENO);
ft_putchar_fd('=', STDOUT_FILENO);
ft_putendl_fd(env->value, STDOUT_FILENO);
}
env = env->next;
}
return (0);
}
6.9 exitコマンドの実装
動作仕様
exit [n]
終了ステータスnでシェルを終了する。
nが省略された場合、直前のコマンドの終了ステータスを使用する。
終了ステータスの理論
POSIX規定の終了ステータス:
- 0: 成功
- 1-125: コマンド固有のエラー
- 126: コマンドは見つかったが実行不可
- 127: コマンドが見つからない
- 128+N: シグナルNによる終了
実装
int builtin_exit(char **argv, t_shell *shell)
{
long long exit_code;
ft_putendl_fd("exit", STDERR_FILENO);
/* 引数なし: 直前の終了ステータス */
if (!argv[1])
{
cleanup_shell(shell);
exit(shell->last_exit);
}
/* 数値でない場合 */
if (!is_numeric_argument(argv[1]))
{
print_exit_error(argv[1], "numeric argument required");
cleanup_shell(shell);
exit(2);
}
/* 引数が多すぎる */
if (argv[2])
{
print_error("exit", "too many arguments");
return (1); /* シェルは終了しない */
}
/* オーバーフローチェック */
if (!is_valid_exit_value(argv[1], &exit_code))
{
print_exit_error(argv[1], "numeric argument required");
cleanup_shell(shell);
exit(2);
}
cleanup_shell(shell);
exit((unsigned char)exit_code);
}
数値引数の検証
int is_numeric_argument(const char *str)
{
int i;
if (!str || !*str)
return (0);
i = 0;
/* 符号 */
if (str[i] == '+' || str[i] == '-')
i++;
/* 少なくとも1つの数字が必要 */
if (!str[i])
return (0);
/* 全て数字 */
while (str[i])
{
if (!ft_isdigit(str[i]))
return (0);
i++;
}
return (1);
}
int is_valid_exit_value(const char *str, long long *result)
{
long long num;
int sign;
int i;
num = 0;
sign = 1;
i = 0;
if (str[i] == '-')
{
sign = -1;
i++;
}
else if (str[i] == '+')
i++;
while (str[i])
{
/* オーバーフローチェック */
if (num > (LLONG_MAX - (str[i] - '0')) / 10)
return (0);
num = num * 10 + (str[i] - '0');
i++;
}
*result = num * sign;
return (1);
}
6.10 シグナル処理の理論
シグナルの歴史
シグナルは、UNIX Version 4(1973年)で導入されたソフトウェア割り込みメカニズムである。初期のシグナルは信頼性に欠けており、BSD信頼シグナル(4.2BSD, 1983年)で改善された。
POSIX.1-1990で標準化され、現在はPOSIX.1-2017が最新規格である。
シグナルの分類
1. プロセス制御シグナル
SIGINT (2) - 端末割り込み(Ctrl+C)
SIGQUIT (3) - 端末終了(Ctrl+\)
SIGTERM (15) - 終了要求
SIGKILL (9) - 強制終了(捕捉不可)
SIGSTOP (19) - 停止(捕捉不可)
2. 端末シグナル
SIGTSTP (20) - 端末停止(Ctrl+Z)
SIGTTIN (21) - バックグラウンド読み取り
SIGTTOU (22) - バックグラウンド書き込み
3. パイプシグナル
SIGPIPE (13) - 壊れたパイプへの書き込み
4. 子プロセスシグナル
SIGCHLD (17) - 子プロセスの状態変化
sigaction()システムコール
POSIXでは、signal()よりもsigaction()の使用を推奨している:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int); /* ハンドラ関数 */
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask; /* ブロックするシグナル */
int sa_flags; /* オプションフラグ */
};
非同期シグナルセーフ関数
シグナルハンドラ内で安全に呼び出せる関数は限られている(POSIX.1-2017 表2-4):
write(), _exit(), signal()
(その他多数、ただしprintf(), malloc(), free()は含まれない)
重要: シグナルハンドラ内ではwrite()を使用し、printf()は使用しない。
6.11 シェルのシグナル処理
インタラクティブモード
プロンプト表示中の動作:
void setup_interactive_signals(void)
{
struct sigaction sa_int;
struct sigaction sa_quit;
/* SIGINT: 新しいプロンプトを表示 */
sigemptyset(&sa_int.sa_mask);
sa_int.sa_flags = 0;
sa_int.sa_handler = handle_sigint_interactive;
sigaction(SIGINT, &sa_int, NULL);
/* SIGQUIT: 無視 */
sigemptyset(&sa_quit.sa_mask);
sa_quit.sa_flags = 0;
sa_quit.sa_handler = SIG_IGN;
sigaction(SIGQUIT, &sa_quit, NULL);
}
void handle_sigint_interactive(int sig)
{
(void)sig;
write(STDOUT_FILENO, "\n", 1);
/* readlineの状態をリセット */
rl_on_new_line();
rl_replace_line("", 0);
rl_redisplay();
/* 終了ステータスを130に設定 */
g_signal_status = 130;
}
コマンド実行モード
子プロセス実行中の動作:
void setup_execution_signals(void)
{
struct sigaction sa;
/* デフォルト動作に戻す */
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = SIG_DFL;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGQUIT, &sa, NULL);
}
ヒアドキュメントモード
ヒアドキュメント入力中の動作:
void setup_heredoc_signals(void)
{
struct sigaction sa;
/* SIGINT: 入力をキャンセル */
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = handle_sigint_heredoc;
sigaction(SIGINT, &sa, NULL);
/* SIGQUIT: 無視 */
signal(SIGQUIT, SIG_IGN);
}
void handle_sigint_heredoc(int sig)
{
(void)sig;
write(STDOUT_FILENO, "\n", 1);
/* ヒアドキュメントを中断 */
g_heredoc_interrupted = 1;
/* readlineを終了させる */
rl_done = 1;
g_signal_status = 130;
}
6.12 グローバル変数とシグナル
volatile sig_atomic_t
シグナルハンドラとメインプログラム間で共有する変数は、volatile sig_atomic_t型を使用する:
volatile sig_atomic_t g_signal_status = 0;
volatile: コンパイラの最適化を防ぐ
sig_atomic_t: アトミックなアクセスを保証
グローバル変数の設計
/* 最小限のグローバル変数 */
typedef struct s_signal_state
{
volatile sig_atomic_t received_signal;
volatile sig_atomic_t in_heredoc;
} t_signal_state;
t_signal_state g_signal;
POSIXシグナルハンドラでは、グローバル変数への書き込みはsig_atomic_t型のみが安全である。
6.13 ビルトインのディスパッチ
コマンドテーブル
typedef int (*t_builtin_func)(char **argv, t_shell *shell);
typedef struct s_builtin
{
const char *name;
t_builtin_func func;
} t_builtin;
static const t_builtin g_builtins[] = {
{"echo", builtin_echo_wrapper},
{"cd", builtin_cd},
{"pwd", builtin_pwd_wrapper},
{"export", builtin_export},
{"unset", builtin_unset},
{"env", builtin_env_wrapper},
{"exit", builtin_exit},
{NULL, NULL}
};
ビルトインの判定と実行
int is_builtin(const char *cmd)
{
int i;
if (!cmd)
return (0);
i = 0;
while (g_builtins[i].name)
{
if (ft_strcmp(cmd, g_builtins[i].name) == 0)
return (1);
i++;
}
return (0);
}
int execute_builtin(t_cmd *cmd, t_shell *shell)
{
int i;
i = 0;
while (g_builtins[i].name)
{
if (ft_strcmp(cmd->argv[0], g_builtins[i].name) == 0)
return (g_builtins[i].func(cmd->argv, shell));
i++;
}
return (1); /* 到達しないはず */
}
6.14 パイプ内のビルトイン
問題
ビルトインがパイプ内にある場合、子プロセスで実行する必要がある:
echo hello | cat # echoは子プロセスで実行
cd /tmp | pwd # cdは子プロセスで実行(親には影響なし)
実装
int execute_builtin_in_pipeline(t_cmd *cmd, t_shell *shell)
{
pid_t pid;
int status;
pid = fork();
if (pid == -1)
return (1);
if (pid == 0)
{
/* 子プロセス */
setup_execution_signals();
if (cmd->redirects)
{
if (setup_redirections(cmd->redirects) != 0)
exit(1);
}
exit(execute_builtin(cmd, shell));
}
waitpid(pid, &status, 0);
return (WEXITSTATUS(status));
}
6.15 特殊な環境変数
SHLVL
シェルのネストレベルを追跡:
void init_shlvl(t_shell *shell)
{
char *shlvl_str;
int level;
char *new_level;
shlvl_str = get_env_value(shell->env, "SHLVL");
if (shlvl_str)
level = ft_atoi(shlvl_str) + 1;
else
level = 1;
if (level < 1)
level = 1;
else if (level > 999)
level = 1; /* リセット */
new_level = ft_itoa(level);
set_env_value(&shell->env, "SHLVL", new_level);
free(new_level);
}
PWDとOLDPWD
void init_pwd_vars(t_shell *shell)
{
char *cwd;
cwd = getcwd(NULL, 0);
if (cwd)
{
set_env_value(&shell->env, "PWD", cwd);
free(cwd);
}
/* OLDPWDは初期化しない(未定義のまま) */
}
6.16 まとめ
本章では、ビルトインコマンドとシグナル処理について学んだ:
- ビルトインの必要性: プロセスアドレス空間の独立性
- 環境変数: 構造と操作
- 各ビルトイン: echo, cd, pwd, export, unset, env, exit
- シグナル理論: POSIX規格とsigaction()
- シグナル処理: インタラクティブ/実行/ヒアドキュメントモード
- グローバル変数: volatile sig_atomic_t
- IEEE (2017). "POSIX.1-2017: Shell & Utilities", IEEE Std 1003.1-2017
- Bourne, S. R. (1978). "The UNIX Shell", Bell System Technical Journal, 57(6)
- Stevens, W. R., & Rago, S. A. (2013). "Advanced Programming in the UNIX Environment", 3rd Edition, Addison-Wesley
- Kerrisk, M. (2010). "The Linux Programming Interface", No Starch Press
- Rochkind, M. J. (2004). "Advanced UNIX Programming", 2nd Edition, Addison-Wesley
- McKusick, M. K., et al. (1996). "The Design and Implementation of the 4.4BSD Operating System", Addison-Wesley
- Bach, M. J. (1986). "The Design of the UNIX Operating System", Prentice Hall
これでminishellの実装に必要な全ての理論的基盤を学んだ。各コンポーネントを統合し、完全に動作するシェルを構築することができる。
---