【Linux】进程控制(十二) 自定义 Shell(二):内建命令与进程工作目录管理

从 cd 命令出发,理解 Shell 为什么需要管理自己的进程状态。

一、从 cd 出发:为什么有些命令不能交给子进程?

1.1 回顾 Shell 的外部命令执行流程

在上一篇中,我们已经实现了 Shell 最基本的功能:接收用户输入、解析命令,并通过创建子进程执行外部程序。

例如,在终端中输入:

bash 复制代码
ls -l

Shell 会经过以下几个步骤:

  1. 获取用户输入的命令字符串。
  2. 通过 strtok() 将命令拆分为程序名称和参数。
  3. 调用 fork() 创建子进程。
  4. 子进程调用 execvp()(当前版本使用 execvpe())执行对应程序。
  5. 父进程调用 waitpid() 等待子进程结束。

这套机制能够处理绝大多数普通命令。

但是,如果我们尝试使用同样的方式执行 cd,就会遇到一个问题。

1.2 如果 cd 也通过 fork 创建子进程,会发生什么?

先来看一个简单的实验:

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

using namespace std;

int main()
{
    pid_t id = fork();

    if (id == 0)
    {
        chdir("/tmp");
        cout << "child: ";
        system("pwd");
    }
    else
    {
        waitpid(id, nullptr, 0);

        cout << "parent: ";
        system("pwd");
    }

    return 0;
}

假设程序原本运行在 /home/user。

子进程调用 chdir("/tmp") 后,自己的工作目录发生了改变。

但是父进程再次查看工作目录时,仍然是原来的 /home/user。

为什么?

问题并不在于 chdir() 没有生效,而在于:父进程和子进程拥有各自独立的工作目录状态。

fork() 创建子进程时,子进程会继承父进程的工作目录,但后续对子进程工作目录的修改,不会反过来改变父进程。

因此,如果 Shell 将 cd 交给子进程执行,即使子进程成功进入了目标目录,子进程结束后,Shell 自己仍然停留在原来的目录。

这显然不是我们想要的结果。

1.3 父子进程的工作目录关系

这里需要联系之前学过的 fork()。

fork() 创建子进程后,父子进程分别拥有自己的进程上下文。工作目录属于进程文件系统上下文的一部分。

从 Linux 内核实现角度看,父子进程在创建时会继承相同的工作目录引用,但它们的文件系统上下文是各自独立的。

可以理解为:

  • 创建子进程时,子进程继承父进程当前的工作目录。
  • 父子进程最初指向同一个目录位置。
  • 子进程调用 chdir(),修改的是子进程自己的工作目录状态。
  • 父进程的工作目录不会因此发生改变。

示意图:

text 复制代码
              Shell 父进程
                   |
              fork()
                   |
          +--------+--------+
          |                 |
          v                 v
       父进程             子进程
       cwd                cwd
      /home              /home
          |                 |
          |              chdir("/tmp")
          |                 |
          v                 v
       /home              /tmp

    父进程 cwd 不受子进程修改的影响

这里要注意:工作目录不是简单地保存一个字符串。内核通过目录相关的文件系统对象来管理进程的当前工作目录。

所以,父子进程的工作目录虽然最初相同,但后续可以独立改变。

1.4 从 cd 引出 Shell 内建命令

既然 cd 不能交给子进程执行,那么应该怎么办?

答案就是:让 Shell 自己执行。

Shell 本身就是一个正在运行的进程,它可以直接调用 chdir() 修改自己的工作目录。

这类由 Shell 自身完成,而不是通过创建外部程序执行的命令,就叫作 Shell 内建命令(Built-in Command)。

例如:

  • cd:改变 Shell 自身的工作目录。
  • export:修改 Shell 自身维护的环境变量。
  • echo:可以由 Shell 自己实现。
  • exit:退出当前 Shell。

这些命令有一个共同特点:它们需要直接操作 Shell 自己的状态,或者需要 Shell 对命令进行特殊解释。

因此,Shell 不能对所有命令都采用统一的 fork() + exec() 执行方式。

这也引出了我们这次代码修改的第一个重点:命令


二、内建命令的实现:CheckAndExecBuiltCommand()

2.1 内建命令与外部命令的区别

我们先区分两种命令的执行方式。

外部命令:

例如:

bash 复制代码
ls -l

Shell 找到对应的可执行程序,通过 fork() 创建子进程,再通过 exec() 系列函数执行程序。

内建命令:

例如:

bash 复制代码
cd /tmp

Shell 不需要创建新的进程,而是直接调用自身的函数完成操作。

两者最大的区别不是命令写法,而是命令由谁执行。

对比 外部命令 内建命令
执行主体 子进程中的外部程序 Shell 自身
是否需要 fork 通常需要 不需要
是否需要 exec 通常需要 不需要
是否能直接修改 Shell 自身状态 不能通过子进程直接修改 可以

2.2 main() 中的命令分流

来看这次修改后的 main():

cpp 复制代码
int main()
{
    InitEnv();

    char command_buffer[basesize];

    while(true)
    {
        PrintCommandLine();

        if(!GetCommandLine(command_buffer, basesize))
        {
            continue;
        }

        ParseCommandLine(
            command_buffer,
            strlen(command_buffer)
        );

        if(CheckAndExecBuiltCommand())
        {
            continue;
        }

        ExecuteCommand();
    }

    return 0;
}

和上一篇相比,最重要的变化是增加了:

cpp 复制代码
if(CheckAndExecBuiltCommand())
{
    continue;
}

这段代码位于 ExecuteCommand() 之前。

也就是说,每次用户输入命令并完成解析后,Shell 都会先检查这条命令是不是内建命令。

如果是,直接执行并通过 continue 回到主循环。

如果不是,才继续执行:

cpp 复制代码
ExecuteCommand();

这就形成了两条执行路径:

text 复制代码
                用户输入命令
                      |
                      v
               ParseCommandLine()
                      |
                      v
          CheckAndExecBuiltCommand()
                      |
                判断命令类型
                /           \
               /             \
          内建命令           外部命令
             |                 |
             v                 v
        Shell 自己执行       ExecuteCommand()
             |                 |
             |               fork()
             |                 |
             |          +------+------+
             |          |             |
             |        父进程         子进程
             |          |             |
             |       waitpid()     execvpe()
             |          |             |
             +----------+-------------+
                        |
                        v
                   下一轮循环

2.3 CheckAndExecBuiltCommand() 的实现

来看代码:

cpp 复制代码
bool CheckAndExecBuiltCommand()
{
    if(strcmp(gargv[0], "cd") == 0)
    {
        // cd 的处理
    }
    else if(strcmp(gargv[0], "export") == 0)
    {
        // export 的处理
    }
    else if(strcmp(gargv[0], "env") == 0)
    {
        // env 的处理
    }
    else if(strcmp(gargv[0], "echo") == 0)
    {
        // echo 的处理
    }

    return false;
}

这里通过 strcmp() 判断用户输入的命令名称。

例如:

cpp 复制代码
strcmp(gargv[0], "cd") == 0

如果成立,就说明用户输入的命令名称是 cd。

为什么函数返回的是 bool?

因为 main() 需要根据返回值判断是否还要执行外部命令。

  • 返回 true:已经作为内建命令处理,不需要再调用 ExecuteCommand()。
  • 返回 false:不是当前 Shell 支持的内建命令,继续按照外部命令执行。

这里的返回值表达的是命令是否已经被内建命令处理逻辑接管,并不直接代表命令执行成功与否。

例如,用户输入:

bash 复制代码
cd

由于当前代码要求 gargc == 2,所以会设置:

cpp 复制代码
lastcode = 1;

但函数仍然返回 true。

这说明命令已经被识别为内建命令,只是参数不符合当前实现的要求。

2.4 为什么必须在 fork() 之前处理 cd?

现在回头看整个过程:

cpp 复制代码
if(CheckAndExecBuiltCommand())
{
    continue;
}

ExecuteCommand();

假设输入的是 cd /tmp:

  1. CheckAndExecBuiltCommand() 识别到 cd。
  2. Shell 自己调用 chdir("/tmp")。
  3. 函数返回 true。
  4. main() 执行 continue。
  5. 直接进入下一轮主循环。

整个过程没有调用 fork()。

这正是内建命令设计的意义。

Shell 不只是负责启动其他程序,它本身也是一个需要维护运行状态的进程。

当命令需要改变 Shell 自身的状态时,就必须由 Shell 自己完成。


三、进程工作目录:cwd 的管理机制

3.1 当前工作目录是什么?

在 Linux 中,每个进程都有自己的当前工作目录(Current Working Directory,简称 cwd)。

当我们执行:

bash 复制代码
ls

或者:

bash 复制代码
cat file.txt

如果使用的是相对路径,系统就需要根据当前工作目录确定文件的位置。

例如:

bash 复制代码
cd /home/user
cat test.txt

这里的 test.txt 是相对路径。

系统会以当前工作目录 /home/user 为起点,解析这个文件路径。

如果当前工作目录改变了:

bash 复制代码
cd /tmp

那么再次执行:

bash 复制代码
cat test.txt

系统查找的就变成了 /tmp/test.txt。

因此,工作目录实际上影响着进程对相对路径的解析。

3.2 工作目录由谁管理?

工作目录并不是由 Shell 自己定义的一个普通字符串来决定的,而是由 Linux 内核为进程维护的文件系统上下文管理。

在 Linux 内核中,进程的文件系统上下文由 fs_struct 等内核结构管理,其中包含当前工作目录和根目录等信息。

可以简单理解为:

text 复制代码
进程
 |
 v
文件系统上下文
 |
 +---- 当前工作目录 cwd
 |
 +---- 根目录 root

这里的 cwd 表示进程当前所处的目录位置。

它不是环境变量,也不只是一个路径字符串。

这也解释了为什么修改一个环境变量,并不能直接改变进程的实际工作目录。

3.3 fork() 后父子进程如何继承工作目录?

前面已经提到,fork() 创建子进程时,子进程会继承父进程的工作目录。

例如:

cpp 复制代码
pid_t id = fork();

if(id == 0)
{
    chdir("/tmp");
}

执行前:

text 复制代码
父进程 cwd = /home/user

执行 fork() 后:

text 复制代码
父进程 cwd = /home/user
子进程 cwd = /home/user

子进程执行 chdir("/tmp") 后:

text 复制代码
父进程 cwd = /home/user
子进程 cwd = /tmp

这里要区分两个概念:

  • 最初继承相同的工作目录位置。
  • 后续分别维护各自的工作目录状态。

因此,子进程修改 cwd 不会改变父进程的 cwd。

3.4 父子进程分别 chdir() 会发生什么?

我们可以通过一个简单的程序验证:

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

using namespace std;

int main()
{
    char path[1024];

    pid_t id = fork();

    if(id == 0)
    {
        chdir("/tmp");

        getcwd(path, sizeof(path));
        cout << "child cwd: " << path << endl;
    }
    else
    {
        waitpid(id, nullptr, 0);

        getcwd(path, sizeof(path));
        cout << "parent cwd: " << path << endl;
    }

    return 0;
}

假设程序最初在 /home/user 下运行,输出的核心结果是:

text 复制代码
child cwd: /tmp
parent cwd: /home/user

通过这个实验,我们可以直接观察到:

子进程改变自己的工作目录后,父进程的工作目录保持不变。

这就是 cd 必须由 Shell 自己执行的根本原因。


四、cd 的实现:chdir() 与 getcwd()

理解了进程工作目录之后,我们就可以回到代码,看看 Shell 是如何获取和修改工作目录的。

4.1 chdir():改变当前工作目录

chdir() 是 Linux 提供的系统调用接口,用于改变调用进程的当前工作目录。

函数原型:

cpp 复制代码
#include <unistd.h>

int chdir(const char *path);

参数:

  • path:目标目录的路径,可以是绝对路径,也可以是相对路径。

返回值:

  • 成功:返回 0。
  • 失败:返回 -1,并设置 errno。

例如:

cpp 复制代码
chdir("/tmp");

调用成功后,当前进程的工作目录就变成了 /tmp。

注意:chdir() 只改变调用它的进程的工作目录。

如果在子进程中调用,它改变的是子进程的工作目录;如果在 Shell 父进程中调用,它改变的就是 Shell 自己的工作目录。

4.2 getcwd():获取当前工作目录

既然 chdir() 可以改变工作目录,那么我们如何知道进程现在处于哪个目录?

这就需要 getcwd()。

函数原型:

cpp 复制代码
#include <unistd.h>

char *getcwd(char *buf, size_t size);

参数:

  • buf:用于保存当前工作目录路径的缓冲区。
  • size:缓冲区的大小。

返回值:

  • 成功:返回 buf。
  • 失败:返回 NULL。

例如:

cpp 复制代码
char path[1024];

if(getcwd(path, sizeof(path)) != nullptr)
{
    cout << path << endl;
}

如果当前工作目录是 /home/user,那么 getcwd() 就会把对应路径写入 path。

这里有一个值得注意的地方:

getcwd() 获取的是当前进程实际的工作目录路径,而不是简单地读取环境变量 PWD。

这两个概念虽然通常指向同一个目录,但它们的来源并不相同。

4.3 CommandCd 的代码实现

老师的代码将 cd 的处理直接写在了 CheckAndExecBuiltCommand() 中:

cpp 复制代码
if(strcmp(gargv[0], "cd") == 0)
{
    if(gargc == 2)
    {
        chdir(gargv[1]);
        lastcode = 0;
    }
    else
    {
        lastcode = 1;
    }

    return true;
}

我们逐步分析。

首先:

cpp 复制代码
strcmp(gargv[0], "cd") == 0

判断当前命令是不是 cd。

接着:

cpp 复制代码
if(gargc == 2)

判断参数数量是否为两个。

例如:

bash 复制代码
cd /tmp

经过命令解析后:

text 复制代码
gargc = 2

gargv[0] = "cd"
gargv[1] = "/tmp"

因此条件成立。

随后:

cpp 复制代码
chdir(gargv[1]);

调用 chdir() 改变 Shell 自己的工作目录。

这里不需要额外创建子进程,因为当前执行代码的就是 Shell 父进程。

最后:

cpp 复制代码
lastcode = 0;

记录当前命令的状态。

不过,这里有一个课堂代码中的问题:

老师没有检查 chdir() 的返回值。

例如:

bash 复制代码
cd /not_exist

如果目录不存在,chdir() 会返回 -1,但是当前代码仍然执行:

cpp 复制代码
lastcode = 0;

这就会错误地把失败的命令记录为成功。

更严谨的写法应该是:

cpp 复制代码
if(chdir(gargv[1]) == 0)
{
    lastcode = 0;
}
else
{
    perror("cd");
    lastcode = 1;
}

这里我们保留老师原始代码作为课堂实现,再通过改进版本认识实际开发中需要考虑的错误处理。


五、cwd 与 PWD:实际工作目录和环境变量

前面我们已经认识了 chdir() 和 getcwd()。

但代码中还出现了一个非常重要的变量:

cpp 复制代码
PWD

它又是什么?

5.1 cwd 与 PWD 的区别

先看两个概念。

cwd(Current Working Directory)

表示进程当前实际的工作目录,由操作系统为进程维护。

PWD(Present Working Directory)

是一个环境变量,通常用来记录当前工作目录的路径。

例如:

bash 复制代码
PWD=/home/user

它本质上只是一个字符串。

两者的区别:

对比 cwd PWD
本质 进程的工作目录状态 环境变量
管理者 Linux 内核 用户态环境变量表
表现形式 内核管理的目录引用 路径字符串
修改方式 chdir() setenv()、putenv() 等
获取方式 getcwd() getenv("PWD")

这里最重要的一句话是:

cwd 决定进程实际从哪个目录开始解析相对路径,PWD 只是对当前目录的一种环境变量记录。

所以,不能认为修改 PWD 就能改变 cwd。

例如:

cpp 复制代码
setenv("PWD", "/tmp", 1);

这只是修改了环境变量。

如果实际工作目录还是 /home/user,那么执行:

cpp 复制代码
getcwd(path, sizeof(path));

得到的仍然是 /home/user。

这说明环境变量中的路径和内核维护的工作目录状态是两个不同层面的东西。

5.2 getenv("PWD") 与 getcwd() 的区别

我们可以通过两个接口分别获取它们。

获取环境变量:

cpp 复制代码
char *pwd = getenv("PWD");

获取实际工作目录:

cpp 复制代码
char path[1024];

getcwd(path, sizeof(path));

如果两者正常同步,结果通常相同:

text 复制代码
getenv("PWD") = /home/user

getcwd()      = /home/user

但如果有人只修改了环境变量:

cpp 复制代码
setenv("PWD", "/tmp", 1);

就可能出现:

text 复制代码
getenv("PWD") = /tmp

getcwd()      = /home/user

所以,如果 Shell 想要准确获取当前实际工作目录,应该使用 getcwd()。

这也是老师在 GetPwd() 中选择 getcwd() 的原因。

5.3 为什么改变 cwd 后还要更新 PWD?

假设 Shell 最初运行在:

text 复制代码
/home/user

此时:

text 复制代码
cwd = /home/user
PWD = /home/user

用户执行:

bash 复制代码
cd /tmp

Shell 调用:

cpp 复制代码
chdir("/tmp");

此时:

text 复制代码
cwd = /tmp
PWD = /home/user

注意,chdir() 只改变实际工作目录,并不会自动保证环境变量 PWD 同步更新。

所以 Shell 还需要更新 PWD:

text 复制代码
cwd = /tmp
PWD = /tmp

为什么要这样做?

因为环境变量可能被后续启动的程序读取。

例如:

bash 复制代码
echo $PWD

如果 PWD 没有更新,就可能得到旧路径。

因此,Shell 在改变工作目录后,还需要维护与之对应的 PWD 环境变量。


六、结合代码:Shell 如何更新 PWD?

现在回到老师的 myshell.cc。

这部分是本篇最值得仔细研究的代码之一。

6.1 GetPwd() 的完整实现

cpp 复制代码
char pwd[basesize];
char pwdenv[basesize];

string GetPwd()
{
    if(nullptr == getcwd(pwd, sizeof(pwd)))
        return "None";

    snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);

    putenv(pwdenv);

    return pwd;
}

注意,这里的 GetPwd() 不仅仅是获取工作目录,还承担了更新 PWD 的工作。

我们拆开来看。

6.2 getcwd() 获取实际目录

cpp 复制代码
if(nullptr == getcwd(pwd, sizeof(pwd)))
    return "None";

这里通过 getcwd() 获取当前进程的实际工作目录。

获取成功后,路径会写入全局数组:

cpp 复制代码
char pwd[basesize];

例如:

text 复制代码
pwd = "/tmp"

为什么这里使用全局数组?

因为后面还需要使用这个路径组织环境变量字符串。

6.3 snprintf() 组织环境变量字符串

接着:

cpp 复制代码
snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);

这里要理解环境变量的基本组织形式:

text 复制代码
KEY=VALUE

例如:

text 复制代码
USER=whb
HOME=/home/whb
PATH=/usr/bin:/bin
PWD=/tmp

所以我们不能只把:

text 复制代码
/tmp

传给环境变量接口。

而是需要组织成:

text 复制代码
PWD=/tmp

snprintf() 就是用来完成这个字符串构造的。

它将格式化后的字符串写入 pwdenv。

6.4 putenv() 更新环境变量

最后:

cpp 复制代码
putenv(pwdenv);

putenv() 用于将指定的 KEY=VALUE 字符串加入当前进程的环境变量表,或者替换同名变量。

例如:

cpp 复制代码
putenv("PWD=/tmp");

这里需要注意一个很重要的接口特点:

putenv() 不会简单地复制传入的字符串,而是使用传入字符串所在的存储空间。

因此,传入的字符串必须在环境变量使用期间保持有效。

老师这里使用:

cpp 复制代码
char pwdenv[basesize];

将它定义为全局数组,避免了函数返回后局部数组失效的问题。

这也是为什么这里没有使用一个普通的局部字符数组来组织环境变量。

6.5 GetPwd() 与 PrintCommandLine() 的联系

再回到提示符构建函数:

cpp 复制代码
string LastDir()
{
    string curr = GetPwd();

    if(curr == "/" || curr == "None")
        return curr;

    size_t pos = curr.rfind("/");

    if(pos == std::string::npos)
        return curr;

    return curr.substr(pos + 1);
}

以及:

cpp 复制代码
string MakeCommandLine()
{
    char command_line[basesize];

    snprintf(command_line, basesize,
             "[%s@%s %s]# ",
             GetUserName().c_str(),
             GetHostName().c_str(),
             LastDir().c_str());

    return command_line;
}

每次 Shell 输出提示符时,都会调用 LastDir()。

而 LastDir() 又会调用 GetPwd()。

所以完整的执行关系是:

text 复制代码
main() 主循环
     |
     v
PrintCommandLine()
     |
     v
MakeCommandLine()
     |
     v
LastDir()
     |
     v
GetPwd()
     |
     +---- getcwd() 获取实际工作目录
     |
     +---- snprintf() 构造 PWD=路径
     |
     +---- putenv() 更新环境变量
     |
     v
返回当前目录名称
     |
     v
输出 Shell 提示符

例如,用户执行:

bash 复制代码
cd /tmp

Shell 首先通过 chdir() 修改自己的工作目录。

下一轮主循环开始时,输出提示符的过程又会调用 GetPwd()。

此时 getcwd() 获取到的已经是 /tmp,因此 PWD 也会被更新为 /tmp。

这就是老师这份代码中 cwd 与 PWD 的联系。

注意:这里的 PWD 更新发生在下一轮提示符构建过程中,而不是直接发生在 chdir() 调用之后。


七、其他内建命令:echo 与 $?

除了 cd,还实现了其他几个内建命令。

它们可以帮助我们理解 Shell 不仅需要管理工作目录,还需要维护自己的运行状态。

7.1 echo 为什么也可以作为内建命令?

通常我们使用:

bash 复制代码
echo hello

就能输出:

text 复制代码
hello

实际上,echo 既可以由 Shell 自己实现,也可以通过外部可执行程序实现。

老师在 CheckAndExecBuiltCommand() 中直接处理了 echo:

cpp 复制代码
else if(strcmp(gargv[0], "echo") == 0)
{
    // ...
}

因此,当前这份 Shell 不需要为这个命令创建子进程。

7.2 Shell 如何识别特殊表达式?

先看代码:

cpp 复制代码
if(gargv[1][0] == '$')
{
    if(gargv[1][1] == '?')
    {
        printf("%d\n", lastcode);
        lastcode = 0;
    }
}
else
{
    printf("%s\n", gargv[1]);
    lastcode = 0;
}

在这里让 Shell 对 $? 做了特殊处理。

例如:

bash 复制代码
echo hello

输出:

text 复制代码
hello

而:

bash 复制代码
echo $?

则输出 Shell 中保存的 lastcode。

这说明 Shell 不只是简单地把命令交给程序执行,还可以识别并解释具有特殊意义的表达式。

不过当前实现只处理了 $?,并没有完整实现 $PATH 等环境变量展开功能。

7.3 lastcode 如何保存退出状态?

先回忆上一篇中的外部命令执行函数。

cpp 复制代码
int status = 0;

pid_t rid = waitpid(id, &status, 0);

if(rid > 0)
{
    if(WIFEXITED(status))
    {
        lastcode = WEXITSTATUS(status);
    }
    else
    {
        lastcode = 100;
    }

    return true;
}

这里出现了两个不同的概念:

status:

waitpid() 获取的进程状态信息。

它不是简单的退出码,而是包含了子进程结束方式等信息的状态值。

lastcode:

Shell 自己定义的全局变量,用来保存最近一次命令的状态。

其中:

cpp 复制代码
WIFEXITED(status)

用于判断子进程是否正常退出。

如果正常退出:

cpp 复制代码
WEXITSTATUS(status)

就可以提取子进程的退出码。

随后保存到:

cpp 复制代码
lastcode

所以,整个过程是:

text 复制代码
外部程序执行
      |
      v
进程退出
      |
      v
父进程 waitpid()
      |
      v
获取 status
      |
      v
WIFEXITED()
      |
      v
WEXITSTATUS()
      |
      v
保存到 lastcode

7.4 $? 与 waitpid()、进程退出状态的关系

现在再看:

bash 复制代码
echo $?

就容易理解了。

$? 并不是 Linux 内核直接提供的一个特殊变量,而是 Shell 自己实现的语法和状态查询机制。

它读取 Shell 保存的:

cpp 复制代码
lastcode

例如:

bash 复制代码
ls
echo $?

假设 ls 正常退出,退出码为 0。

那么 Shell 通过 waitpid() 获取状态,提取退出码并保存到 lastcode。

之后执行:

bash 复制代码
echo $?

Shell 直接输出这个值。

这里还有一个细节:

老师的代码在输出 $? 后执行:

cpp 复制代码
lastcode = 0;

因此,这个实现会在读取 $? 后把状态重置为 0。

这属于当前课堂 Shell 的简化设计,不应直接当作所有 Shell 的完整行为。


八、整体回顾:从代码还原 Shell 的工作机制

学习完这一篇,我们不应该只记住 chdir()、getcwd()、putenv() 分别有什么作用。

更重要的是把它们放回整个 Shell 的工作流程中。

8.1 内建命令与外部命令的分流图

text 复制代码
             用户输入命令
                   |
                   v
          ParseCommandLine()
                   |
                   v
       CheckAndExecBuiltCommand()
                   |
             判断命令类型
             /           \
            /             \
       内建命令           外部命令
          |                  |
          v                  v
      Shell 自己执行    ExecuteCommand()
          |                  |
          |                fork()
          |                  |
          |           +------+------+
          |           |             |
          |         父进程         子进程
          |           |             |
          |        waitpid()     execvpe()
          |           |             |
          +-----------+-------------+
                      |
                      v
                  主循环

8.2 父子进程工作目录关系图

text 复制代码
                Shell 父进程
                     |
                   fork()
                     |
              +------+------+
              |             |
              v             v
           父进程          子进程
             cwd             cwd
              |               |
           /home            /home
                              |
                         chdir("/tmp")
                              |
                              v
                            /tmp

       父进程 cwd 仍然是 /home

8.3 cd 修改 cwd 并更新 PWD 的流程图

text 复制代码
用户输入 cd /tmp
        |
        v
CheckAndExecBuiltCommand()
        |
        v
识别为内建命令
        |
        v
chdir("/tmp")
        |
        v
Shell 的 cwd 改变
        |
        v
回到 main() 主循环
        |
        v
PrintCommandLine()
        |
        v
GetPwd()
        |
        +---- getcwd()
        |       |
        |       v
        |    获取 /tmp
        |
        +---- snprintf()
        |       |
        |       v
        |    构造 PWD=/tmp
        |
        +---- putenv()
                |
                v
           更新当前进程的 PWD

8.4 本篇核心知识总结

接口或概念 核心作用
内建命令 由 Shell 自己执行的命令
fork() 创建子进程
cwd 进程当前实际工作目录
chdir() 修改调用进程的工作目录
getcwd() 获取调用进程的实际工作目录
PWD 记录当前目录的环境变量
snprintf() 格式化构造字符串
putenv() 修改当前进程的环境变量
waitpid() 等待子进程并获取状态
lastcode Shell 保存的最近一次命令状态

九、进一步思考:Shell 还需要管理什么?

通过这次自定义 Shell 的实现,我们发现了一个重要变化:

最初的 Shell 只负责接收命令、创建子进程和执行程序。

但是随着功能增加,它开始管理更多属于自己的状态:

  • 当前工作目录。
  • 环境变量。
  • 最近一次命令的退出状态。
  • 内建命令的执行逻辑。

这也意味着 Shell 不再只是一个简单的程序启动器,而是一个需要维护自身运行环境的进程。

下一篇,我们继续研究环境变量管理。

代码中已经出现了:

cpp 复制代码
extern char **environ;

char *genv[envnum];

以及:

cpp 复制代码
InitEnv();
AddEnv();
execvpe(gargv[0], gargv, genv);

这些代码背后涉及一个非常重要的问题:

Shell 自己维护的环境变量表,与 Linux 原有的环境变量表究竟是什么关系?当 Shell 创建子进程并执行新程序时,这些环境变量又是如何传递的?

我们将在下一篇中继续研究 environ、genv、export、execvpe(),把环境变量的初始化、修改和进程传递机制串联起来。


写在最后

从第一篇到第二篇,我们的研究对象发生了一点变化。

第一篇主要解决的是:如何让 Shell 执行一条命令?

第二篇进一步解决的是:Shell 如何管理自己的状态?

一个看似简单的 cd,实际上把进程创建、进程上下文、工作目录、环境变量等知识联系在了一起。

这也是 Linux 学习中很有意思的一点:很多看似独立的系统接口,实际上都在共同完成一个完整的系统功能。

学会从代码中发现问题,再通过操作系统原理解释问题,最后回到代码验证理解,才能真正建立起对 Linux 的整体认识。

相关推荐
Gary移动存储固件1 小时前
eMMC5.1协议中文学习讲解(6.6.17~6.6.22.4.6)
学习·ufs·emmc·nand·存储固件
一个天蝎座 白勺 程序猿1 小时前
别让收藏夹替你学习:我搭AI知识体系的完整方法,脚本直接拿去用
人工智能·学习·ai·ai学习
楚山之石2 小时前
【Nat Biomed Eng】Connectome 2.0 超高梯度扫描仪:500 mT/m 与 600 T/m/s 把活体人脑微观结构分辨推进到亚微米级
图像处理·学习·数据分析·mri
坤坤子吖4 小时前
Linux网络编程:TCP 粘包与半包问题,如何正确处理 TCP 流式数据?
linux·服务器·网络·笔记·网络协议·学习
AI_Gump5 小时前
Harness篇|DeepSeek Harness 上手机:五条安卓移植路线与 App 内 Linux 拆解
android·linux·人工智能·智能手机·deepseek
码客-ygluu6 小时前
一个通用游戏数据服务器
c++·架构
another heaven6 小时前
【C/C++ Ranges 是零开销抽象吗?拆开管道、惰性求值和编译期膨胀】
c语言·开发语言·c++
星恒随风6 小时前
MySQL 从零入门(六):复合查询详解——多表查询、自连接、子查询与 UNION
linux·服务器·数据库·mysql
少司府6 小时前
嵌入式入门篇(二):初识STM32F103ZET6
linux·python·stm32·单片机·嵌入式硬件·mcu·嵌入式软件