如何让一个进程诞生、工作、终止并等待回收?——探索Linux进程控制与Shell的诞生

引言

在操作系统中,进程是程序执行的基本单位。理解进程的控制机制是深入学习系统编程的重要一步。本文将围绕进程创建、终止、等待、程序替换以及如何构建一个简单的 Shell 展开讲解,带你逐步深入理解进程的工作原理。
进程控制核心概念
fork创建进程
进程终止与退出码
进程等待避免僵尸
进程程序替换exec
微型Shell实现
写时拷贝技术
父子进程执行流程
exit与_exit区别
退出码含义解析
wait阻塞等待
waitpid非阻塞等待
六种exec函数
程序替换原理
内建命令处理
外部命令执行
cd, export, echo
fork + exec + wait

一、进程创建:fork()

什么是 fork?

在 Linux 中,fork() 是一个重要的系统调用,用于从当前进程(父进程)创建一个新进程(子进程)。子进程会复制父进程的代码和数据,然后两者各自独立执行。

c 复制代码
pid_t fork(void);
  • 成功时:父进程返回子进程的 PID,子进程返回 0
  • 失败时:返回 -1

fork 的工作原理

  1. 内核为子进程分配新的内存空间和数据结构
  2. 复制父进程的部分数据结构到子进程
  3. 将子进程添加到系统进程列表中
  4. 返回结果,由调度器决定哪个进程先执行

子进程 内核 父进程 子进程 内核 父进程 par [并行执行] 调用fork() 分配子进程内存空间 复制父进程数据结构 添加子进程到进程列表 返回子进程PID 返回0 继续执行父进程代码 执行与父进程相同的代码

写时拷贝(Copy-On-Write)

父子进程在创建时共享物理内存,只有当其中一方尝试写入时,系统才会为它分配独立的物理空间。这种机制节省了内存,提高了效率。

常见的 fork 用法

  • 父进程创建子进程处理任务(如网络请求)
  • 子进程执行另一个程序(通过 exec 系列函数)

二、进程终止:如何优雅地退出

进程终止时,系统会回收其占用的资源。进程退出有以下几种情况:

  • 正常退出(代码执行完毕)
  • 异常退出(如收到信号)

退出方式

  1. main 返回
  2. 调用 exit():会清理缓冲区,执行用户定义的清理函数
  3. 调用 _exit():直接退出,不进行清理
  4. 收到终止信号 (如 Ctrl+C

退出码

退出码用于表示进程执行的结果:

  • 0:成功
  • 0:失败(不同值代表不同错误)

例如:

  • 1:通用错误
  • 127:命令未找到
  • 130:被 Ctrl+C 终止

三、进程等待:避免僵尸进程

如果父进程不等待子进程退出,子进程可能成为"僵尸进程",占用系统资源。父进程可以通过 wait()waitpid() 回收子进程资源并获取其退出状态。

wait() 与 waitpid()

c 复制代码
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
  • wait():阻塞等待任意子进程退出
  • waitpid():可指定等待某个子进程,支持非阻塞模式(WNOHANG

获取子进程状态

通过 status 参数可以判断子进程是正常退出还是异常终止,并获取退出码或终止信号。


四、进程程序替换:exec 系列函数

如果我们希望子进程执行一个全新的程序,可以使用 exec 系列函数。它会用新程序的代码和数据替换当前进程的内容,但进程 PID 不变。

常见的 exec 函数

c 复制代码
execl("/bin/ls", "ls", "-l", NULL);
execvp("ls", argv);
  • l 结尾的函数使用参数列表
  • v 结尾的使用参数数组
  • p 的自动搜索 PATH
  • e 的允许自定义环境变量

Exec函数家族
exec函数
execl: 列表参数

无路径搜索
execp: 列表参数

PATH搜索
execle: 列表参数

自定义环境变量
execv: 数组参数

无路径搜索
execvp: 数组参数

PATH搜索
execve: 数组参数

自定义环境变量
真正的系统调用


五、动手实现一个微型 Shell

理解了进程控制的基础后,我们可以尝试实现一个简单的 Shell。一个基本的 Shell 需要完成以下步骤:

  1. 显示提示符 (如 [user@host dir]$
  2. 读取用户输入的命令
  3. 解析命令和参数
  4. 判断是否为内建命令 (如 cdexport
  5. 创建子进程执行外部命令
  6. 等待子进程退出并处理结果



Shell开始
打印提示符
读取用户输入
解析命令参数
是否为内建命令?
执行内建命令
创建子进程fork
子进程exec执行
父进程wait等待
子进程退出
获取退出状态

内建命令 vs 外部命令

  • 内建命令 :由 Shell 自身处理(如 cdexport
  • 外部命令 :通过创建子进程并调用 exec 执行

微型 Shell 完整源码

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;

// 全局的命令行参数表
char *gargv[argvnum];
int gargc = 0;

// 全局的变量
int lastcode = 0;

// 我的系统的环境变量
char *genv[envnum];

// 全局的当前shell工作路径
char pwd[basesize];
char pwdenv[basesize];

#define TrimSpace(pos) do{ \
    while(isspace(*pos)){ \
        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=XXX
    return pwd;
}

string LastDir()
{
    string curr = GetPwd();
    if(curr == "/" || curr == "None") return curr;
    // /home/whb/XXX
    size_t pos = curr.rfind("/");
    if(pos == std::string::npos) return curr;
    return curr.substr(pos+1);
}

string MakeCommandLine()
{
    // [whb@bite-alicloud myshell]#
    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() // 1. 命令行提示符
{
    printf("%s", MakeCommandLine().c_str());
    fflush(stdout);
}

bool GetCommandLine(char command_buffer[], int size) // 2. 获取用户命令
{
    // 我们认为:我们要将用户输入的命令行,当做一个完整的字符串
    // "ls -a -l -n"
    char *result = fgets(command_buffer, size, stdin);
    if(!result)
    {
        return false;
    }
    command_buffer[strlen(command_buffer)-1] = 0;
    if(strlen(command_buffer) == 0) return false;
    return true;
}

void ParseCommandLine(char command_buffer[], int len) // 3. 分析命令
{
    (void)len;
    memset(gargv, 0, sizeof(gargv));
    gargc = 0;
    
    // "ls -a -l -n"
    const char *sep = " ";
    gargv[gargc++] = strtok(command_buffer, sep);
    // = 是刻意写的
    while((bool)(gargv[gargc] = strtok(nullptr, sep))) gargc++;
    gargc--;
}

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

// 在shell中
// 有些命令,必须由子进程来执行
// 有些命令,不能由子进程执行,要由shell自己执行--内建命令 built command
bool ExecuteCommand() // 4. 执行命令
{
    // 让子进程进行执行
    pid_t id = fork();
    if(id < 0) return false;
    if(id == 0)
    {
        // 子进程
        // 1. 执行命令
        execvpe(gargv[0], gargv, genv);
        // 2. 退出
        exit(1);
    }
    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;
}

// shell自己执行命令,本质是shell调用自己的函数
bool CheckAndExecBuiltCommand()
{
    if(strcmp(gargv[0], "cd") == 0)
    {
        // 内建命令
        if(gargc == 2)
        {
            chdir(gargv[1]);
            lastcode = 0;
        }
        else
        {
            lastcode = 1;
        }
        return true;
    }
    else if(strcmp(gargv[0], "export") == 0)
    {
        // export也是内建命令
        if(gargc == 2)
        {
            AddEnv(gargv[1]);
            lastcode = 0;
        }
        else
        {
            lastcode = 2;
        }
        return true;
    }
    else if(strcmp(gargv[0], "env") == 0)
    {
        for(int i = 0; genv[i]; i++)
        {
            printf("%s\n", genv[i]);
        }
        lastcode = 0;
        return true;
    }
    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;
}

// 作为一个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;
}

int main()
{
    InitEnv();
    char command_buffer[basesize];
    while(true)
    {
        PrintCommandLine(); // 1. 命令行提示符
        // command_buffer -> output
        if(!GetCommandLine(command_buffer, basesize)) // 2. 获取用户命令
        {
            continue;
        }
        ParseCommandLine(command_buffer, strlen(command_buffer)); // 3. 分析命令
        
        if (CheckAndExecBuiltCommand())
        {
            continue;
        }
        
        ExecuteCommand(); // 4. 执行命令
    }
    return 0;
}

Shell 的工作流程

复制代码
循环:
  打印提示符 → 读取命令 → 解析命令 → 
  是内建命令? → 执行 → 继续
  否 → 创建子进程 → exec 执行命令 → 父进程等待

六、总结:进程与函数的类比

我们可以将进程之间的关系类比为函数调用:

  • fork() / exec() 类似于函数调用
  • exit() 类似于函数返回
  • wait() 类似于获取函数返回值

类比
类比
类比
类比
函数调用模型
调用函数
传递参数
执行函数体
返回结果
进程调用模型
fork创建子进程
exec传递参数
执行新程序
exit返回结果
wait获取结果

这种类比帮助我们理解进程间的协作与通信,是系统编程中重要的思维方式。


通过本文,我们从进程的创建、终止、等待、替换,逐步深入到如何实现一个简单的 Shell。这不仅加深了对进程控制的理解,也为后续学习多进程编程、进程间通信等内容打下了坚实基础。

如果你对进程控制或 Shell 实现有更多疑问,欢迎留言讨论!

相关推荐
NAGNIP17 小时前
轻松搞懂全连接神经网络结构!
人工智能·算法·面试
NAGNIP17 小时前
一文搞懂激活函数!
算法·面试
董董灿是个攻城狮17 小时前
AI 视觉连载7:传统 CV 之高斯滤波实战
算法
爱理财的程序媛1 天前
openclaw 盯盘实践
算法
端平入洛1 天前
auto有时不auto
c++
Rockbean1 天前
用40行代码搭建自己的无服务器OCR
服务器·python·deepseek
MobotStone1 天前
Google发布Nano Banana 2:更快更便宜,图片生成能力全面升级
算法
茶杯梦轩1 天前
CompletableFuture 在 项目实战 中 创建异步任务 的核心优势及使用场景
服务器·后端·面试
崔小汤呀1 天前
最全的docker安装笔记,包含CentOS和Ubuntu
linux·后端