【Linux】 自主shell编写巩固复习篇


一、全局配置与状态(代码最开头)

变量 类型 作用
basesize = 1024 const int 命令行缓冲区和 pwd 缓冲区大小
argvnum = 64 const int gargv 最多存 64 个参数
envnum = 64 const int 自维护环境表 genv 最多 64 项
gargv[64] static char*\[\] 解析后的参数表,末尾必须 NULL(exec 族要求)
gargc static int 参数个数
lastcode static int 上一条命令退出码,echo $? 就读它
genv[64] static char*\[\] shell 自己维护的环境变量表(后面重点讲)
pwd[1024] / pwdenv[1024] static char\[\] 当前目录 / 拼好的 "PWD=xxx" 字符串

几个设计要点:

  • 这些都是全局 static,生命周期等于整个程序,后续函数随便用。
  • gargv 是 char* 数组,指向命令行缓冲区内部的各个单词(strtok 原地切出来的),不是拷贝。
  • lastcode 是整个 shell 的状态核心------echo $? 的 $? 不是系统变量,就是打印这个变量。

二、提示符函数组(6 个函数,作用一条链)

复制代码
GetUserName → GetHostName → GetPwd → LastDir → MakeCommandLine → PrintCommandLine

GetUserName / GetHostName:

getenv("USER")、getenv("HOSTNAME"),从进程环境表读用户名和主机名。

找不到返回 "None"。

注意 getenv 返回的指针是只读的(指向 environ 表内部),不要尝试修改。

GetPwd(含一个关键动作):

cpp 复制代码
if (getcwd(pwd, sizeof(pwd)) == NULL) return "None";
snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);
putenv(pwdenv);
  • getcwd 是系统调用:问内核"我的当前工作目录在哪",结果写入 pwd。
  • snprintf 拼出 "PWD=/home/xx" 存进全局 pwdenv。
  • putenv(pwdenv) 把这个字符串写回进程的真实环境表(注意:写的是 pwd 里的,不是 genv,这就是后面的坑之一,详见第八节)。
维度 getcwd getenv
头文件 <unistd.h> <stdlib.h>
获取对象 当前工作目录(绝对路径) 环境变量的值
结果存哪 你自己的缓冲区(或动态分配) 进程环境块里的内部静态区
谁管生命周期 调用者 进程环境(后续调用可能覆盖)
失败表现 返回 NULL + errno(ERANGE 缓冲区太小 / EACCES 无权限 / ENOENT 目录被删) 返回 NULL,无 errno 区分 "不存在"
对应的修改函数 chdir() setenv() / putenv()

LastDir:

取最后一个 / 后面的部分做提示符目录显示(/home/xx → xx)。

MakeCommandLine:拼成 [user@host dir]# 。

PrintCommandLine:

复制代码
printf("%s", MakeCommandLine().c_str());
fflush(stdout);     // 必须 fflush,因为提示符没 \n

这是难点:

printf 默认是行缓冲(有 \n 才真正输出),提示符没有换行,所以必须手动 fflush,否则你输入命令时提示符还憋在缓冲区里没显示出来。


三、GetCommandLine:读一行

cpp 复制代码
if (fgets(buf, size, stdin) == NULL) { printf("\n"); return false; }
buf[strlen(buf) - 1] = '\0';   // 去掉 fgets 读进来的 \n
if (strlen(buf) == 0) return false;
  • fgets 读到换行或缓冲区满为止,换行会保留在 buf 里,所以要手动把最后一位(\n)换成 '\0'。
  • 三个返回值语义:
    • 正常一行 → true
    • 空行(直接回车,去 \n 后为空)→ false,主循环 continue 重新打印提示符
    • EOF(Ctrl+D)→ 打印换行、false------这里埋了个 bug,见第八节。

四、ParseCommandLine:strtok 拆词(难点①)

复制代码
const char *sep = " ";
gargv[gargc++] = strtok(buf, sep);          // 第一次调用传 buf
while ((gargv[gargc++] = strtok(NULL, sep)) != NULL)
    ;
gargc--;

strtok 的工作原理(这是最值得深挖的地方):

  1. 第一次调用 strtok(buf, " "):从 buf 开头找第一个非空格字符,记下单词起点;再从那里往后找空格,找到就把空格替换成 '\0',返回单词起点指针。同时,strtok 内部用一个静态指针记住当前位置(停在刚才那个 '\0' 之后)。
  2. 后续调用 strtok(NULL, " "):不传新字符串,strtok 从内部记住的位置继续切下一个单词。
  3. 连续空格自动跳过------所以 ls -l 和 ls -l 结果一样。
  4. 它直接修改原字符串:buf 里的空格被改写成 '\0',于是 buf 被原地拆成好几段,gargv[i] 只是指向这些段的首地址。

举例:

输入 ls -l /home,解析后内存里是 ls\0-l\0/home\0,gargv = {"ls", "-l", "/home", NULL}。

最后那个 gargc-- 是因为 while 循环在赋 NULL 之后又把 gargc 加了一次(循环条件先赋值后判断),所以要多退一格,但 NULL 已经写进数组------这正是 exec 族需要的结尾标记。

潜在崩溃点:

如果输入是纯空格(如 " "),第一个 strtok 返回 NULL,gargv[0]=NULL,后面 CheckAndExecBuiltCommand 里 strcmp(gargv[0], "exit") 会对 NULL 做 strcmp → 段错误。


五、环境变量:InitEnv / AddEnv(难点②:三套环境表)

这个程序里其实有三套环境,初学者最容易混淆:

表 是谁 怎么产生的 谁在用
environ 内核 + libc 维护的进程真实环境表 (extern char **environ,以 NULL 结尾) 进程启动时由 exec 传入 getenv / putenv 操作它
genv shell 自己 malloc 拷贝的一份 InitEnv 从 environ 拷贝,export 追加 传给 execvpe 给子进程
pwdenv 全局静态数组 GetPwd 每次拼 PWD=xxx 并 putenv 只写回 environ,不进 genv

InitEnv:

cpp 复制代码
extern char **environ;      // 声明:进程真实环境表
for (; environ[i] && i < envnum - 1; i++) {
    genv[i] = (char *)malloc(strlen(environ[i]) + 1);
    strcpy(genv[i], environ[i]);
}
genv[i] = NULL;

为什么要 malloc 拷贝而不是直接 genv[i] = environ[i]?

因为 putenv、setenv 可能会移动/改写 environ 表项,拷一份自己的副本才安全。这个 shell 后续要给子进程一份"自己说了算"的环境(用户 export 加进去的变量也要在),所以它维护自己的 genv。

AddEnv:

cpp 复制代码
while (genv[i]) i++;          // 找到末尾
genv[i] = malloc(strlen(item)+1);
strcpy(genv[i], item);
genv[++i] = NULL;

注意它没有检查 genv 是否已满 64 项------export 超过 64 次会数组越界写(内存破坏)。


六、ExecuteCommand:fork / exec / waitpid(难点③,全程序核心)

cpp 复制代码
pid_t id = fork();
if (id < 0) { perror("fork"); lastcode = 1; return; }
if (id == 0) {
    execvpe(gargv[0], gargv, genv);   // 子进程
    perror(gargv[0]);                 // exec 失败才会走到这
    exit(127);
}
int status = 0;
waitpid(id, &status, 0);              // 父进程
if (WIFEXITED(status))
    lastcode = WEXITSTATUS(status);
else if (WIFSIGNALED(status))
    lastcode = 128 + WTERMSIG(status);
else
    lastcode = 100;

① fork():一次调用,两次返回

  • fork 会复制当前进程(写时拷贝 COW:不修改数据时父子共享物理内存页,谁先改谁复制),创建子进程。
  • 返回值语义:父进程得到子进程的 PID(>0);子进程得到 0;失败得 -1。
  • 判断 id == 0 就是在问"我现在是不是子进程?"------这就是 fork 后父子代码分道扬镳的经典写法。

② execvpe:exec 家族里功能最全的一个 execvpe(文件名, argv, envp) 拆开看:

  • exec:用新程序替换当前进程映像------代码段、数据段、堆栈全换,PID 不变。
  • v:参数以 char*[](argv 数组)传入。
  • p:自动按 PATH 环境变量搜索可执行文件,所以不用写 /bin/ls,写 ls 就行。
  • e:用自定义环境 genv 作为新程序的环境表(不是默认的 environ)。

关键:

exec 成功不返回------新程序跑起来后,"子进程"这个身份就变成 ls 进程了,perror 和 exit(127) 那两行永远不会执行;

只有 exec 失败(比如命令不存在)才返回 -1,打印错误并 exit(127)。

127 是 shell 约定:"命令找不到"。

③ waitpid:父进程收尸

  • waitpid(id, &status, 0) 阻塞等待指定子进程结束,回收其资源(僵尸进程回收)。status 是一个 int,退出信息被打包在特定位里,必须用宏拆开:
    • WIFEXITED(status):是否正常退出(调用了 exit / main 返回)→ WEXITSTATUS 取低 8 位退出码。
    • WIFSIGNALED(status):是否被信号杀死(如 kill、Ctrl+C 的 SIGINT)→ 128 + WTERMSIG(status),这是 shell 惯例:被 9 号信号杀就是 137。
  • 拆出的结果存进全局 lastcode,供 echo $? 读取。

七、内建命令 CheckAndExecBuiltCommand

逐个看:

exit:

直接 exit(0) 终止整个进程。

为什么 shell 的 exit 必须是内建?

因为如果是外部程序,exit 只能结束那个子进程,shell 自己还活着。

cd:

cpp 复制代码
if (gargc == 2) {
    if (chdir(gargv[1]) != 0) perror("cd");
} else {
    chdir(getenv("HOME") ? getenv("HOME") : "/");
}
lastcode = 0;

chdir 是系统调用,只改变当前进程(shell)自己的工作目录------这也是必须内建的原因:外部子进程改了目录跟 shell 无关。

注意 lastcode = 0 无条件执行,chdir 失败也是 0(缺陷)。

export:

AddEnv(gargv[1]),把 KEY=VALUE 追加进 genv 表。

下一轮 fork 出来的子进程就会带上这个变量------这就是环境变量"继承"的实现。

env:遍历打印 genv。

echo:

复制代码
if (gargc == 2) {
    if (gargv[1][0] == '$' && gargv[1][1] == '?')
        printf("%d\n", lastcode);
    else
        printf("%s\n", gargv[1]);
} else {
    printf("\n");
}
  • echo $? 就是打印 lastcode。
  • 只支持单个参数:echo a b c 会走 else 只打印换行;echo $?x 因为第二个字符是 ? 也会被误判成 $?(缺陷)。

八、main 主循环 + 已知缺陷清单

cpp 复制代码
int main() {
    InitEnv();              // 启动时先拷贝环境
    char cmd[basesize];
    while (true) {
        PrintCommandLine();
        if (!GetCommandLine(cmd, basesize)) continue;
        ParseCommandLine(cmd);
        if (CheckAndExecBuiltCommand()) continue;
        ExecuteCommand();
    }
}

逻辑链:提示符 → 读 → 解析 → 内建?→ 是:continue 回到顶部;否:fork 执行外部命令 → 回到顶部。

这个教学版代码里隐藏着几个真实缺陷,值得你知道(面试常被问):

# 缺陷 原因 后果
1 Ctrl+D 死循环 EOF 时 GetCommandLine 返回 false,main 里却 continue 回到循环顶部,下一次 fgets 又 EOF 刷屏死循环,Ctrl+D 根本退不出去(应改为 break 或区分 EOF/空行)
2 纯空格输入段错误 strtok 返回 NULL,gargv[0]=NULL,随后 strcmp(NULL,...) 直接崩
3 PWD 不同步 GetPwd 用 putenv 更新的是真实 environ,而传给子进程的是 genv(启动时拷贝的旧值) cd 之后外部命令看到的 $PWD 仍是 shell 启动时的值
4 argv 溢出 ParseCommandLine 不检查 gargc < argvnum 超 64 个参数越界写
5 export 越界 AddEnv 不检查 genv 容量 export 超 64 次越界写
6 echo a b c 只输出空行 echo 只处理 gargc==2 多参数被吞
7 cd 失败也 lastcode=0 lastcode = 0 无条件执行 $? 不反映真实失败

九、编译注意

  • 编译命令:g++ -o mini_shell mini_shell.cpp -std=c++11
  • execvpe 是 GNU 扩展,声明在 unistd.h 的 #ifdef __USE_GNU 块里。如果编译报 execvpe was not declared,需要加宏:g++ -o mini_shell mini_shell.cpp -std=c++11 -D_GNU_SOURCE
  • 它依赖 Linux/Unix 环境(fork/exec/waitpid 都是 POSIX 系统调用),Windows 上无法直接编译运行。

十、函数复习

cpp 复制代码
C 语言 `<string.h>` 中以 `str` 开头的函数(C 风格字符串):

表格

| 函数 | 作用 |
| --- | --- |
| `strlen(s)`             | 求长度(不含 `\0`) |
| `strcpy(dest, src)`     | 复制字符串(不检查容量,危险) |
| `strncpy(dest, src, n)` | 复制最多 n 个字符 |
| `strcat(dest, src)`     | 拼接(不检查容量,危险) |
| `strncat(dest, src, n)` | 拼接最多 n 个字符 |
| `strcmp(s1, s2)`        | 比较(<0、0、>0) |
| `strncmp(s1, s2, n)`    | 比较前 n 个字符 |
| `strchr(s, c)`          | 正向找字符,返回指针,找不到返回 `NULL` |
| `strrchr(s, c)`         | 反向找字符 |
| `strstr(s, sub)`        | 找子串,返回子串首地址指针 |
| `strtok(s, delim)`      | 按分隔符切分字符串 |
| `strdup(s)`             | 复制一份新字符串(需 `free`) |
| `strspn(s, accept)`     | 返回 s 开头连续属于 accept 的字符数 |
| `strcspn(s, reject)`    | 返回 s 开头连续不属于 reject 的字符数 |
| `strerror(errno)`       | 把错误码转成错误信息字符串 |

常用示例:

```
#include <string.h>

char s1[] = "hello";
char s2[20];

strcpy(s2, s1);                    // s2 = "hello"
strcat(s2, " world");              // s2 = "hello world"

if (strcmp(s1, "hello") == 0) {    // 相等
}

char *p = strstr(s2, "wor");       // 指向 "world" 开头
char *c = strchr(s2, 'o');         // 指向第一个 'o'

// strtok 切分
char s[] = "apple,banana,cherry";
char *tok = strtok(s, ",");
while (tok) {
    printf("%s\n", tok);           // apple / banana / cherry
    tok = strtok(NULL, ",");
}
```
函数 输出目标 安全版本 说明
printf 标准输出 --- 最常用
fprintf 指定文件流 --- fprintf(stderr, ...) 打错误
sprintf 字符缓冲区 sprintf_s 不检查容量,会溢出,慎用
snprintf 字符缓冲区(限长) snprintf_s 推荐,安全
asprintf 自动分配缓冲区 --- GNU 扩展,自动 malloc,需 free
dprintf 文件描述符 fd --- POSIX,直接写 fd
sprintf_s 字符缓冲区 --- MSVC 安全版,验证参数
snprintf_s 字符缓冲区(限长) --- MSVC 安全版
vprintf 标准输出 --- 接受 va_list
vfprintf 文件流 --- 接受 va_list
vsprintf 缓冲区 vsprintf_s 不检查容量
vsnprintf 缓冲区(限长) vsnprintf_s 接受 va_list 的 snprintf

版本一:C语言

cpp 复制代码
#include "myshell.h"

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

// 提示符(真的是字符串)相关
static char username[32];
static char hostname[64];
static char cwd[256];
static char commandline[256];

// 命令行相关
static char *argv[64];  // 一般最多也就十几个
static int argc = 0;    
static const char *sep = " ";

// 与退出码有关
static int lastcode;

// 与环境变量有关,应该由bash来维护,从系统配置文件读,直接从系统bash拷贝即可
static char *env[64];

static void GetUserName()
{
    char *_username = getenv("USER");   // 获取环境变量
    strcpy(username,(_username ? _username : "None"));
}

static void GetHostName()
{
    char *_hostname = getenv("HOSTNAME");   // 获取环境变量
    strcpy(hostname,(_hostname ? _hostname : "None"));
}

//static void GetCwdName()    // 要详细处理,包括只保留"/"之后的路径
//{
//    char *_cwd = getenv("PWD");   // 获取环境变量
//    strcpy(cwd,(_cwd ? _cwd : "None"));
//}


static void GetCwdName()    // 要详细处理,包括只保留"/"之后的路径
{
    char _cwd[256];
    getcwd(_cwd,sizeof(_cwd));
    if(strcmp(_cwd,"/") == 0)
    {
        strcpy(cwd,_cwd);
    }
    else
    {
        int end = strlen(_cwd) - 1;
        while(end >= 0)
        {
            if(_cwd[end] == '/')
            {
                // 如 /home/Alice
                strcpy(cwd,&_cwd[end + 1]);
                break;
            }
            end--;
        }
    }
}

void PrintPrompt()
{
    GetUserName();
    GetHostName();
    GetCwdName();
    printf("[%s@%s %s]# ",username,hostname,cwd);
    fflush(stdout); // 直接刷新缓冲区
}

static void GetCommandLine()
{
    if(fgets(commandline,sizeof(commandline),stdin) != NULL)
    {
        // 比如"abcd\n" -> "abcd"
        commandline[strlen(commandline) - 1] = 0;
        // printf("debug:%s\n",commandline);
    }
}

static void ParseCommandLine()
{
    // 清空
    argc = 0;
    memset(argv,0,sizeof(argv));
    // 判空
    if(strlen(commandline) == 0)
        return;
    // 解析 ls -a -l
    argv[argc] = strtok(commandline,sep);   // strtok分割字符串,变成几个子串
    while((argv[++argc] = strtok(NULL,sep)));   // argc的大小即子串个数

// // 调试信息注释掉
//    printf("argc:%d\n",argc);
//    int i = 0;
//    for(;argv[i];i++)
//    {
//        printf("argv[%d]:%s\n",i,argv[i]);
//    }
}

// 创建子进程
void Execute()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return;
    }
    else if(id == 0)
    {
        // printf("child running..\n");
        // 子进程
        // 程序替换
        execvp(argv[0],argv);
        exit(1);
    }
    else{
        // 父进程
        int status = 0;
        pid_t rid = waitpid(id,&status,0);  // 得到状态
        (void)rid;
        lastcode = WEXITSTATUS(status);
    }
}

// 1:yes
// 0:no
int CheckBuiltinAndExcute()
{
    int ret = 0;
    if(strcmp(argv[0],"cd") == 0)
    {
        // 内建命令
        ret = 1;
        if(argc == 2)   // 有选项
        {
            chdir(argv[1]);
        }
    }
    else if(strcmp(argv[0],"echo") == 0)
    {
        ret = 1;
        if(argc == 2)
        {
            // echo $?
            // echo $PATH
            // echo "helloworld"
            // echo helloworld
            if(argv[1][0] == '$')
            {
                if(strcmp(argv[1],"$?") == 0)
                {
                    printf("%d\n",lastcode);
                    lastcode = 0;   // 返回上一次的退出码
                }
                else
                {
                    // env:环境变量
                }
            }
            else
            {
                printf("%s\n",argv[1]); // 打印字符串,不细分什么""或者没有""了
            }
        }
    }

    return ret;
}

void Bash()
{
    while(1)    // 软件都是死循环的
    {
        // 第1步:输出命令行
        PrintPrompt();

        // 第2步:等待用户输入,获取用户输入
        // char commandline[256]; -> scanf()
        // sleep(1);   // 等待一会儿
        GetCommandLine();

        // 第3步:解析字符串,"ls -a -l" -> "ls" "-a" "-l"
        ParseCommandLine();
        if(argc == 0)
            continue;

        // 第4步:有些命令,cd、ench、env、export等命令,不应该让子进程执行,而应该让父进程自己执行,内建命令。bash内部的函数
        if(CheckBuiltinAndExcute())
        {
            continue;
        }

        // 第5步:执行命令
        Execute();
    }
}

版本二:C++

cpp 复制代码
/******************************************************************************
 * mini_shell.cpp
 * 一个最小可用的 Linux Shell:
 *   - 支持普通外部命令(ls / ps / whoami / pwd ...)
 *   - 支持内建命令:cd / export / env / echo $? / exit
 *   - 支持环境变量继承与维护
 *   - 支持 echo $? 打印上一条命令退出码
 * 编译:g++ -o mini_shell mini_shell.cpp -std=c++11
 * 运行:./mini_shell
 *****************************************************************************/
 
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctype.h>
using namespace std;
 
/* ====================== 全局配置 ====================== */
const int basesize = 1024;   // 命令行缓冲区大小
const int argvnum  = 64;     // argv 最多参数个数
const int envnum   = 64;     // 环境变量最多个数
 
/* ====================== 全局状态 ====================== */
static char *gargv[argvnum]; // 命令行参数表,末尾必须为 NULL
static int   gargc = 0;      // 参数个数
 
static int lastcode = 0;     // 上一条命令的退出码(echo $? 用)
 
static char *genv[envnum];   // shell 自己维护的环境变量表
 
static char pwd[basesize];   // 当前工作目录
static char pwdenv[basesize];// "PWD=xxx" 字符串
 
/* ====================== 提示符相关 ====================== */
static string GetUserName() {
    const char *name = getenv("USER");
    return name ? name : "None";
}
 
static string GetHostName() {
    const char *host = getenv("HOSTNAME");
    return host ? host : "None";
}
 
static string GetPwd() {
    if (getcwd(pwd, sizeof(pwd)) == NULL) return "None";
    snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);
    putenv(pwdenv);
    return pwd;
}
 
static string LastDir() {
    string curr = GetPwd();
    if (curr == "/" || curr == "None") return curr;
    size_t pos = curr.rfind("/");
    if (pos == string::npos) return curr;
    return curr.substr(pos + 1);
}
 
static string MakeCommandLine() {
    char line[basesize];
    snprintf(line, basesize, "[%s@%s %s]# ",
             GetUserName().c_str(),
             GetHostName().c_str(),
             LastDir().c_str());
    return line;
}
 
static void PrintCommandLine() {
    printf("%s", MakeCommandLine().c_str());
    fflush(stdout);     // 必须 fflush,因为提示符没 \n
}
 
/* ====================== 读命令行 ====================== */
static bool GetCommandLine(char buf[], int size) {
    if (fgets(buf, size, stdin) == NULL) {
        printf("\n");          // Ctrl+D 退出
        return false;
    }
    buf[strlen(buf) - 1] = '\0';   // 去掉 fgets 读进来的 \n
    if (strlen(buf) == 0) return false;
    return true;
}
 
/* ====================== 解析命令行 ====================== */
static void ParseCommandLine(char buf[]) {
    memset(gargv, 0, sizeof(gargv));
    gargc = 0;
    const char *sep = " ";
    gargv[gargc++] = strtok(buf, sep);
    while ((gargv[gargc++] = strtok(NULL, sep)) != NULL)
        ;
    gargc--;
}
 
/* ====================== 环境变量操作 ====================== */
static void InitEnv() {
    extern char **environ;
    int i = 0;
    for (; environ[i] && i < envnum - 1; i++) {
        genv[i] = (char *)malloc(strlen(environ[i]) + 1);
        strcpy(genv[i], environ[i]);
    }
    genv[i] = NULL;
}
 
static void AddEnv(const char *item) {
    int i = 0;
    while (genv[i]) i++;
    genv[i] = (char *)malloc(strlen(item) + 1);
    strcpy(genv[i], item);
    genv[++i] = NULL;
}
 
/* ====================== 外部命令执行 ====================== */
static void ExecuteCommand() {
    pid_t id = fork();
    if (id < 0) { perror("fork"); lastcode = 1; return; }
 
    if (id == 0) {
        // 子进程:替换成外部程序
        execvpe(gargv[0], gargv, genv);
        perror(gargv[0]);       // exec 失败才会走到这
        exit(127);
    }
 
    // 父进程:阻塞等子进程
    int status = 0;
    waitpid(id, &status, 0);
 
    if (WIFEXITED(status))
        lastcode = WEXITSTATUS(status);
    else if (WIFSIGNALED(status))
        lastcode = 128 + WTERMSIG(status);   // 被信号杀
    else
        lastcode = 100;
}
 
/* ====================== 内建命令 ====================== */
static bool CheckAndExecBuiltCommand() {
    /* exit:退出 shell */
    if (strcmp(gargv[0], "exit") == 0) { printf("bye~\n"); exit(0); }
 
    /* cd:改 shell 自己的工作目录 */
    if (strcmp(gargv[0], "cd") == 0) {
        if (gargc == 2) {
            if (chdir(gargv[1]) != 0) perror("cd");
        } else {
            chdir(getenv("HOME") ? getenv("HOME") : "/");
        }
        lastcode = 0;
        return true;
    }
 
    /* export KEY=VALUE */
    if (strcmp(gargv[0], "export") == 0) {
        if (gargc == 2) { AddEnv(gargv[1]); lastcode = 0; }
        else lastcode = 1;
        return true;
    }
 
    /* env:列环境变量 */
    if (strcmp(gargv[0], "env") == 0) {
        for (int i = 0; genv[i]; i++) printf("%s\n", genv[i]);
        lastcode = 0;
        return true;
    }
 
    /* echo:支持 echo $? / echo xxx */
    if (strcmp(gargv[0], "echo") == 0) {
        if (gargc == 2) {
            if (gargv[1][0] == '$' && gargv[1][1] == '?')
                printf("%d\n", lastcode);
            else
                printf("%s\n", gargv[1]);
        } else {
            printf("\n");
        }
        lastcode = 0;
        return true;
    }
 
    return false;   // 不是内建,去 fork
}
 
/* ====================== 主循环 ====================== */
int main() {
    InitEnv();
    char cmd[basesize];
 
    while (true) {
        PrintCommandLine();
        if (!GetCommandLine(cmd, basesize)) continue;
        ParseCommandLine(cmd);
        if (CheckAndExecBuiltCommand()) continue;
        ExecuteCommand();
    }
    return 0;
}

WIFEXITED 是 <sys/wait.h> 中的宏,判断子进程是否正常退出(通过 exit() 或 return),配合 wait() / waitpid() 使用:

cpp 复制代码
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>

pid_t pid = fork();
if (pid == 0) {
    exit(42);                    // 子进程正常退出
}

int status;
waitpid(pid, &status, 0);

if (WIFEXITED(status)) {
    printf("正常退出,退出码 = %d\n", WEXITSTATUS(status));  // 42
}

wait 状态宏全家桶:

宏 作用
WIFEXITED(status) 是否正常退出(exit/return)
WEXITSTATUS(status) 正常退出时的退出码(仅 WIFEXITED 为真时用)
WIFSIGNALED(status) 是否被信号杀死
WTERMSIG(status) 杀死它的信号编号(仅 WIFSIGNALED 为真时用)
WCOREDUMP(status) 是否产生核心转储(非 POSIX 标准,部分系统有)
WIFSTOPPED(status) 是否因信号暂停(配合 WUNTRACED)
WSTOPSIG(status) 使其暂停的信号编号
WIFCONTINUED(status) 是否被 SIGCONT 恢复(配合 WCONTINUED)

完整判断模板:

cpp 复制代码
if (WIFEXITED(status)) {
    printf("正常退出,码 %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
    printf("被信号 %d 杀死\n", WTERMSIG(status));
} else if (WIFSTOPPED(status)) {
    printf("被信号 %d 暂停\n", WSTOPSIG(status));
}

核心点:

status 是打包的位字段,不能直接比较,必须用宏解包。

判断 "是否正常退出" 看 WIFEXITED,取退出码看 WEXITSTATUS,两者通常成对出现。


相关推荐
Qt云程序员3 小时前
零代码网页数据采集教程:不写爬虫,把网页数据搬进 Excel
运维·自动化
十年Java程序媛3 小时前
Java 多线程基础与锁机制详解|进程线程、锁升级、synchronized 与 Lock 对比
java·开发语言·spring boot
弯_弯3 小时前
FDE 的工程能力画像:数据接入、PoC 与可复用资产的落地口径
java·服务器·开发语言
鱼很腾apoc3 小时前
【MySQL】第3期 索引、事务与视图系统详解
linux·mysql·事务·索引·dml·视图·macc
雾沉川3 小时前
Visual C++运行库合集 v105.0 安装教程
开发语言·c++·dll
anew___3 小时前
《从零手写操作系统 (49):虚拟内存进阶——Demand Paging、Swap与缺页异常处理》
linux·服务器·php
迷途之人不知返3 小时前
【基础IO】-4-再谈重定向
linux
Android系统攻城狮4 小时前
Linux Gstreamer深度解析之gst_audio_ring_buffer_parse_caps调用流程与实战(八十)
linux·运维·服务器·gstreamer音视频·音视频进阶
l1t4 小时前
DeepSeek总结的 DuckDB 2.0 文档新内容
服务器·网络·数据库