代码演示fork + exec 整套机制
父进程先创建子进程;子进程用 exec 加载另外一个程序(otherExe),把参数、环境变量传给它;父进程用 waitpid 等子进程结束、回收资源。 这就是你的 myshell 执行外部命令的底层原版模型!
分成 4 大板块来讲: 板块 1:全局环境变量准备 板块 2:fork 创建子进程,分出父子两条执行流 板块 3:子进程区域(exec 加载新程序,重点) 板块 4:父进程区域(waitpid 等待回收)
配套小提醒: otherExe 是另外一个 C++ 程序,作用:接收参数、接收环境变量并打印出来,用来观察 exec 传参效果。
最后配上完整带详细注释代码
板块1:全局环境变量准备
extern char **environ;
putenv("PRIVATE_ENV=666666");
extern char **environ;``environ是操作系统给每个进程自带的全局环境变量表。extern意思:这个变量不在当前文件定义,在系统库里面,我们只是声明拿来用。 shell 里面执行外部命令,默认就是把这个 environ 传给子进程。putenv("PRIVATE_ENV=666666");给当前这个进程新增一条环境变量PRIVATE_ENV=666666。 fork 之后子进程默认继承父进程的环境变量; 如果 exec 不指定自定义环境,子进程加载出来的程序也能读到这条变量。
对应你myshell:
export本质就是类似 setenv/putenv 修改 environ。
板块2:fork 创建子进程,分流父子进程
pid_t id = fork();
if(id == 0)
{
// 子进程代码
}
// 下面是父进程代码
fork():系统调用,复制当前进程,生成子进程。- 返回值规则:
- 返回 >0:当前是父进程,返回值就是子进程pid
- 返回 0:当前是子进程
- 返回 <0:fork失败
fork之后,父子进程代码完全一样,但是从fork这一行之后分开跑。
重点:fork 只是复制进程,不会执行新程序,想要跑 ls、otherExe,必须靠 exec。
板块3:if(id == 0) 里面 —— 子进程板块(核心!exec)
printf("before: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
getpid():自己的pid getppid():父进程的pid 这一句是 exec 之前打印。 👉 只要 exec 成功,后面所有C代码直接被新程序覆盖,再也不会跑!
下面一堆注释的exec函数(6兄弟,myshell只用到execvp)
//execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
//execlp("ls", "ls", "-a", "-l", NULL);
//execl("/usr/bin/top", "top", NULL);
//execv("/usr/bin/ls", myargv);
//execvp("ls", myargv);
字母口诀:
- l(list):参数一个个写出来,逗号隔开,末尾NULL
- v(vector):参数放到 char* 数组里面传
- p(path):自动去PATH里面找程序,不用写完整路径
- e(env):自己指定一套环境变量,不继承父进程
| 函数 | 特点 |
|---|---|
| execl | 全路径 + 罗列参数 |
| execlp | 名字搜索PATH + 罗列参数 |
| execv | 全路径 + 参数数组 |
| execvp | 名字搜索PATH + 参数数组 ✅ 你的myshell用它 |
char* const myargv[] = {
"otherExe",
"-a",
"-b",
"-c",
NULL
};
构造命令参数数组,和你splitstring切出来的argv一模一样! 最后必须放 NULL,告诉exec:参数到此结束。
char *const myenv[] = {
"MYVAL=1111",
"MYPATH=/usr/bin/XXX",
NULL
};
自定义环境数组,同样NULL结尾。
不写这个,exec默认继承父进程的 environ。 写了这个,execle / execve 就只用这一套,父的环境全部丢掉。
//execle("./otherExe", "otherExe", "-a", "-w", "-v", NULL, myenv);
//execve("./otherExe", myargv, environ);
//execl("/usr/bin/bash", "bash", "test.sh", NULL);
//execl("/usr/bin/python3", "python3", "test.py", NULL);
-
execle:罗列参数 + 自定义环境 -
execve:数组传参 + 自定义环境,6个exec里面最底层的系统调用,别的exec都是它的包装 -
后面两个演示:exec可以调用bash跑shell脚本、调用python跑py脚本
这就是注释那句话:不管什么语言,跑起来都是进程,exec只管加载可执行文件
printf("after: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
exit(1);
⚠️ 超级重要: 只有exec调用失败的时候,这行printf才会打印! exec成功:当前进程的代码、数据全部被新程序(otherExe)替换,这行代码直接消失,不会执行。 exec失败(文件不存在、权限不够),才往下走,打印提示,然后exit(1)退出子进程,退出码1。
对应你myshell:execvp失败,我们设置退出码44,就是这个逻辑。
板块4:if外面 —— 父进程板块(waitpid回收)
pid_t ret = waitpid(id, NULL, 0);
if(ret > 0) printf("wait success, father pid: %d, ret id: %d\n", getpid(), ret);
sleep(5);
return 0;
waitpid(子进程pid, 状态指针, 选项)
- 第二个参数写NULL:不关心子进程退出码(myshell里我们会传变量,拿到$?)
- 第三个参数0:阻塞等待,父进程卡在这,一直等到子进程退出才往下走
- 返回值ret>0:成功等到子进程,ret就是子进程pid
作用: 子进程退出之后内核会暂时保留它的退出信息,如果父进程不wait,就变成僵尸进程。myshell必须waitpid回收!
sleep(5); 父进程休眠5秒,方便你用 ps aux 命令观察父子进程状态,属于调试用,实际myshell不需要。
otherExe.cpp(被exec加载的程序,配套板块)
int main(int argc, char *argv[], char *env[])
main三参数版本:
- argc:参数个数
- argv:命令行参数数组(就是exec传进来的myargv)
- env:环境变量数组(exec传进来的环境)
循环打印argv,循环打印env,用来直观看到: exec真的把参数和环境变量完整传给新程序了!
运行流程完整走一遍(打开 execve 那一行注释)
- 父进程启动,putenv增加环境变量
- fork,产生子进程
- 子进程先打印before
- execve 加载 ./otherExe,把子进程的代码全部替换成otherExe
- otherExe 运行,打印收到的参数、环境变量
- otherExe执行完毕,子进程退出
- 父进程waitpid收到子进程退出,打印wait success
- 父进程sleep5秒,程序结束
详细讲解execvp / execlp 里的 p,全称 PATH。
p 到底干什么
一句话:带 p 的函数,会自动去 PATH 环境变量里面搜索这个程序,不用你写完整绝对路径。
对比两组,一眼看懂
不带p(execv / execl)
第一个参数必须是完整路径,内核直接按这个路径去磁盘找文件:
execv("/usr/bin/ls", argv);
execl("/usr/bin/ls", "ls", "-l", NULL);
你写 ls 不行,会直接执行失败,找不到文件。
带p(execvp / execlp)
第一个参数只写程序名字就行:
execvp("ls", argv);
execlp("ls", "ls", "-l", NULL);
它内部做的事情:
- 拿到环境变量
PATH,PATH 是一串目录,用冒号隔开,类似:
PATH=/usr/bin:/bin:/usr/local/bin
PATH 里面就是放了好多个目录路径,用冒号 : 隔开。
冒号 : 只是分隔符,用来告诉系统:这里是不同的文件夹。
2. 把名字依次拼到每一个目录后面: /usr/bin/ls → 存在吗?存在,就用这个路径调用底层的 execve
3. 找到第一个存在、可执行的文件,就执行它
4. 全部目录找完都没有 → execvp 返回失败
底层本质:p只是帮你拼接路径查找,找到之后,内部最终还是调用 execve
重点!两个坑(非常容易错,写myshell必踩)
坑1:如果你给的名字里面带 /,p 直接失效,不再搜PATH
execvp("./otherExe", myargv);
里面有 /,execvp 判定这是路径,不会去PATH里面搜,直接当成路径传给execve。 和 execve("./otherExe", myargv, environ) 行为一样。
规则:参数包含
/→ 跳过PATH搜索,直接当成路径使用。
坑2:搜索只看PATH,不看你当前目录
默认PATH里面没有.(当前目录),所以:
execvp("otherExe", myargv);
就算当前目录有 otherExe,也找不到! 想要执行当前目录的程序,必须写:
execvp("./otherExe", myargv);
因为带了 /,不再走PATH搜索。
(exec家族演示,C语言)
文件名:shell_code.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
// 声明全局环境变量表,系统自带,extern代表这个变量在别的地方定义好了,我们拿来用
extern char **environ;
// putenv:往当前进程的环境变量里新增一条环境变量
// 格式:KEY=VALUE,字符串形式
putenv("PRIVATE_ENV=666666");
// fork() 创建子进程,返回值是pid_t(进程id类型,本质int)
pid_t id = fork();
// id == 0 代表【子进程】
if(id == 0)
{
// getpid():获取自己的进程ID
// getppid():获取父进程的ID
printf("before: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
// sleep(5); 让子进程休眠5秒,方便用ps看父子进程关系,这里注释掉了
//sleep(5);
// ========== exec系列6个函数,作用:加载新程序,替换当前进程代码,不创建新进程!==========
// execl l=list:参数一个一个列出来,最后必须NULL结尾
// 第一个参数:程序完整路径;第二个开始:命令参数,argv[0]是程序名,最后NULL
//execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
// execlp p=path:自动去PATH环境变量搜索程序,不用写全路径
//execlp("ls", "ls", "-a", "-l", NULL);
// execl加载top命令
//execl("/usr/bin/top", "top", NULL);
// execv v=vector:参数放到数组里传进去,第一个参数是程序全路径
//char *const myargv[] = { "ls", "-a", "-l", NULL };
//execv("/usr/bin/ls", myargv);
// execvp vp:数组传参 + 自动走PATH搜索,就是你myshell正在用的!
//execvp("ls", myargv);
// 注释提问:为什么可以跨语言调用?
// 答案:不管C/C++/python/shell脚本,运行起来在内核眼里统统都是【进程】
// exec只负责加载可执行文件,不关心这个文件是什么语言写的
// 定义参数数组,传给exec,argv数组,最后一定要NULL标记结束
char* const myargv[] = {
"otherExe",
"-a",
"-b",
"-c",
NULL
};
// 自定义环境变量数组,最后NULL结尾,给execle / execve用
char *const myenv[] = {
"MYVAL=1111",
"MYPATH=/usr/bin/XXX",
NULL
};
// execle le:列表传参 + 自己指定环境变量(不继承父进程environ)
//execle("./otherExe", "otherExe", "-a", "-w", "-v", NULL, myenv);
// execve ve:数组传参 + 自己指定环境变量,是exec系列最底层、系统原生版本!
// 最后一个参数:environ,代表继承父进程全部环境变量
//execve("./otherExe", myargv, environ);
// execl调用bash,直接跑shell脚本test.sh
//execl("/usr/bin/bash", "bash", "test.sh", NULL);
// execl调用python3,运行python脚本test.py
//execl("/usr/bin/python3", "python3", "test.py", NULL);
// ⚠️重点:exec成功之后,后面代码全部不会执行!
// 只有exec调用失败,才会往下跑这句printf
printf("after: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
// 子进程失败直接退出,退出码1
exit(1);
}
// ========== 下面是父进程代码 ==========
// waitpid:等待子进程退出,回收子进程资源,防止僵尸进程
// 参数1:要等待的子进程id;参数2:保存子进程退出状态(NULL代表不关心退出码);参数3:选项0=阻塞等待
pid_t ret = waitpid(id, NULL, 0);
// ret>0 代表成功等到子进程,ret是子进程pid
if(ret > 0) printf("wait success, father pid: %d, ret id: %d\n", getpid(), ret);
// 父进程休眠5秒,方便观察
sleep(5);
return 0;
}
第二份代码 otherExe.cpp(C++,给上面exec加载的被调用程序)
文件名:otherExe.cpp
#include <iostream>
using namespace std;
// main完整三参数版本:
// argc:参数个数
// argv:命令行参数数组
// env:环境变量数组
int main(int argc, char *argv[], char *env[])
{
// argv[0] 永远是程序自身名字(标签字符串,内核不靠它找文件)
cout << argv[0] << " begin running" << endl;
cout << "这是命令行参数:\n";
// 循环打印argv,argv数组最后一个元素是NULL,作为结束条件
for(int i = 0; argv[i]; i++)
{
cout << i << " : " << argv[i] << endl;
}
cout << "这是环境变量信息:\n";
// env数组同样以NULL结尾,遍历打印所有环境变量
for(int i = 0; env[i]; i++)
{
cout << i << " : " << env[i] << endl;
}
cout << argv[0] << " stop running" << endl;
return 0;
}
编译运行步骤(一定要试一遍,瞬间懂exec)
- 先编译C++程序:
g++ otherExe.cpp -o otherExe
- 编译exec演示C代码:
gcc shell_code.c -o shell_code
- 运行
./shell_code
核心知识点总结(必须背,myshell核心)
fork:复制一份进程,父子进程代码一模一样,都从fork之后继续跑exec家族:只替换当前进程的程序代码,不新建进程,pid不变 ✅ exec成功:后面代码全部抛弃,不再执行 ✅ exec失败:继续往下执行,一般就是路径错、程序不存在- 6个exec字母含义
- l:list,参数一个个罗列
- v:vector,参数放数组
- p:PATH,自动搜索环境变量PATH,不用写全路径
- e:env,自己传入环境变量,不继承父进程
- waitpid:父进程回收子进程,避免僵尸进程
- 环境变量两种方式:
- 不传自定义env:继承父进程
environ(myshell默认就是这个) - 传myenv:完全使用自定义环境,父的环境全部丢掉
- 不传自定义env:继承父进程
运行现象
打开注释 execve("./otherExe", myargv, environ); 再运行: 父进程fork → 子进程走到execve → 加载otherExe,打印argv和环境变量 你就能亲眼看到:myshell传给外部命令的参数、环境变量,就是这样送过去的!
1. argv[0]
刚才讲过:只是一个标记字符串,一般放程序名字(可以带路径 ./xxx,甚至乱写)。 内核不靠它找程序,找程序是 execve 第一个参数的活。 程序可以拿 argv [0] 知道自己是被以什么名字启动的。
2. argv [1], argv [2]…… 才是真正的选项和参数
程序内部自己读取 argv 来决定行为:
ls读到-a→ 显示隐藏文件ls读到-l→ 长格式展示- 你写的 otherExe,循环遍历 argv,就是打印收到的这些参数
3 举个好玩的例子,看懂独立性
char* const myargv[] = {
"testname",
"-h",
NULL
};
execve("./otherExe", myargv, environ);
- 内核加载
./otherExe(文件名叫 otherExe) - 但是 otherExe 里面读到 argv [0] 是
"testname"程序会打印:testname begin running程序照样正常跑,只是名字标签被改了。
我们平时写 shell、用 execvp,习惯上 argv [0] 和程序名保持一致,是行业约定,不是内核强制要求。
问:execve(“./otherExe”, myargv, environ); 没有用到 otherexe main 函数的 argc 也没有传这个参数
这个点特别容易混淆,一句话先说结论: 我们代码里没有单独传 argc!argc 是操作系统拿到 argv 数组之后,自动帮你算出来的,不是我们手动传给 main 的。
// shell_code.c
char* const myargv[] = {
"otherExe",
"-a",
"-b",
"-c",
NULL
};
execve("./otherExe", myargv, environ);
我们只交给内核两件东西:
myargv:以 NULL 结尾的字符串数组- 环境数组
environ
内核加载 otherExe 程序、准备调用 main 的时候:
- 遍历
myargv,数有多少个有效字符串 → 这个数字就是 argc - 把整个数组原样交给
argv - 环境数组交给
env
所以 otherExe 的 main:
int main(int argc, char *argv[], char *env[])
argv:直接就是我们传过去的 myargvargc:内核自己数出来的,不是我们代码传进去的参数
举这个例子:
myargv = {"otherExe", "-a", "-b", "-c", NULL};
内核数一下一共4个字符串,所以 argc = 4。
对比区分(很关键)
execve 的原型:
int execve(const char *path, char *const argv[], char *const envp[]);
它只有三个参数:路径、argv数组、环境数组。 根本没有 argc 的位置。
✅ 我们只负责构造 argv[]、末尾放 NULL ✅ 内核负责统计个数,填充 argc,再调用main
再联动你之前的疑问
argv[0] 也是放在这个数组里面一起传过去的,内核不会单独处理它,只是作为数组第一个元素。
简单一句话背下来
exec系列只传argv数组(NULL结尾),argc由内核自动统计生成,不用我们传。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)