Linux 指令分內建 (Shell 進程直接執行,如 `cd`, `exit`, `export`) 與外部 (獨立可執行檔,如 `ls`, `grep`, `cat`)。Shell 核心迴圈:**讀取 → 解析 → 派生子進程執行外部指令 / 直接執行內建指令 → 等待/繼續**。
#include
#include
#include
#include
#include
#include
#define MAX_LINE 1024
#define MAX_ARGS 64
void print_prompt(void) {
char cwd[MAX_LINE], host[MAX_LINE];
const char *user = getenv("USER");
gethostname(host, sizeof(host));
getcwd(cwd, sizeof(cwd));
printf("%s@%s:%s%s ", user, host, cwd, (geteuid()==0) ? "#" : "$");
}
int parse_line(char *line, char **argv) {
int argc = 0;
char *token = strtok(line, " \t\n");
while (token && argc < MAX_ARGS-1) {
argv[argc++] = token;
token = strtok(NULL, " \t\n");
}
argv[argc] = NULL;
return argc;
}
bool is_builtin(const char *cmd) {
return strcmp(cmd, "cd") == 0 || strcmp(cmd, "exit") == 0;
}
int exec_builtin(char **argv) {
if (strcmp(argv[0], "cd") == 0) {
const char *dir = argv[1] ? argv[1] : getenv("HOME");
if (chdir(dir) != 0) perror("cd");
return 1; // 內建指令已處理
}
if (strcmp(argv[0], "exit") == 0) exit(0);
return 0; // 非內建
}
int main(void) {
char line[MAX_LINE];
char *argv[MAX_ARGS];
while (1) {
print_prompt();
if (!fgets(line, MAX_LINE, stdin)) break; // EOF (Ctrl+D)
int argc = parse_line(line, argv);
if (argc == 0) continue; // 空行
if (exec_builtin(argv)) continue; // 內建指令直接處理
/* 外部指令:fork + execvp */
pid_t pid = fork();
if (pid < 0) { perror("fork"); continue; }
if (pid == 0) { // 子進程
execvp(argv[0], argv);
perror(argv[0]); // execvp 失敗才會執行到這
_exit(1);
} else { // 父進程
int status;
waitpid(pid, &status, 0);
}
}
return 0;
}
sequenceDiagram
Shell->>fork: 建立子進程
Child->>execvp: 替換映像為指令程式
Parent->>waitpid: 等待子進程結束
Child-->>Kernel: 退出碼
Parent->>Shell: 繼續迴圈
/* ls | grep c */
int pipefd[2];
pipe(pipefd);
if (fork() == 0) { // 左側指令
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[0]); close(pipefd[1]);
execvp("ls", (char*[]){"ls", NULL});
}
if (fork() == 0) { // 右側指令
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]); close(pipefd[1]);
execvp("grep", (char*[]){"grep", "c", NULL});
}
close(pipefd[0]); close(pipefd[1]);
wait(NULL); wait(NULL);
/* cmd > out.txt */
int fd = open("out.txt", O_WRONLY|O_CREAT|O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO);
close(fd);
execvp(...);
/* export VAR=value */
setenv("VAR", "value", 1);
/* alias ll='ls -l' */
typedef struct { char *name; char *value; } Alias;
$ gcc -std=c99 -Wall -Wextra -o mysh mysh.c
$ ./mysh
user@host:/home/user$ ls -l
total 12
-rwxr-xr-x 1 user user 8432 Jan 1 12:00 mysh
-rw-r--r-- 1 user user 215 Jan 1 12:00 mysh.c
user@host:/home/user$ cd ..
user@host:/home$ pwd
/home
user@host:/home$ exit
$
| 現象 | 原因 | 修正 |
|---|---|---|
| `cd` 無效 | 在子進程 `chdir` | 父進程直接呼叫 `chdir` |
| 管道掛住 | 未關閉未使用的管道端點 | 每進程 `close` 所有不用的 `pipefd` |
| 殭屍進程累積 | 背景任務未 `wait` | 安裝 `SIGCHLD` handler 呼叫 `waitpid(-1, NULL, WNOHANG)` |
| 指令未找到 | `PATH` 未正確傳遞 | `execvp` 自動用環境 `PATH`,確認 `environ` 完整 |
一個極簡 Shell 只需 **`fork` + `execvp` + `waitpid` + 內建指令分派**。在這 100 行骨架上,逐步加入管道、重導向、作業控制、別名、歷史、補全,即可演化成功能完整的 Shell。理解這個核心迴圈,是掌握 Unix 進程模型與 Shell 內部機制的最佳切入點。