Linux 进程控制
本文系统梳理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进程控制全套核心知识点与代码实现,吃透这些内容,足以应对日常开发、作业练习与基础面试。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)