本文系统梳理Linux进程创建、终止、等待、程序替换核心知识点,搭配带详细注释的可运行代码,从原理到实践一站式掌握,适合复习、作业与面试使用。


1. 进程创建:fork/vfork函数

1.1 fork函数初识

函数原型

#include <unistd.h>
// 创建子进程,子进程是父进程的副本
pid_t fork(void);

返回值说明

进程类型 返回值 含义
子进程 0 子进程的标识,用于区分父子进程 
父进程 >0 子进程的PID(进程ID) 
出错 -1 进程数超限、系统资源不足等原因导致创建失败 

内核执行流程

当进程调用fork时,内核会完成以下操作:

1) 为子进程分配新的内存块和内核数据结构(PCB进程控制块等)

2) 将父进程的部分数据结构内容拷贝到子进程(写时拷贝优化)

3) 将子进程添加到系统进程列表中

4) fork返回,由调度器决定父子进程的执行顺序

核心原理图解

fork之前:只有父进程独立执行,用户空间和内核空间唯一

fork之后:父子进程拥有完全相同的代码段,执行流分离,各自独立运行

关键结论:fork之前的代码只由父进程执行,fork之后的代码由父子进程共同执行

示例代码与运行分析

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(void)
{
    pid_t pid;

    // fork之前,只有父进程执行
    printf("Before: pid is %d\n", getpid());

    // 调用fork创建子进程
    if ((pid = fork()) == -1)
    {
        perror("fork()");
        exit(1);
    }

    // fork之后,父子进程都会执行这段代码
    printf("After:pid is %d, fork return %d\n", getpid(), pid);
    sleep(1); // 保证子进程有时间执行完
    return 0;
}

运行结果

[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0

结果分析

   只打印1次Before:fork之前只有父进程(PID 43676)执行

   打印2次After:fork之后,父进程(PID 43676)和子进程(PID 43677)分别执行

   子进程返回0,父进程返回子进程PID,通过返回值区分父子进程

1.2 fork函数返回值的本质

   子进程返回0:子进程是被创建的进程,用0作为唯一标识,方便代码中区分父子逻辑

   父进程返回子进程PID:父进程需要通过PID管理子进程(如等待、发送信号)

   一个函数两个返回值:fork是内核级函数,执行后会创建新进程,两个进程分别从fork的下一行代码开始执行,因此各自拿到不同的返回值

1.3 写时拷贝(Copy-On-Write, COW)

核心原理

默认共享:fork创建子进程后,父子进程的代码段完全共享,数据段、堆、栈默认共享同一块物理内存

延时拷贝:只有当任意一方尝试修改共享数据时,内核才会为修改方分配新的物理内存,拷贝数据副本,实现进程独立性

优势:提高内存使用率,避免不必要的拷贝;保证进程独立性,修改互不影响;提升fork的执行效率

图解说明

   修改前:父子进程的页表指向同一块物理内存,页表标记为只读

   修改后:触发缺页中断,内核为修改进程分配新物理内存,拷贝数据,更新页表指向,双方完全独立

1.4 fork的常规用法

1) 父子进程执行不同代码段:父进程等待客户端请求,子进程处理请求(如网络服务器)

2) 子进程执行新程序:子进程fork返回后,调用exec系列函数加载新程序(如Shell执行命令)

1.5 fork调用失败的原因

系统中进程总数达到上限;实际用户的进程数超过限制(ulimit -u可查看);系统内存不足,无法为子进程分配资源

2. 进程终止

2.1 进程终止的本质

释放进程申请的所有系统资源(内核数据结构、内存、文件句柄等),从系统进程列表中移除。

2.2 进程退出场景

1) 代码运行完毕,结果正确(退出码0)

2) 代码运行完毕,结果不正确(退出码非0)

3) 代码异常终止(被信号杀死,如Ctrl+C、kill -9)

2.3 进程常见退出方法

2.3.1 正常终止(可通过echo $?查看退出码)

1) 从main返回:main函数的return n等价于exit(n),是最常用的正常退出方式

2) 调用exit函数:标准库函数,会执行清理操作后退出

3) 调用_exit函数:系统调用,直接终止进程,不做任何清理

什么是“清理操作”?

就是程序退出前,系统帮你自动做的收尾工作,主要就3件事:

1. 刷新缓冲区:把还没打印、没写到文件里的数据,真正输出出去

2. 关闭打开的文件:防止文件损坏、占用资源

3. 执行你注册的退出函数:比如 atexit 注册的函数

2.3.2 异常退出

Ctrl + C:发送SIGINT信号终止进程

其他信号:如SIGTERM(kill默认)、SIGKILL(kill -9)等

2.3.3 退出码(Exit Code)

核心说明

退出码0:表示程序执行成功,无错误

退出码非0:表示执行失败,不同非0值对应不同错误类型

Linux Shell常用退出码表

退出码 解释
0 命令成功执行 
1 通用错误代码
2 命令/参数使用不当 
126 权限被拒绝/无法执行 
127 未找到命令/PATH错误 
128+n 命令被信号n终止(如130=128+2,对应Ctrl+C) 
130 Ctrl+C/SIGINT终止 
143 SIGTERM终止(默认kill) 
255/* 退出码超出0-255,取模后返回 

工具函数

strerror(退出码):获取退出码对应的错误描述

echo $?:Shell中查看上一条命令的退出码

示例1

#include <stdio.h>
#include <string.h>   // strerror
#include <stdlib.h>

int main() {
    // 打印常见退出码的文字描述
    printf("strerror(0): %s\n", strerror(0));
    printf("strerror(1): %s\n", strerror(1));
    printf("strerror(2): %s\n", strerror(2));
    return 0;
}
strerror(0): Success
strerror(1): Operation not permitted
strerror(2): No such file or directory

示例2

随便运行一个错误命令:ls 不存在的文件
然后立刻输入:echo $?
你会看到输出:2
这个 2 就是上一条命令的退出码。

2.3.4 _exit函数详解

#include <unistd.h>
// 直接终止进程,不做任何清理
void _exit(int status);

参数:status为退出状态,仅低8位有效(因此_exit(-1)实际返回255)

特点:立即终止进程,不刷新缓冲区、不执行清理函数,直接进入内核

2.3.5 exit函数详解

#include <stdlib.h>
// 标准库退出函数,执行清理后调用_exit
void exit(int status);

执行流程:

1) 执行用户通过atexit/on_exit注册的清理函数

2) 关闭所有打开的流,刷新所有缓冲区(将内存中的数据写入文件)

3) 调用_exit(status),真正终止进程

与_exit的核心区别:exit会做用户层清理,_exit直接内核级终止

示例代码对比

// 示例1:exit会刷新缓冲区,打印hello
#include <stdio.h>
#include <stdlib.h>
int main()
{
    printf("hello"); // 无换行,数据留在缓冲区
    exit(0); // 刷新缓冲区,写入屏幕
}
// 运行结果:hello[root@localhost linux]#

// 示例2:_exit不刷新缓冲区,不打印hello
#include <stdio.h>
#include <unistd.h>
int main()
{
    printf("hello"); // 无换行,数据留在缓冲区
    _exit(0); // 直接终止,不刷新缓冲区
}
// 运行结果:[root@localhost linux]#

2.3.6 return退出

return n是main函数中最常用的退出方式,等价于exit(n):main函数的运行时函数会将return的返回值作为exit的参数;非main函数的return仅返回函数值,不终止进程

3. 进程等待

3.1 进程等待的必要性

1) 解决僵尸进程问题:子进程退出后,父进程若不回收,子进程会变成僵尸进程,占用系统资源,且无法被kill -9杀死

2) 获取子进程退出信息:父进程需要知道子进程的执行结果(成功/失败、退出码、终止信号)

3) 保证进程顺序:父进程等待子进程执行完毕后再继续,保证逻辑正确性

3.2 进程等待的方法

3.2.1 wait方法

#include <sys/types.h>
#include <sys/wait.h>
// 等待任意一个子进程退出
pid_t wait(int* status);

返回值:成功返回被等待子进程的PID,失败返回-1

参数:status为输出型参数,用于获取子进程的退出状态;不关心可传NULL

特点:阻塞式等待,父进程会一直挂起,直到有子进程退出

3.2.2 waitpid方法

#include <sys/types.h>
#include <sys/wait.h>
// 更灵活的进程等待函数
pid_t waitpid(pid_t pid, int *status, int options);

参数详解

参数 取值 含义
pid -1 等待任意子进程,与wait等效 
pid >0 等待PID等于pid的特定子进程 

status

输出型参数 存储子进程的退出状态,不关心传NULL 
options 0 阻塞式等待(默认) 
options WNOHANG 非阻塞式等待,子进程未退出则立即返回0 

状态解析宏

   WIFEXITED(status):判断子进程是否正常退出,正常退出返回真

   WEXITSTATUS(status):若正常退出,提取子进程的退出码(仅低8位有效)

   WIFSIGNALED(status):判断子进程是否被信号杀死

   WTERMSIG(status):若被信号杀死,提取终止信号的编号

示例 1)wait 阻塞示例(最常用)

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

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

    if (pid == 0) {
        printf("子进程运行\n");
        sleep(2);
        exit(10);
    } else {
        int status;
        wait(&status); // 阻塞等子进程

        if (WIFEXITED(status)) {
            printf("子进程退出码:%d\n", WEXITSTATUS(status));
        }
    }
    return 0;
}

// 输出结果
// 子进程运行
// 子进程退出码:10

执行流程

1. 父进程调用 fork() 创建子进程。

2. 内核分裂出两个进程:父进程、子进程。

3. 子进程(pid=0):打印:子进程运行; sleep(2) 休眠2秒; exit(10) 退出,退出码=10

4. 父进程执行 wait(&status):进入阻塞等待,直到子进程结束

5. 子进程退出后,父进程被唤醒:通过WIFEXITED判断正常退出,通过WEXITSTATUS取出退出码 10

6. 父进程打印:子进程退出码:10

7. 父子进程全部结束,程序运行完成。

示例 2)waitpid 非阻塞示例(轮询)

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

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

    if (pid == 0) {
        sleep(2);
        exit(20);
    } else {
        int status;
        pid_t ret;

        // 轮询非阻塞等待
        do {
            ret = waitpid(pid, &status, WNOHANG);
            if (ret == 0) {
                printf("子进程还在跑...\n");
                sleep(1);
            }
        } while (ret == 0);

        printf("子进程退出码:%d\n", WEXITSTATUS(status));
    }
    return 0;
}

执行流程

1. 父进程 fork() 创建子进程。

2. 子进程开始运行,sleep(2) 休眠。

3. 父进程进入 do-while 循环,调用:waitpid(pid, &status, WNOHANG)

4. 子进程没退出时,waitpid 返回 0,父进程打印提示、睡1秒,继续循环。

5. 2秒后子进程退出,exit(20)。

6. 父进程再次 waitpid,返回子进程PID,循环结束。

7. 父进程通过 WIFEXITED、WEXITSTATUS 获取退出码 20 并打印。

8. 程序结束。

运行结果

子进程还在跑...
子进程还在跑...
子进程退出码:20

wait:阻塞等任意子进程,简单;waitpid:可指定PID、可非阻塞(WNOHANG)、更灵活

3.2.3 status参数的位结构

status是一个32位整数,仅低16位有效,结构如下:

15~8位(高8位) 7位 6~0位(低7位) 
正常终止:退出码 0 0
异常终止:未使用 core dump标志 终止信号编号 

正常终止:(status >> 8) & 0xFF 提取退出码

退出码存在高 8 位,你要把它右移 8 位,拉到最低位,再用 0xFF 只保留 8 位。

0xFF 就是 十进制的 255 ,二进制是:1111 1111(8 个 1)

例子:退出码 = 5    status 二进制 = 00000101 00000000

右移8位 → 00000101                   00000101 & 0xFF → 保留8bit → 5

异常终止:status & 0x7F 提取终止信号编号

信号只占低 7 位      0x7F = 二进制 01111111      只保留最后 7 位,正好就是信号编号。

系统真正规则(Linux 官方)

• 如果 低7位 = 0 → 正常退出 , 退出码 = 高8位

• 如果 低7位 ≠ 0 → 被信号杀死 , 信号编号 = 低7位

3.2.4 阻塞与非阻塞等待

1. 阻塞式等待(默认options=0)

父进程调用wait/waitpid后,会进入阻塞状态,直到子进程退出才返回,期间不执行任何代码。

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

int main()
{
    // 创建子进程
    pid_t pid = fork();

    // fork失败
    if(pid < 0) {
        perror("fork error");
        return 1;
    }
    // 子进程
    else if(pid == 0) {
        printf("child is run, pid is : %d\n", getpid());
        sleep(5);       // 子进程休眠5秒
        exit(257);      // 退出码257,只取低8位 → 1
    }
    // 父进程
    else {
        int status = 0;
        // waitpid(-1, &status, 0) = 阻塞等待任意子进程
        pid_t ret = waitpid(-1, &status, 0);
        printf("this is test for wait\n");

        // 判断是否正常退出
        // WIFEXITED(status) 正常退出→ 返回 真(非0);被信号杀死、崩溃 → 返回 假(0)
        if(WIFEXITED(status) && ret == pid) {
            // 取出退出码低8位:1
            // WEXITSTATUS(status) 所以 exit(257) → 257 % 256 = 1
            printf("wait child 5s success, child return code is :%d.\n", WEXITSTATUS(status));
        } else {
            printf("wait child failed, return.\n");
            return 1;
        }
    }
    return 0;
}

运行结果

[root@localhost linux]# ./a.out
child is run, pid is : 45110
this is test for wait
wait child 5s success, child return code is :1.

说明:父进程等待5秒后,子进程退出,父进程才继续执行,退出码257取低8位为1

2. 非阻塞式等待(WNOHANG)

父进程调用waitpid后,若子进程未退出,立即返回0,父进程可继续执行其他任务;需要通过轮询的方式反复检查子进程状态。

// 非阻塞式等待示例(带轮询+任务处理)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <vector>

using namespace std;

// 定义任务函数指针类型
typedef void (*handler_t)();
// 任务函数数组
vector<handler_t> handlers;

// 临时任务1
void fun_one()
{
    printf("这是一个临时任务1\n");
}

// 临时任务2
void fun_two()
{
    printf("这是一个临时任务2\n");
}

// 加载任务到数组
void Load()
{
    handlers.push_back(fun_one);
    handlers.push_back(fun_two);
}

// 执行所有任务
void handler()
{
    if(handlers.empty())
        Load();
    for (auto iter : handlers)
        iter();
}

int main()
{
    pid_t pid = fork();
    if (pid < 0)
    {
        printf("%s fork error\n", __FUNCTION__);
        return 1;
    }
    else if (pid == 0) // 子进程
    {
        printf("child is run, pid is : %d\n", getpid());
        sleep(5); // 子进程运行5秒后退出
        exit(1);
    }
    else // 父进程
    {
        int status = 0;
        pid_t ret = 0;
        // 轮询非阻塞等待
        do
        {
            // 非阻塞等待,子进程未退出则返回0
            ret = waitpid(-1, &status, WNOHANG);
            if (ret == 0)
            {
                // 子进程还在运行,父进程执行自己的任务
                printf("child is running\n");
                handler();
            }
            // 每隔1秒检查一次,避免CPU空转
            sleep(1);
        } while (ret == 0);

        // 子进程退出,检查状态
        if (WIFEXITED(status) && ret == pid)
        {
            printf("wait child 5s success, child return code is :%d.\n", WEXITSTATUS(status));
        }
        else
        {
            printf("wait child failed, return.\n");
            return 1;
        }
    }
    return 0;
}

运行结果

child is running
这是一个临时任务1
这是一个临时任务2
child is running
这是一个临时任务1
这是一个临时任务2
...(重复5次,子进程运行5秒)
wait child 5s success, child return code is :1.

说明:父进程在等待子进程的同时,循环执行自己的任务,实现了并发处理

4. 进程程序替换

4.1 替换原理

fork创建的子进程默认执行父进程的代码,若要让子进程执行全新的程序,需要调用exec系列函数:

   核心逻辑:将调用进程的用户空间代码和数据,完全替换为磁盘上新程序的代码和数据,从新程序的main函数开始执行

   关键特性:不创建新进程,进程ID(PID)保持不变,仅替换地址空间内容

图解说明

   替换前:进程的虚拟内存中是原程序的代码段、数据段、堆、栈

   替换后:虚拟内存被新程序的代码和数据覆盖,页表重新映射到新程序的物理内存,PCB保持不变

4.2 替换函数(exec函数族)

Linux提供6个以exec开头的函数,统称exec函数族,最终都调用系统调用execve。

#include <unistd.h>

// 列表传参,不自动搜索PATH,使用当前环境变量
int execl(const char *path, const char *arg, ...);
// 列表传参,自动搜索PATH,使用当前环境变量
int execlp(const char *file, const char *arg, ...);
// 列表传参,不自动搜索PATH,自定义环境变量
int execle(const char *path, const char *arg, ..., char *const envp[]);

// 数组传参,不自动搜索PATH,使用当前环境变量
int execv(const char *path, char *const argv[]);
// 数组传参,自动搜索PATH,使用当前环境变量
int execvp(const char *file, char *const argv[]);
// 数组传参,不自动搜索PATH,自定义环境变量(真正的系统调用)
int execve(const char *path, char *const argv[], char *const envp[]);

4.2.1 函数命名规律

后缀 含义
l (list) 参数以可变参数列表传递,最后以NULL结尾 
v (vector) 参数以字符串数组传递,数组最后以NULL结尾 
p (path) 自动搜索系统PATH环境变量,可只写程序名 
e (env) 自定义环境变量数组,不使用当前进程的环境变量 

函数对比表

函数名 参数格式 是否带路径 是否使用当前环境变量
execl 列表 否(需写全路径)
execlp 列表 是(自动搜PATH)
execle 列表 否(需写全路径) 否(需自定义环境变量) 
execv 数组 否(需写全路径)
execvp 数组 是(自动搜PATH)
execve 数组 否(需写全路径) 否(需自定义环境变量) 

4.2.2 函数调用示例

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    // 1. execl:列表传参,全路径,当前环境
    char *const argv[] = {"ps", "-ef", NULL};
    char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
    
    execl("/bin/ps", "ps", "-ef", NULL);
    // 2. execlp:列表传参,自动搜PATH,当前环境
    execlp("ps", "ps", "-ef", NULL);
    // 3. execle:列表传参,全路径,自定义环境
    execle("/bin/ps", "ps", "-ef", NULL, envp);
    // 4. execv:数组传参,全路径,当前环境
    execv("/bin/ps", argv);
    // 5. execvp:数组传参,自动搜PATH,当前环境
    execvp("ps", argv);
    // 6. execve:数组传参,全路径,自定义环境
    execve("/bin/ps", argv, envp);

    // 程序替换成功后,后续代码不会执行
    perror("exec");
    exit(1);
}

关键说明

• 所有exec函数只有出错返回-1,成功则无返回值(因为原程序代码被完全替换,后续代码不会执行)

• execve是唯一的系统调用,其他5个函数都是标准库封装,最终都会调用execve

5. 自主Shell命令行解释器(微型Shell)

5.1 实现原理

Shell的核心执行流程:

1) 获取命令行:读取用户输入的命令(如ls -l)

2) 解析命令行:将命令字符串分割为参数数组(argv)

3) 建立子进程:调用fork创建子进程

4) 程序替换:子进程调用execvp执行目标命令

5) 父进程等待:父进程调用waitpid等待子进程退出,回收资源

6) 循环执行:回到步骤1,等待下一条命令

5.2 完整源码

整体结构

1. 打印提示符 [user@host dir]$

2. 读取你输入的命令

3. 把命令切成一段段参数(ls -l → 切成 ls 和 -l)

4. 判断是不是内建命令(cd/export/env/echo/$?)

5. 不是内建 → 创建子进程 + 程序替换执行

6. 循环一直跑

#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;

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

// 全局环境变量数组:存储Shell的环境变量
char *genv[envnum];

// 全局路径相关:当前工作路径
char pwd[basesize];
char pwdenv[basesize];

// 宏定义:去除字符串开头的空格
#define TrimSpace(pos) do{\
    while(isspace(*(pos))){\
        (pos)++;\
    }\
}while(0)

// -------------------------- 工具函数 --------------------------
// 获取当前用户名(从环境变量USER获取)
string GetUserName()
{
    string name = getenv("USER");
    return name.empty() ? "None" : name;
}

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

// 获取当前工作路径,并更新环境变量PWD
string GetPwd()
{
    char pwd_buf[basesize];
    // getcwd获取当前工作目录,失败返回NULL
    if(nullptr == getcwd(pwd_buf, sizeof(pwd_buf))) 
        return "None";
    // 构造PWD=xxx格式的环境变量
    snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd_buf);
    // 更新环境变量
    putenv(pwdenv);
    return pwd_buf;
}

// 获取上一级目录(用于命令行提示符显示)
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); // /home/xxx → 只显示 xxx
}

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

// -------------------------- 核心流程函数 --------------------------
// 1. 打印命令行提示符
void PrintCommandLine()
{
    printf("%s", MakeCommandLine().c_str());
    fflush(stdout); // 强制刷新缓冲区,确保提示符立即显示
}

// 2. 获取用户输入的命令行
bool GetCommandLine(char command_buffer[], int size)
{
    // 从标准输入读取一行命令,fgets会包含换行符\n
    char *result = fgets(command_buffer, size, stdin);
    if(!result) 
        return false;
    
    // 将换行符替换为\0,去除末尾换行
    command_buffer[strlen(command_buffer)-1] = 0;
    // 处理空输入(用户直接按回车)
    if(strlen(command_buffer) == 0) 
        return false;
    return true;
}

// 3. 解析命令行,分割为参数数组
void ParseCommandLine(char command_buffer[], int len)
{
    (void)len; // 没用上,防止编译器报未使用变量警告
    // 清空上次的命令参数
    memset(gargv, 0, sizeof(gargv));
    gargc = 0;

    // 以空格为分隔符,分割命令行
    const char *sep = " ";
    // strtok分割字符串,第一次传原字符串,后续传NULL
    gargv[gargc++] = strtok(command_buffer, sep);
    // 继续分割,知道strtok返回NULL
    while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
    gargc--; // 修正计数,strtok最后返回NULL,需要回退
}

// 调试函数:打印解析后的命令参数
void debug()
{
    printf("argc: %d\n", gargc);
    for(int i = 0; gargv[i]; i++)
    {
        printf("argv[%d]: %s\n", i, gargv[i]);
    }
}

// -------------------------- 内建命令处理 --------------------------
// 向环境变量数组添加新的环境变量
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;
}

// 检查并执行内建命令(cd、export、env、echo等)
bool CheckAndExecBuiltCommand()
{
    // 1. cd命令:切换工作目录(内建命令,必须Shell自己执行)
    if(strcmp(gargv[0], "cd") == 0)
    {
        // cd命令必须带参数,否则报错
        if(gargc == 2)
        {
            chdir(gargv[1]); // 系统调用,切换当前工作目录
            lastcode = 0;
        }
        else
        {
            lastcode = 1;
        }
        return true;
    }
    // 2. export命令:添加环境变量(内建命令)
    else if(strcmp(gargv[0], "export") == 0)
    {
        if(gargc == 2)
        {
            AddEnv(gargv[1]);
            lastcode = 0;
        }
        else
        {
            lastcode = 2;
        }
        return true;
    }
    // 3. env命令:打印所有环境变量(内建命令)
    else if(strcmp(gargv[0], "env") == 0)
    {
        for(int i = 0; genv[i]; i++)
        {
            printf("%s\n", genv[i]);
        }
        lastcode = 0;
        return true;
    }
    // 4. echo命令:打印内容,支持echo $?(打印上一条命令退出码)
    else if(strcmp(gargv[0], "echo") == 0)
    {
        if(gargc >= 2)
        {
            // 处理echo $?
            if(gargv[1][0] == '$')
            {
                if(gargv[1][1] == '?')
                {
                    printf("%d\n", lastcode);
                    lastcode = 0;
                }
            }
            else
            {
                // 普通echo,打印参数 echo XXX -> 打印 XXX
                printf("%s\n", gargv[1]);
                lastcode = 0;
            }
        }
        else
        {
            lastcode = 3;
        }
        return true;
    }

    // 不是内建命令,返回false,交给子进程执行
    return false;
}

// -------------------------- 外部命令执行 --------------------------
// 执行外部命令:fork子进程+execvp替换+waitpid等待
bool ExecuteCommand()
{
    pid_t id = fork();
    if(id < 0) 
        return false;
    if(id == 0) // 子进程:执行目标命令
    {
        // 程序替换,执行命令
        execvp(gargv[0], gargv);
        // 替换失败,直接退出
        exit(1);
    }

    // 父进程:阻塞式等待子进程退出
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        // 子进程正常退出,更新lastcode为退出码
        if(WIFEXITED(status))
        {
            lastcode = WEXITSTATUS(status);
        }
        // 子进程异常退出,设置lastcode为100(自定义错误码)
        else
        {
            lastcode = 100;
        }
    }
    return true;
}

// -------------------------- 环境变量初始化 --------------------------
// 从父进程(系统Shell)继承环境变量,初始化Shell的环境变量数组
void InitEnv()
{
    // 系统全局环境变量指针,extern声明即可使用
    extern char **environ;
    int index = 0;
    // 拷贝父进程的环境变量到genv数组
    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()
{
    // 1. 初始化环境变量
    InitEnv();
    // 2. 命令缓冲区
    char command_buffer[basesize];

    // 3. Shell主循环:一直运行,直到用户输入exit或Ctrl+C
    while(true)
    {
        // 3.1 打印命令行提示符
        PrintCommandLine();
        // 3.2 获取用户输入的命令
        if( !GetCommandLine(command_buffer, basesize) )
        {
            continue;
        }
        // 3.3 解析命令行
        ParseCommandLine(command_buffer, strlen(command_buffer));
        // debug(); // 调试用,打印解析后的参数

        // 3.4 检查是否为内建命令,是则直接执行
        if ( CheckAndExecBuiltCommand() )
        {
            continue;
        }

        // 3.5 执行外部命令
        ExecuteCommand();
    }

    return 0;
}

这是一个 C++ 写的迷你 Linux Shell(命令行终端)
实现了:ls、cd、pwd、echo、export、env、$? 等基础功能,原理 = 系统自带的 bash 简化版。

逐段超简解释

1. 全局变量

char *gargv[64];   // 存切好的命令
int lastcode;      // 存上一条命令退出码(给 echo $? 用)
char *genv[64];    // 存环境变量
char pwd[1024];    // 当前路径

2. 工具函数

GetUserName()      拿环境变量 USER → 你的用户名

GetHostName()      拿环境变量 HOSTNAME → 主机名

GetPwd()      调用系统函数 getcwd(),拿到当前路径,并更新环境变量 PWD

LastDir()      只显示最后一级目录(比如 /home/aaa → 显示 aaa)

MakeCommandLine()      拼成提示符:[user@host 目录]$

3. 主流程三大步

PrintCommandLine()      打印提示符

GetCommandLine()      读你输入的一行命令,把最后的 \n 换成 \0

ParseCommandLine()      用 strtok 按空格切割命令

ls -a -l
→ gargv[0] = "ls"
→ gargv[1] = "-a"
→ gargv[2] = "-l"

4. 内建命令(重点!)

内建命令 = 不能创建子进程,必须 shell 自己干

cd      chdir(gargv[1]);      系统调用,切换目录

export      把 name=value 放进环境变量数组

env      打印所有环境变量

echo      • echo hello → 打印 hello      • echo $? → 打印上一条命令退出码 lastcode

5. 外部命令执行(最核心)

pid_t id = fork();      创建子进程

子进程      execvp(gargv[0], gargv);      程序替换,变成你要执行的命令(ls、pwd、ps 等)

父进程      waitpid(id, &status, 0);      等着子进程跑完,拿到退出码给 echo $? 使用

6. 环境变量初始化

extern char **environ;      把系统环境变量全部拷进自己的 genv 数组,这样 env、export 才能用

7. 主循环

while(true)
{
    打印提示符
    读命令
    切命令
    内建?执行
    否则:fork + exec 执行
}

一直跑,直到你 Ctrl+C

这个 Shell 实现了什么?

• 自定义命令提示符 [user@host dir]$

• 读取、解析命令行

• 内建命令:cd、export、env、echo、$?

• 外部命令:ls、pwd、ps、mkdir 等所有系统命令

• 子进程创建 + 程序替换 + 进程等待

• 环境变量管理

• 记录上一条命令退出码($?)

5.3 代码核心逻辑说明

1. 内建命令 vs 外部命令

   内建命令:如cd、export、env、echo,必须由Shell自己执行(因为需要修改Shell自身的环境变量、工作目录,子进程执行无法影响父进程)

   外部命令:如ls、ps、cat,由子进程执行,父进程等待回收

2. 环境变量处理

   InitEnv从系统Shell继承环境变量,存储在genv数组中

   export命令可添加新环境变量,env命令打印所有环境变量

   PWD环境变量实时更新,保证cd后提示符显示正确路径

3. 退出码处理

   lastcode全局变量存储上一条命令的退出码;echo $?可打印该值,符合标准Shell的行为

4. 命令行提示符

   格式:[用户名@主机名 最后一级目录]$;实时更新当前工作目录,符合用户使用习惯

6. 函数与进程的相似性总结

函数调用 进程调用
call fork+exec 

return

exit
函数栈帧 进程地址空间 
函数参数 命令行参数
函数返回值 进程退出码
wait获取返回值 wait/waitpid获取退出码 

核心类比:函数是程序内的调用,进程是程序间的调用,fork+exec对应函数call,exit对应函数return,wait对应获取返回值,完美体现了Linux的设计哲学。

7. 关键知识点速查

操作 核心函数 关键特性
进程创建 fork 写时拷贝,父子进程分离,两个返回值 
进程终止 exit/_exit/return exit做清理,_exit直接终止,return等价exit 
进程等待 wait/waitpid 阻塞/非阻塞,回收僵尸进程,获取退出状态 
进程替换 exec函数族 不创建新进程,PID不变,替换地址空间 
Shell实现 fork+execvp+waitpid 循环读取命令,内建/外部命令分离 

以上就是Linux进程控制全套核心知识点与代码实现,吃透这些内容,足以应对日常开发、作业练习与基础面试。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐