四、进程控制(进程等待与进程程序替换,shell)


3. 进程等待


3-1 为什么要等待子进程

问题一:僵尸进程 → 内存泄漏

子进程干完活死了,但父进程不管不问。子进程的 task_struct 赖在内存里不走------变成了僵尸。僵尸不干活,但占资源(task_struct、内核栈等),积累多了系统进程数耗尽。

问题二:僵尸刀枪不入

复制代码
子进程已经死了 → kill -9 对它无效
谁也没法杀死一个已经死去的进程
只有父进程 wait 才能回收

问题三:父进程需要验收结果

父进程派子进程去干活,总得知道:活干完了没?干对了没?返回值多少?

父进程通过 wait / waitpid 回收子进程资源,同时获取子进程的退出信息。


3-2 wait 和 waitpid

wait
复制代码
#include <sys/wait.h>
pid_t wait(int *status);
// 返回:成功 → 被回收的子进程 pid,失败 → -1
// status:输出型参数,拿到子进程是怎么死的、返回值是多少

wait 是阻塞的------子进程还没死,父进程就一直等。

waitpid
复制代码
pid_t waitpid(pid_t pid, int *status, int options);
//status: 输出型参数
//WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是
//否是正常退出)
//WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的
//退出码)
参数 含义
pid -1 等任意一个孩子(效果=wait)
>0 只等 pid 对应的那个孩子
options 0 阻塞等待
WNOHANG 不阻塞,没人就返回 0: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等 待。 若正常结束,则返回该子进程的ID。

waitpid 更精细:能指定等哪个孩子,能选择不阻塞。

• 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子 进程退出信息。

• 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。

• 如果不存在该子进程,则立即出错返回。


3-2-3 解析 status 位图

• wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。

• 如果传递NULL,表示不关心子进程的退出状态信息。

• 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

• status不能简单的当作整形来看待,可以当作位图来看待

wait 拿到的 status 是 int,但只看低 16 位,格式如下:

复制代码
status 的低 16 位(以 exit(10) 为例):
高 8 位                    低 8 位
┌──────────────┬──────────────┐
│  退出码 = 10  │ 终止信号 = 0   │  ← 正常退出时:低 8 位为 0
└──────────────┴──────────────┘
  (st>>8)&0xFF    st & 0x7F

status 的低 16 位(以被 SIGKILL 杀死为例):
┌──────────────┬──────────────┐
│  退出码 = 0   │ 信号 = 9      │  ← 被信号杀死时:低 8 位 = 信号编号
└──────────────┴──────────────┘
  (st>>8)&0xFF    st & 0x7F

还有一位 core dump 标志(bit 7):

复制代码
完整位图:
┌──────┬──────┬──────────────┬──────────────┐
│ (高16)│coredump│  退出码(8位)  │ 终止信号(7位)  │
│ 不管  │ 1位   │ (st>>8)&0xFF │ st & 0x7F    │
└──────┴──────┴──────────────┴──────────────┘

实验结果对照

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

int main(void)
{
    pid_t pid;
    if ((pid = fork()) == -1)
        perror("fork"), exit(1);

    if (pid == 0) {                    // 子进程
        sleep(20);                     // 睡 20 秒等被杀
        exit(10);
    } else {                           // 父进程
        printf("父进程: 子进程pid=%d, 3秒后kill它\n", pid);
        sleep(1);
        kill(pid, SIGKILL);            // 父进程亲手杀儿子

        int st;
        int ret = wait(&st);
        if (ret > 0 && (st & 0x7F) == 0) {
            printf("正常退出, exit code: %d\n", (st >> 8) & 0xFF);
        } else if (ret > 0) {
            printf("异常退出, sig code: %d\n", st & 0x7F);
        }
    }
    return 0;
}
复制代码
正常退出(exit(10)):
  st & 0x7F == 0   →  正常退出
  (st >> 8) & 0xFF → 10

被 SIGKILL 杀死:
  st & 0x7F == 9   →  异常退出,信号编号 9(SIGKILL)

判断逻辑:

复制代码
if (st & 0x7F == 0)          // 正常退出
    exit_code = (st >> 8) & 0xFF;
else                          // 被信号杀死
    signal = st & 0x7F;

系统提供宏,不用手写位运算:

作用
WIFEXITED(status) 正常退出?真/假
WEXITSTATUS(status) 提取退出码(前提 WIFEXITED 为真)
WIFSIGNALED(status) 被信号杀死?真/假
WTERMSIG(status) 提取信号编号(前提 WIFSIGNALED 为真)

总结

复制代码
父进程 fork 出子进程
    │
    ▼
子进程干活(可能正常退出 exit(n),也可能被 kill -9)
    │
    ▼
父进程 wait(&status)
    ├── 阻塞等着
    ├── 子进程死了 → 回收 task_struct(僵尸消失)
    └── 从 status 里解出退出码或信号

没人 wait 的孩子变僵尸,wait 之后的僵尸被安葬。

4、进程程序替换

fork() 之后,父子各自执行父进程代码的一部分如果子进程就想执行一个全新的程序呢? 进程的程序 替换来完成这个功能:通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中!

4-1 替换原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一 种exec函数执行另一个程序 。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换 ,从新程序的启动例程开始执行。调用exec并不创建新进程 ,所以调用exec前后该进程的id并未改变。

4.2 替换函数(exec函数族)

1. 概述

程序替换是通过特定接口,加载磁盘上一个全新的程序(代码和数据),加载到调用进程的地址空间中。fork()之后,父子各自执行父进程代码的一部分,如果子进程想执行一个全新的程序,就需要进程的程序替换功能。

2. exec函数族

共有6种以exec开头的函数,统称exec函数:

复制代码
#include <unistd.h>

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
3. 函数解释(4-2-1)
  • 调用成功 :加载新的程序从启动代码开始执行,不再返回
  • 调用出错:返回-1
  • 重要特点:exec函数只有出错的返回值,没有成功的返回值
4. 命名理解(4-2-2)

这些函数原型看起来容易混淆,但掌握规律就很好记:

字母 含义 说明
l (list) 参数采用列表 参数逐个传递
v (vector) 参数用数组 参数通过数组传递
p (path) 自动搜索PATH 有p则自动搜索环境变量PATH
e (env) 自己维护环境变量 不使用当前环境变量,需自己组装
5. 函数对比表
函数名 参数格式 是否带路径 是否使用当前环境变量
execl 列表 不是
execlp 列表
execle 列表 不是 不是,须自己组装环境变量
execv 数组 不是
execvp 数组
execve 数组 不是 不是,须自己组装环境变量
6. 调用示例
复制代码
#include <unistd.h>

int main()
{
    char *const argv[] = {"ps", "-ef", NULL};
    char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};

    execl("/bin/ps", "ps", "-ef", NULL);           // 完整路径,列表参数
    execlp("ps", "ps", "-ef", NULL);               // 带p,自动搜索PATH
    execle("ps", "ps", "-ef", NULL, envp);         // 带e,自己组装环境变量
    execv("/bin/ps", argv);                        // 完整路径,数组参数
    execvp("ps", argv);                            // 带p,自动搜索PATH
    execve("/bin/ps", argv, envp);                 // 带e,自己组装环境变量
    
    exit(0);
}
7. 函数关系图
复制代码
execp ──────→ execv ──────→ execve(系统调用)
  ↓              ↓              ↓
  把可变参数保存    使用environ      使用envp
  到以NULL结尾     所指向的当前      指向的当前
  的指针数组中     环境变量表        环境变量表

execl ──────→ execv
  ↓              ↓
  把可变参数保存    使用environ
  到以NULL结尾     所指向的当前
  的指针数组中     环境变量表

execle ─────→ execve(系统调用)
  ↓              ↓
  把可变参数保存    使用envp
  到以NULL结尾     指向的当前
  的指针数组中     环境变量表
8. 重要说明
  1. 只有execve是真正的系统调用,其他5个函数最终都调用execve
  2. execve在man手册第2节,其他函数在man手册第3节
  3. 下图exec函数簇 一个完整的例子:
  1. 调用exec并不创建新进程,所以调用exec前后该进程的id并未改变
  2. 当进程调用exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行
9. 实际应用

在shell实现中,exec函数族用于执行用户输入的命令:

  1. 获取命令行
  2. 解析命令行
  3. 建立子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程等待子进程退出(wait)

5. 自主Shell命令行解释器

5-1 目标

Shell(命令行解释器)的目标是:

  1. 能处理普通命令 (如 ls, ps, cat 等)
  2. 能处理内建命令 (如 cd, export, env, echo 等)
  3. 帮助理解内建命令/本地变量/环境变量的概念
  4. 帮助理解 Shell 的运行原理

5-2 实现原理

Shell的基本工作流程

Shell与用户交互的过程可以这样理解:

复制代码
用户输入 "ls" 
    ↓
Shell 读入字符串 "ls"
    ↓
Shell 建立一个新的子进程(fork)
    ↓
在子进程中运行 ls 程序(execvp)
    ↓
父进程(Shell)等待子进程退出(wait)
    ↓
显示新的命令行提示符,等待下一条命令

用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时 间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运 行ls程序并等待那个进程结束。

关键点: Shell本身是一个死循环程序,不断重复"读取命令→解析命令→执行命令"这个过程。

Shell处理命令的5个步骤

复制代码
1. 获取命令行        // 读取用户输入
2. 解析命令行        // 将字符串分割成参数数组
3. 建立子进程(fork) // 创建子进程来执行命令
4. 替换子进程(execvp)// 用目标程序替换子进程
5. 父进程等待子进程退出(wait)// 回收子进程资源

5-3 源码详解

1. 头文件和全局变量

复制代码
#include <iostream>      // C++标准输入输出流
#include <cstdio>        // C标准输入输出(printf, fgets等)
#include <cstdlib>       // C标准库(exit, malloc等)
#include <cstring>       // C字符串操作(strcmp, strtok等)
#include <string>        // C++ string类
#include <unistd.h>      // Unix标准函数(fork, exec, chdir等)
#include <sys/types.h>   // 系统类型定义(pid_t)
#include <sys/wait.h>    // 进程等待函数(waitpid)
#include <ctype.h>       // 字符判断函数(isspace)

using namespace std;

const int basesize = 1024;   // 缓冲区基础大小
const int argvnum = 64;      // 最大参数个数
const int envnum = 64;       // 最大环境变量个数

// 全局的命令行参数表(解析后的参数存放在gargv数组中)
char *gargv[argvnum];        // 参数数组,如 {"ls", "-a", "-l", NULL}
int gargc = 0;               // 参数个数

// 全局变量:上一条命令的退出码(用于 echo $?)
int lastcode = 0;

// 自定义的环境变量表(Shell自己维护的环境变量)
char *genv[envnum];

// 当前Shell工作路径
char pwd[basesize];          // 当前目录路径
char pwdenv[basesize];       // 环境变量格式 "PWD=/path"

2. 辅助宏:去空格

复制代码
// TrimSpace宏:跳过字符串开头的所有空白字符
// 用于处理用户输入时可能带的前导空格
#define TrimSpace(pos) do{        \
    while(isspace(*pos)){         \  // isspace判断字符是否为空格/tab/换行等
        pos++;                    \  // 指针后移,跳过空白
    }                             \
}while(0)

3. 获取用户信息和工作目录

复制代码
// 获取当前用户名(从环境变量USER获取)
string GetUserName()
{
    string name = getenv("USER");  // getenv获取环境变量值
    return name.empty() ? "None" : name;  // 如果为空返回"None"
}

// 获取主机名(从环境变量HOSTNAME获取)
string GetHostName()
{
    string hostname = getenv("HOSTNAME");
    return hostname.empty() ? "None" : hostname;
}

// 获取当前工作目录
string GetPwd()
{
    if(nullptr == getcwd(pwd, sizeof(pwd)))  // getcwd获取当前目录
        return "None";
    snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);  // 格式化为PWD=xxx
    putenv(pwdenv);  // 更新环境变量PWD
    return pwd;
}

// 获取上一级目录名(用于命令行提示符显示)
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);  // 返回最后一个目录名
}

// 生成命令行提示符,格式:[用户名@主机名 目录名]#
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;
}

4. 打印命令行提示符

复制代码
void PrintCommandLine()  // 1. 打印命令行提示符
{
    printf("%s", MakeCommandLine().c_str());
    fflush(stdout);  // 刷新输出缓冲区,确保提示符立即显示
}

5. 获取用户输入

复制代码
bool GetCommandLine(char command_buffer[], int size)  // 2. 获取用户命令
{
    // fgets从标准输入读取一行,存入command_buffer
    char *result = fgets(command_buffer, size, stdin);
    if(!result)  // 读取失败(如EOF)
    {
        return false;
    }
    // 去掉末尾的换行符\n(fgets会保留换行符)
    command_buffer[strlen(command_buffer)-1] = 0;
    if(strlen(command_buffer) == 0) return false;  // 空命令
    return true;
}

6. 解析命令行

复制代码
void ParseCommandLine(char command_buffer[], int len)  // 3. 分析命令
{
    (void)len;  // 忽略未使用的参数
    memset(gargv, 0, sizeof(gargv));  // 清空参数数组
    gargc = 0;

    // 使用strtok分割字符串
    // "ls -a -l -n" 分割成 "ls", "-a", "-l", "-n"
    const char *sep = " ";  // 分隔符是空格
    gargv[gargc++] = strtok(command_buffer, sep);  // 第一次调用传入字符串
    // 后续调用传入nullptr,继续分割同一个字符串
    while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
    gargc--;  // 因为最后多加了一次,需要减回来
}

7. 调试函数

复制代码
void debug()
{
    printf("argc: %d\n", gargc);
    for(int i = 0; gargv[i]; i++)
    {
        printf("argv[%d]: %s\n", i, gargv[i]);
    }
}

8. 执行外部命令(核心函数)

复制代码
// 在Shell中:
// 有些命令必须由子进程执行(如 ls, ps, cat 等)
// 有些命令必须由Shell自己执行(内建命令:cd, export, env, echo 等)

bool ExecuteCommand()  // 4. 执行命令
{
    pid_t id = fork();  // 创建子进程
    if(id < 0) return false;  // fork失败
    
    if(id == 0)  // 子进程
    {
        // 子进程执行目标程序
        // execvp会搜索PATH环境变量来找到程序
        execvp(gargv[0], gargv);  // 用gargv[0]指定的程序替换当前进程
        exit(1);  // 如果execvp失败,子进程退出
    }
    
    // 父进程(Shell)
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);  // 阻塞等待子进程退出
    if(rid > 0)
    {
        if(WIFEXITED(status))  // 子进程正常退出
        {
            lastcode = WEXITSTATUS(status);  // 获取退出码
        }
        else  // 子进程被信号杀死
        {
            lastcode = 100;
        }
        return true;
    }
    return false;
}

9. 内建命令处理

复制代码
// 添加环境变量到自定义环境变量表
void AddEnv(const char *item)
{
    int index = 0;
    while(genv[index])  // 找到数组末尾
    {
        index++;
    }
    // 动态分配内存并复制环境变量字符串
    genv[index] = (char*)malloc(strlen(item)+1);
    strncpy(genv[index], item, strlen(item)+1);
    genv[++index] = nullptr;  // 保持数组以nullptr结尾
}

// 检查并执行内建命令
bool CheckAndExecBuiltCommand()
{
    // cd命令 - 切换目录(内建命令,必须由Shell自己执行)
    if(strcmp(gargv[0], "cd") == 0)
    {
        if(gargc == 2)  // cd 后面跟一个目录
        {
            chdir(gargv[1]);  // chdir系统调用切换目录
            lastcode = 0;
        }
        else
        {
            lastcode = 1;  // 参数错误
        }
        return true;  // 已处理,不再需要fork执行
    }
    // export命令 - 设置环境变量
    else if(strcmp(gargv[0], "export") == 0)
    {
        if(gargc == 2)
        {
            AddEnv(gargv[1]);  // 添加到环境变量表
            lastcode = 0;
        }
        else
        {
            lastcode = 2;
        }
        return true;
    }
    // env命令 - 显示所有环境变量
    else if(strcmp(gargv[0], "env") == 0)
    {
        for(int i = 0; genv[i]; i++)
        {
            printf("%s\n", genv[i]);
        }
        lastcode = 0;
        return true;
    }
    // echo命令 - 输出文本或变量值
    else if(strcmp(gargv[0], "echo") == 0)
    {
        if(gargc == 2)
        {
            // echo $?  显示上一条命令的退出码
            // echo $PATH 显示环境变量
            // echo hello 输出字符串
            if(gargv[1][0] == '$')  // 以$开头表示变量
            {
                if(gargv[1][1] == '?')  // $? 特殊变量
                {
                    printf("%d\n", lastcode);
                    lastcode = 0;
                }
            }
            else  // 普通字符串
            {
                printf("%s\n", gargv[1]);
                lastcode = 0;
            }
        }
        else
        {
            lastcode = 3;
        }
        return true;
    }
    return false;  // 不是内建命令,返回false
}

10. 初始化环境变量

复制代码
// Shell启动时,从父进程(系统Shell)继承环境变量
void InitEnv()
{
    extern char **environ;  // 系统的环境变量(从父进程继承)
    int index = 0;
    while(environ[index])
    {
        // 为每个环境变量分配内存并复制
        genv[index] = (char*)malloc(strlen(environ[index])+1);
        strncpy(genv[index], environ[index], strlen(environ[index])+1);
        index++;
    }
    genv[index] = nullptr;  // 以nullptr结尾
}

11. 主函数(Shell的死循环)

复制代码
int main()
{
    InitEnv();  // 初始化环境变量
    char command_buffer[basesize];  // 命令输入缓冲区
    
    while(true)  // Shell的死循环(一直运行直到Ctrl+C)
    {
        PrintCommandLine();  // 1. 打印命令行提示符
        
        if(!GetCommandLine(command_buffer, basesize))  // 2. 获取用户命令
        {
            continue;  // 获取失败,继续循环
        }
        
        ParseCommandLine(command_buffer, strlen(command_buffer));  // 3. 解析命令
        
        if(CheckAndExecBuiltCommand())  // 4. 检查是否是内建命令
        {
            continue;  // 是内建命令,已处理,继续循环
        }
        
        ExecuteCommand();  // 5. 执行外部命令
    }
    return 0;
}

汇总:

cpp 复制代码
#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;      // 最大参数个数
const int envnum = 64;       // 最大环境变量个数

// 全局的命令行参数表(解析后的参数存放在gargv数组中)
char *gargv[argvnum];
int gargc = 0;               // 参数个数

// 上一条命令的退出码(用于 echo $?)
int lastcode = 0;

// 自定义的环境变量表(Shell自己维护的环境变量)
char *genv[envnum];

// 当前Shell工作路径
char pwd[basesize];
char pwdenv[basesize];

// ==================== 辅助宏:跳过前导空白字符 ====================
#define TrimSpace(pos) do{          \
    while(isspace(*pos)){           \  // 如果是空格/tab/换行
        pos++;                      \  // 指针后移
    }                               \
}while(0)

// ==================== 获取用户信息 ====================

// 获取当前用户名
string GetUserName()
{
    string name = getenv("USER");
    return name.empty() ? "None" : name;
}

// 获取主机名
string GetHostName()
{
    string hostname = getenv("HOSTNAME");
    return hostname.empty() ? "None" : hostname;
}

// 获取当前工作目录
string GetPwd()
{
    if(nullptr == getcwd(pwd, sizeof(pwd)))
        return "None";
    snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);
    putenv(pwdenv);  // 更新环境变量PWD
    return pwd;
}

// 获取上一级目录名(用于提示符显示)
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);
}

// ==================== 命令行提示符 ====================

// 生成命令行提示符:[用户名@主机名 目录名]#
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;
}

// 打印命令行提示符
void PrintCommandLine()
{
    printf("%s", MakeCommandLine().c_str());
    fflush(stdout);  // 刷新缓冲区,确保立即显示
}

// ==================== 获取用户输入 ====================

bool GetCommandLine(char command_buffer[], int size)
{
    char *result = fgets(command_buffer, size, stdin);
    if(!result)
    {
        return false;  // 读取失败(如Ctrl+D)
    }
    // 去掉末尾的换行符
    command_buffer[strlen(command_buffer) - 1] = 0;
    if(strlen(command_buffer) == 0) return false;  // 空命令
    return true;
}

// ==================== 解析命令行 ====================

void ParseCommandLine(char command_buffer[], int len)
{
    (void)len;
    memset(gargv, 0, sizeof(gargv));
    gargc = 0;

    // 使用strtok分割字符串
    // 例如:"ls -a -l -n" 分割成 "ls", "-a", "-l", "-n"
    const char *sep = " ";
    gargv[gargc++] = strtok(command_buffer, sep);
    while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
    gargc--;  // 减去多加的一次
}

// ==================== 调试函数 ====================

void debug()
{
    printf("argc: %d\n", gargc);
    for(int i = 0; gargv[i]; i++)
    {
        printf("argv[%d]: %s\n", i, gargv[i]);
    }
}

// ==================== 执行外部命令 ====================

bool ExecuteCommand()
{
    pid_t id = fork();  // 创建子进程
    if(id < 0) return false;

    if(id == 0)  // 子进程
    {
        // execvp会自动在PATH环境变量指定的目录中搜索程序
        execvp(gargv[0], gargv);
        exit(1);  // 如果execvp失败,退出子进程
    }

    // 父进程(Shell)
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);  // 阻塞等待子进程
    if(rid > 0)
    {
        if(WIFEXITED(status))  // 正常退出
        {
            lastcode = WEXITSTATUS(status);  // 获取退出码
        }
        else  // 被信号杀死
        {
            lastcode = 100;
        }
        return true;
    }
    return false;
}

// ==================== 添加环境变量 ====================

void AddEnv(const char *item)
{
    int index = 0;
    while(genv[index])
    {
        index++;
    }
    genv[index] = (char*)malloc(strlen(item) + 1);
    strncpy(genv[index], item, strlen(item) + 1);
    genv[++index] = nullptr;
}

// ==================== 内建命令处理 ====================

bool CheckAndExecBuiltCommand()
{
    // cd命令 - 切换目录
    if(strcmp(gargv[0], "cd") == 0)
    {
        if(gargc == 2)
        {
            chdir(gargv[1]);  // 切换工作目录
            lastcode = 0;
        }
        else
        {
            lastcode = 1;
        }
        return true;
    }
    // export命令 - 设置环境变量
    else if(strcmp(gargv[0], "export") == 0)
    {
        if(gargc == 2)
        {
            AddEnv(gargv[1]);
            lastcode = 0;
        }
        else
        {
            lastcode = 2;
        }
        return true;
    }
    // env命令 - 显示环境变量
    else if(strcmp(gargv[0], "env") == 0)
    {
        for(int i = 0; genv[i]; i++)
        {
            printf("%s\n", genv[i]);
        }
        lastcode = 0;
        return true;
    }
    // echo命令 - 输出文本
    else if(strcmp(gargv[0], "echo") == 0)
    {
        if(gargc == 2)
        {
            if(gargv[1][0] == '$')  // 变量
            {
                if(gargv[1][1] == '?')  // $? 特殊变量
                {
                    printf("%d\n", lastcode);
                    lastcode = 0;
                }
            }
            else  // 普通字符串
            {
                printf("%s\n", gargv[1]);
                lastcode = 0;
            }
        }
        else
        {
            lastcode = 3;
        }
        return true;
    }
    return false;  // 不是内建命令
}

// ==================== 初始化环境变量 ====================

void InitEnv()
{
    extern char **environ;  // 系统环境变量
    int index = 0;
    while(environ[index])
    {
        genv[index] = (char*)malloc(strlen(environ[index]) + 1);
        strncpy(genv[index], environ[index], strlen(environ[index]) + 1);
        index++;
    }
    genv[index] = nullptr;
}

// ==================== 主函数 ====================

int main()
{
    InitEnv();  // 初始化环境变量
    char command_buffer[basesize];

    while(true)  // Shell的死循环
    {
        PrintCommandLine();  // 1. 打印提示符

        if(!GetCommandLine(command_buffer, basesize))  // 2. 获取命令
        {
            continue;
        }

        ParseCommandLine(command_buffer, strlen(command_buffer));  // 3. 解析命令

        if(CheckAndExecBuiltCommand())  // 4. 检查内建命令
        {
            continue;
        }

        ExecuteCommand();  // 5. 执行外部命令
    }
    return 0;
}

5-4 总结:Shell的核心原理

exec/exit就像call/return 一个C程序有很多函数组成。一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数 执行一定的操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过call/return系统进 行通信

这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。 Linux鼓励将这 种应用于程序之内的模式扩展到程序之间。如下图

一个C程序可以fork/exec另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后 通过exit(n)来返回值。调用它的进程可以通过wait(&ret)来获取exit的返回值

exec/exit 与 call/return 的类比

函数调用 进程执行
call 函数A fork + exec 启动新进程
函数执行 进程执行
return 返回值 exit(n) 返回退出码
调用者获取返回值 wait(&ret) 获取退出码

核心思想:

  • 一个C程序可以通过 fork/exec 启动另一个程序
  • 被调用的程序执行完毕后通过 exit(n) 返回退出码
  • 调用它的进程可以通过 wait(&ret) 来获取退出码

这就是 Shell工作的基本原理:Shell本身是一个循环程序,不断fork子进程来执行用户输入的命令,然后wait等待子进程退出。

相关推荐
java_logo1 天前
轻量AI接口网关一键部署|calciumion/new-api Windows/Linux Docker 部署全教程
linux·人工智能·windows·one api·calciumion·ai网关部署·one api 部署
原来是猿1 天前
Linux - 【理解进程组、会话与作业控制】
linux·运维·服务器
怀旧,1 天前
【Linux网络编程】1. 网络基础概念
linux·网络
怀旧,1 天前
【Linux网络编程】5. 应用层协议 HTTP
linux·网络·http
SurpriseDPD1 天前
Linux 内核基础知识:READ_ONCE、内存屏障与指令重排
linux·系统架构
D4c-lovetrain1 天前
Linux个人心得29(深入理解K8S Pod优先级与驱逐机制:从原理到实战踩坑)
linux·运维·kubernetes
小吴伴学者1 天前
Linux RX报文处理全流程解析
linux
小侯不躺平.1 天前
C++ Boost库【2】 --stringalgo字符串算法
linux·c++·算法
夏乌_Wx1 天前
计算机网络实践项目 | 云相册(文件互传与管理系统)
linux·计算机网络