【Linux】进程控制(4)自主shell命令行解释器

【Linux】进程控制(4)自主shell命令行解释器

这一部分的目标是:自己动手写一个极简的 shell,通过这个过程把之前学过的进程控制知识(fork/exec/wait/signal/管道/重定向等)串联起来。

目标 shell 的功能范围(极简版)

  • 显示提示符(如 myshell$
  • 读取一行用户输入
  • 支持最基本的命令执行(不带参数也可以先支持)
  • 支持带参数的命令(空格分隔)
  • 支持后台运行(命令末尾有 &
  • 支持前台命令的 wait
  • 支持 exit / quit 退出
  • (可选扩展)支持管道 | 、重定向 > >> <&& ||

实现思路总览

while (true) {
    打印提示符
    读取整行输入 → line

    如果 line 为空 或 是 exit/quit → break

    解析 line → 切分出命令 + 参数数组 argv[]
                     → 判断是否有 & (后台)

    if (是内置命令,如 cd exit) {
        直接在当前进程执行
    } else {
        pid = fork()

        if (pid == 0) {   // 子进程
            execvp(命令, argv)
            perror("exec失败"); exit(1);
        } else {          // 父进程(shell)
            if (不是后台) {
                waitpid(pid, &status, 0);
            } else {
                printf("[后台任务] pid = %d\n", pid);
                // 可以选择不 wait,等它自己结束或用 waitpid(-1,...) 回收
            }
        }
    }
}

极简版代码(推荐先实现这个)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>

#define MAX_LINE    1024
#define MAX_ARGS    64

int main(void) {
    char line[MAX_LINE];
    char *argv[MAX_ARGS];
    char prompt[] = "myshell$ ";

    while (1) {
        printf("%s", prompt);
        fflush(stdout);

        // 读取整行
        if (!fgets(line, MAX_LINE, stdin)) {
            break;  // ctrl+D
        }

        // 去掉末尾换行
        line[strcspn(line, "\n")] = 0;

        // 空行直接继续
        if (strlen(line) == 0) continue;

        // 退出命令
        if (strcmp(line, "exit") == 0 || strcmp(line, "quit") == 0) {
            break;
        }

        // 解析参数
        int argc = 0;
        char *token = strtok(line, " \t");
        int background = 0;

        while (token) {
            if (strcmp(token, "&") == 0) {
                background = 1;
                break;  // & 后面不再解析
            }
            argv[argc++] = token;
            token = strtok(NULL, " \t");
        }
        argv[argc] = NULL;

        if (argc == 0) continue;

        // 内置命令(先简单处理 exit/cd)
        if (strcmp(argv[0], "cd") == 0) {
            if (argc < 2) {
                fprintf(stderr, "cd: 缺少参数\n");
            } else if (chdir(argv[1]) != 0) {
                perror("chdir");
            }
            continue;
        }

        // fork + exec
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork");
            continue;
        }

        if (pid == 0) {   // 子进程
            execvp(argv[0], argv);
            // 走到这里说明 exec 失败
            fprintf(stderr, "命令 '%s' 执行失败: %s\n", argv[0], strerror(errno));
            exit(127);
        }

        // 父进程
        if (!background) {
            int status;
            waitpid(pid, &status, 0);
        } else {
            printf("[后台作业] pid = %d\n", pid);
            // 可以不 wait,让它成为孤儿进程,由 init 回收
            // 或者后续用 waitpid(-1, ...) 非阻塞回收
        }
    }

    printf("\nbye~\n");
    return 0;
}

编译 & 运行

gcc -o myshell myshell.c
./myshell

然后就可以输入:

myshell$ ls -l
myshell$ sleep 10 &
myshell$ pwd
myshell$ cd /tmp
myshell$ exit

进阶功能(按难度递增)

优先级功能关键实现点难度
★☆☆支持管道 cmd1 | cmd2fork 两次 + pipe() + dup2★★★★
★★☆支持重定向 > >> <解析 > >> <,用 open() + dup2★★★
★★☆回收所有后台进程(waitpid)非阻塞 waitpid(-1, &status, WNOHANG) 循环★★
★★★支持 && ||根据前一个命令的退出码决定是否执行后一个★★★★
★★★实现 ! 历史命令保存历史链表,按 !n 执行★★★
★★★★支持环境变量展开 $HOME解析 $ 开头的词,用 getenv() 替换★★★★
★★★★支持通配符 * ?用 glob() 或自己实现匹配★★★★★

推荐进阶顺序(比较实用)

  1. 先把上面的极简版跑通
  2. 加上后台进程的非阻塞回收(每轮循环末尾试一次 waitpid(-1, …, WNOHANG))
  3. 实现最简单的重定向(只支持 ><
  4. 再实现单级管道(最有成就感)

小提示

  • 解析命令行时建议用 strtok_r()(可重入版本)更安全
  • 处理输入时考虑连续空格、tab、行首尾空格
  • execvp() 失败时退出码常用 127(command not found)
  • 后台进程结束时如果不回收,会出现僵尸进程(zombie)

如果你现在想继续做这个项目,可以告诉我你想先实现哪个功能(管道、重定向、后台回收、历史命令……),我可以给你对应的核心代码片段和注意事项。

祝你写出一个属于自己的 shell~

文章已创建 4323

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部