本文是小编巩固自身而作,如有错误,欢迎指出!
目录
[exec 系列程序替换总结](#exec 系列程序替换总结)
[1. 本质是什么](#1. 本质是什么)
[2. 核心作用](#2. 核心作用)
[3. 6 个函数统一规律](#3. 6 个函数统一规律)
一、进程替换
进程替换的核心原理
进程替换 (Process Replacement),在类 Unix 系统(如 Linux)中,指一个进程不创建新进程、不改变 PID ,完全替换自身的代码段、数据段、堆栈 ,转而执行另一个全新程序的机制。核心是 "换程序,不换进程"。
进程 = 内核结构(PCB、PID、文件描述符) + 用户空间(代码、数据、堆、栈)
- 保留:PID、PPID、进程控制块(PCB)、打开的文件描述符、当前工作目录、用户 ID 等。
- 替换:清空原用户空间,加载新程序的代码、数据、BSS 段,重建页表与内存映射。
- 执行 :成功后从新程序入口(
_start→main)开始执行,原进程后续代码不再执行。 - 返回 :成功不返回 ;失败返回 -1 并设置
errno。

exec系列函数

我们在系统自带的man手册中可以看到exec相关函数族的用法,下面我们详细解释.
execl
cpp
#include <unistd.h>
// 原型
int execl(const char *path, const char *arg, ... /* NULL */);
-
path- 要执行的程序完整路径
- 例如:
/bin/ls、/usr/bin/python3 - 不能只写
ls,必须写全路径
-
arg(以及后面所有 ...)- 传给新程序的命令行参数
- 第一个 arg = 程序自己的名字 (
argv[0]) - 后面是真正的参数
- 最后必须写 NULL 表示结束
cpp
#include <stdio.h>
#include <unistd.h>
int main() {
printf("我是原来的程序,即将替换成 ls...\n");
// 程序替换:把当前进程换成 /bin/ls
execl("/bin/ls", "ls", "-l", NULL);
// 下面这句话 **只有 execl 失败时才会打印**
perror("execl 失败");
return 1;
}

execlp
cpp
int execlp(const char *file, const char *arg, ... /* NULL */);
-
file- 程序文件名,不用写路径
- 系统会自动去
PATH里找 - 写
ls就行,不用/bin/ls
-
arg...- 和 execl 完全一样
- 第一个是程序名
- 最后必须 NULL
execv
cpp
int execv(const char *path, char *const argv[]);
-
path- 程序完整路径,同 execl
-
argv[]-
字符串指针数组
-
格式:
cppargv[0] = 程序名 argv[1] = 参数1 argv[2] = 参数2 ... 最后一个 = NULLcpp#include <stdio.h> #include <unistd.h> int main() { // 构造参数数组:必须以 NULL 结尾 char *argv[] = { "ls", // argv[0] "-l", // argv[1] NULL // 结束标志 }; printf("准备替换成 ls...\n"); // 执行程序替换 execv("/bin/ls", argv); // 失败才会走到这里 perror("execv failed"); return 1; }
-
execle
cpp
int execle(const char *path,
const char *arg, ...,
NULL,
char *const envp[]);
-
path- 要执行程序的完整路径 ,如
/bin/ls - 不会自动搜索 PATH
- 要执行程序的完整路径 ,如
-
arg, ...- 命令行参数列表
- 第一个 arg =
argv[0](程序名) - 后面是参数
- 必须以
NULL结束参数列表
-
envp[]-
自定义环境变量数组
-
格式:
"VAR=VALUE" -
数组最后一项必须是
NULL -
新进程只使用这些环境变量 ,不继承父进程
cpp#include <stdio.h> #include <unistd.h> int main() { // 自定义环境变量:必须以 NULL 结尾 char *env[] = { "NAME=test_exec", "AGE=22", "PATH=/bin", // 保证能找到系统命令 NULL }; printf("execle 替换程序...\n"); // execle(路径, argv[0], 参数..., NULL, 环境变量数组); execle( "/bin/env", // 要执行的程序 "env", // argv[0] NULL, // 参数结束 env // 自定义环境变量 ); perror("execle 失败"); return 1; }
-

execvpe
cpp
int execvpe(const char *file,
char *const argv[],
char *const envp[]);
-
file- 程序名,如
ls - 带 p → 会自动在 PATH 中搜索
- 程序名,如
-
argv[]- 参数数组
argv[0]= 程序名- 最后一项必须是
NULL
-
envp[]- 自定义环境变量数组
- 格式同上
- 结尾必须
NULL
- l:参数用列表(逗号分隔)
- v:参数用数组
- p:自动搜 PATH
- e:自定义环境变量(不继承

exec 系列程序替换总结
1. 本质是什么
- 进程不变,程序换掉
- PID 不变、文件描述符不变、进程身份不变
- 把当前进程的代码、数据、堆栈全部替换成新程序
- 成功不返回,失败返回 -1
2. 核心作用
让一个进程运行另一个程序 ,最经典搭配:fork() 创建子进程 → exec 替换成新程序这就是 shell 执行命令、系统启动程序的底层原理。
3. 6 个函数统一规律
- l :参数用列表,逗号分隔,结尾 NULL
- v :参数用数组,argv [],结尾 NULL
- p :根据 PATH 搜索命令,不用写全路径
- e :使用自定义环境变量,不继承父进程
二、自主myshell的实现
1.交互界面,命令行的复现
我们可以看到,在我们xshell的界面,是这样的命令行
他们都是这样的格式
bash
[\u@\h \W]\$
\u:用户名\h:短主机名\W:当前工作目录的基名(仅显示最后一级目录)\$:普通用户 /root 提示符切换
那我们要复现出这样的提示符,首先就得包含上述内容
cpp
#include <stdio.h>
#include <stdlib.h>
char* getusrname()
{
return getenv("USER");
}
char* gethostname()
{
return getenv("HOSTNAME");
}
char* getpwd()
{
return getenv("PWD");
}
int main()
{
printf("[%s@%s %s]# \n", getusrname(), gethostname(), getpwd());
return 0;
}

可以看到我们的提示符已经实现,那么我们要怎么实现一个可以输入命令的命令行呢?
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024
char commandline[LINE_SIZE];
char* getusrname()
{
return getenv("USER");
}
char* gethostname()
{
return getenv("HOSTNAME");
}
char* getpwd()
{
return getenv("PWD");
}
void interact(char* cline, int size)
{
printf(FORMATE, getusrname(), gethostname(), getpwd());
fflush(stdout);
fgets(cline, size, stdin);
// 删掉换行符(必须保留!)
cline[strcspn(cline, "\n")] = '\0';
// 测试:把输入的命令打印出来
printf("%s\n", cline);
}
int main()
{
while(1)
{
interact(commandline, sizeof(commandline));
}
return 0;
}

在上述代码中,重点在于以下几点:
1. 死循环 while (1):
这就是Shell 能一直让你输命令的原因!
作用:
- 输完一条命令 → 循环回来
- 再显示提示符 → 再输
- 再执行 → 再回来
- 永远不退出
类比:
你用的终端是不是输完 ls 还能输 pwd ?就是因为里面有个 while (1) 死循环。
2. 为什么要去掉换行符?
cpp
cline[strcspn(cline, "\n")] = '\0';
原因只有一句话:
fgets 会把你按的回车键(Enter)一起读进来!
\n = 换行符,不是命令的一部分!
不删会怎样?
- 执行
ls会变成ls\n - 系统找不到这个命令
- 直接报错,无法运行
3. fgets 是什么?怎么用?
cpp
fgets(cline, size, stdin);
cline:读到哪里去(存命令的数组)size:最多读多少字符stdin:从键盘读(标准输入)
- 读到回车就停止
- 会把回车 \n 一起读进去(所以要删)
- 安全、不会越界
2.字串的分隔问题,解析命令行
上一个模块,我们可以将用户的输入写入到一个字符数组commandline中了,那么接下来我们就要解析一下用户的输入,如果用户的输入带选项的指令,那么选项和指令之间,选项和选项之间都是以空格为分隔符,例如ls -a -l,所以我们应该按照空格为分隔符进行分隔用户的输入,即字符数组commandline
而为了实现切割的目的,我们使用c语言的strtok
strtok的使用
cpp
char *strtok(char *str, const char *delim);
char *str
要被分割的字符串
const char *delim
分隔符(你想按什么切)
可以是:
- 单个字符:
" "(空格) - 多个字符:
" ;\t\n"(空格、分号、制表符、换行)
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024
char commandline[LINE_SIZE];
// 用来存放分割后的命令和参数
char *args[64];
char* getusrname()
{
return getenv("USER");
}
char* gethostname()
{
return getenv("HOSTNAME");
}
char* getpwd()
{
return getenv("PWD");
}
// 新增:用 strtok 分割命令行
void parse_command()
{
int i = 0;
// 第一次分割:传原字符串
args[i] = strtok(commandline, " ");
// 继续分割,直到分割完毕
while(args[i] != NULL)
{
i++;
args[i] = strtok(NULL, " ");
}
}
void interact(char* cline, int size)
{
printf(FORMATE, getusrname(), gethostname(), getpwd());
fflush(stdout);
fgets(cline, size, stdin);
// 去掉换行符
cline[strcspn(cline, "\n")] = '\0';
// 测试打印你输入的内容
printf("%s\n", cline);
}
int main()
{
while(1)
{
// 1. 交互获取命令
interact(commandline, sizeof(commandline));
// 2. 使用 strtok 解析命令
parse_command();
// 测试:打印分割后的结果
printf("--- 分割后 ---\n");
for(int i=0; args[i]!=NULL; i++)
{
printf("args[%d] = %s\n", i, args[i]);
}
}
return 0;
}

3.常规命令的执行
我们上两个模块我们已经可以接收用户输入,将用户输入的字符串解析出来,那么接下来就是根据解析出来的命令和选项去执行命令了,对于普通命令,是由bash创建子进程,子进程去执行普通命令,由于我们有命令就是argv[0],但是我们没有路径,我们有命令行参数argv,子进程进行程序替换execvp即可
cpp
// 执行命令(核心!)
void execute_cmd()
{
// 空命令直接跳过
if(args[0] == NULL)
return;
pid_t pid = fork(); // 创建子进程
if(pid == 0)
{
// 子进程:执行命令
execvp(args[0], args);
// 如果 execvp 执行失败,才会走到这里
perror("exec error");
exit(1);
}
else if(pid > 0)
{
// 父进程:等待子进程结束
wait(NULL);
}
else
{
// 创建进程失败
perror("fork error");
}
整体思路就是通过我们的输入,把输入分割成一个个块,然后通过execvp在系统中环境变量中寻找相关命令。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024
char commandline[LINE_SIZE];
char *args[64];
char* getusername()
{
return getenv("USER");
}
char* gethost()
{
return getenv("HOSTNAME");
}
char* getmypwd()
{
return getenv("PWD");
}
void interact(char* cline, int size)
{
printf(FORMATE, getusername(), gethost(), getmypwd());
fflush(stdout);
fgets(cline, size, stdin);
cline[strcspn(cline, "\n")] = '\0';
}
void parse_command()
{
int i = 0;
args[i] = strtok(commandline, " ");
while(args[i] != NULL)
{
i++;
args[i] = strtok(NULL, " ");
}
}
void execute_cmd()
{
if(args[0] == NULL)
return;
pid_t pid = fork();
if(pid == 0)
{
execvp(args[0], args);
perror("exec error");
exit(1);
}
else if(pid > 0)
{
wait(NULL);
}
else
{
perror("fork error");
}
}
int main()
{
while(1)
{
interact(commandline, sizeof(commandline));
// 空输入跳过
if(strlen(commandline) == 0)
continue;
parse_command();
execute_cmd();
return 0;
}

4.内建命令
在上述学习中,我们已经了解自主实现的shell要运用系统本身自带的命令该怎么做,现在我们看看怎么在自主实现的shell中实现。我们现在就以自建命令cd为例子
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024
char commandline[LINE_SIZE];
char *args[64];
char* getusername()
{
return getenv("USER");
}
char* gethost()
{
return getenv("HOSTNAME");
}
char* getmypwd()
{
return getenv("PWD");
}
void interact(char* cline, int size)
{
printf(FORMATE, getusername(), gethost(), getmypwd());
fflush(stdout);
fgets(cline, size, stdin);
cline[strcspn(cline, "\n")] = '\0';
}
void parse_command()
{
int i = 0;
args[i] = strtok(commandline, " ");
while(args[i] != NULL)
{
i++;
args[i] = strtok(NULL, " ");
}
}
// ======================
// 仅实现 cd 内建命令
// ======================
int builtin_cd()
{
// 如果不是 cd,返回 0,继续执行系统命令
if (strcmp(args[0], "cd") != 0)
return 0;
// 是 cd,自己执行
if (args[1] == NULL)
{
// 没参数 → 回到家目录
chdir(getenv("HOME"));
}
else
{
// 有参数 → 切换到目标目录
chdir(args[1]);
}
// 更新 PWD 环境变量,让提示符路径刷新
char buf[1024];
getcwd(buf, sizeof(buf));
setenv("PWD", buf, 1);
return 1;
}
void execute_cmd()
{
if(args[0] == NULL)
return;
pid_t pid = fork();
if(pid == 0)
{
execvp(args[0], args);
perror("exec error");
exit(1);
}
else if(pid > 0)
{
wait(NULL);
}
}
int main()
{
while(1)
{
interact(commandline, sizeof(commandline));
if(strlen(commandline) == 0) continue;
parse_command();
// 如果是 cd,执行完直接跳过系统命令
if(builtin_cd())
continue;
// 其他命令正常执行
execute_cmd();
}
return 0;
}
- cd 是内建命令,必须在 Shell 进程自身执行。
- 通过 strcmp 判断命令是否为 cd。
- 使用 chdir () 系统调用切换目录。
- 处理无参数(回到家目录)和有参数两种情况。
- 更新 PWD 环境变量,保证提示符路径刷新。
- cd 执行完毕后,不再进入 fork/exec 流程

本次分享就到这里结束了,后续会继续更新,感谢阅读!