进程程序替换(思维提升重点)

进程程序替换(思维提升重点) 进程程序替换第一部分程序替换的基本原理1.1 什么是程序替换调用 exec 系列接口时当前进程已经有了自己的代码、PCB、地址空间和页表你一定是先有一个进程才能替换程序替换执行时会把目标程序的代码和数据覆盖式地载入到当前进程的代码段和数据段替换之后当前进程就开始执行新程序的代码一句话定义程序替换 用新程序的代码和数据把当前进程的代码和数据覆盖掉。现象举例写一个程序前半部分打印自己的代码逻辑后半部分调用 exec 执行ls——运行后你会发现前半部分是你自己的输出后半部分全变成了ls的执行结果。1.2 替换不创建新进程重点在程序替换的过程中并没有创建新的进程进程还是那个进程PCB 还是那个 PCB地址空间还是那个地址空间只是代码段和数据段里的内容被换成了新程序的验证方法PID在程序替换前打印getpid()在 exec 调用的参数里让目标程序也打印自己的 PID例如让新程序自己getpid()打印。会发现替换前后的 PID 完全一样——这就证明了 exec 没有创建新进程。1.3 替换 vs 创建 的区别对比项进程创建fork程序替换exec进程个数多了一个进程2个进程个数不变还是1个PCB新建一个 PCBPCB 不变代码和数据父子各一份写时拷贝原地覆盖目的让子进程成为新的执行单位让当前进程去执行新程序第二部分exec 函数族7 个接口2.1 总览6 1 7 个接口接口特点说明execll参数以列表形式一个个传execlpl p列表传参 PATH 中查找execlel e列表传参 自定义环境变量execvv参数以**数组指针数组**形式传execvpv p数组传参 PATH 中查找execvpev p e数组传参 PATH 自定义环境变量execve系统调用唯一的系统调用其余 6 个都是它的封装61 的秘密你背 6 个名字时按规律排出来是execl, execlp, execl e, execv, execvp, execvpe——等一下execl eexecle和execvpe都在但为什么感觉少了一个其实不是少了而是execve没有和那 6 个货放在一起它单独出来了。2.2 命名规律l / v / p / e我觉得这样方便我们记忆和使用字母英文含义llist参数用列表形式可变参数...一个传vvector参数用数组指针数组char* argv[]一次传pPATH自动去PATH 环境变量里找可执行程序不用带路径eenviron允许自定义环境变量传入不带 p → 必须写完整路径带 p → 只写程序名自动去 PATH 里找。不带 e → 环境变量用默认的父进程继承下来的带 e → 用你传的环境变量表会覆盖旧的。2.3 execl第一个学会的接口重点#includeunistd.hintexecl(constchar*path,constchar*arg,...);参数详解第一个参数path—— 目标程序的路径 程序名告诉系统我要执行谁后续参数可变参数列表C语言的...—— 命令行参数告诉系统我要怎么执行最后一个参数必须是 NULL作为列表结束的标志哨兵值// 示例让当前进程去执行 ls -a -lexecl(/usr/bin/ls,ls,-a,-l,NULL);// ↑路径程序名 ↑程序名(惯例) ↑参数 ↑参数 ↑NULL结尾// 示例执行我们自己的程序 myprocexecl(./myproc,myproc,hello,world,NULL);注意第一个参数和第二个参数的区别——第一个参数path告诉系统文件在哪加载谁第二个参数arg0是命令行参数列表的第一个按惯例写程序名argv[0]它会成为新进程的 argv[0]2.4 返回值特性只有失败才返回重点这是 exec 系列最反直觉的特性printf(before exec\n);execl(/usr/bin/ls,ls,-l,NULL);printf(after exec\n);// 这行永远不会执行成功时如果替换成功exec 不返回代码已经被替换了回不来了后续代码全部不执行如果替换失败exec 返回-1后续代码继续执行进程还是原来的进程原理exec 成功的那一刻当前进程的代码段已经被新程序覆盖CPU 去执行新程序了原来的代码根本不存在了自然无家可归——所以成功时不需要返回也无法返回。失败时代码没被换才能返回 -1 告诉你出错了。常见面试考点exec 函数只有失败时才有返回值-1成功时没有返回值。2.5 execv数组传参v vectorintexecv(constchar*path,char*constargv[]);参数以指针数组形式一次传不用像 execl 那样列表一个个写char*myargv[]{ls,-a,-l,NULL};// 注意 NULL 结尾execv(/usr/bin/ls,myargv);与 execl 对比效果完全一样只是传参形式不同——l 是列表一个个传v 是数组一次传完。小知识main函数的argc/argv之所以能拿到命令行参数就是因为父进程通过 exec 的参数把命令行参数传了进来——你在命令行输入ls -a -lbash 就会用 exec 把ls,-a,-l传给你的程序。2.6 execvpp PATH 自动查找intexecvp(constchar*file,char*constargv[]);两个变化第一个参数不用带路径了只要写程序名如ls系统自动去PATH 环境变量指定的目录里找这个程序char*myargv[]{ls,-a,-l,NULL};execvp(ls,myargv);// 自动在 PATH 里找到 /usr/bin/ls2.7 execvpev p e 全都要intexecvpe(constchar*file,char*constargv[],char*constenvp[]);v参数用数组传p不带路径去 PATH 找e多了一个envp参数——自定义环境变量表char*myargv[]{myproc,hello,NULL};char*myenvp[]{MYENV100,OTHER200,NULL};// 自定义环境变量表execvpe(myproc,myargv,myenvp);注意execvpe 是 GNU 扩展不是所有系统都有。标准 POSIX 里带 e 的是 execle 和 execve。2.8 execve唯一的系统调用重点考点用 man 手册号来区分execve→man 22 号手册→真正的系统调用其余 6 个execl、execlp、execle、execv、execvp、execvpe→man 33 号手册→ C 语言库函数// execve 系统调用原型intexecve(constchar*filename,char*constargv[],char*constenvp[]);关键结论考试重点Linux 系统里真正执行程序替换的系统调用只有一个——execve。其余 6 个接口都是 C 语言对 execve 的封装方便我们在不同场景下传参带路径/不带路径、列表/数组、带不带环境变量。封装时内部会把各种传参形式统一转化成 execve 需要的样子最终都会调用 execve。推论——环境变量为什么不传也有你调用execl、execvp这类没有 e的接口时看起来没传环境变量但底层最终调 execve 时环境变量是必须要传的所以这些接口内部封装时会自动把默认的环境变量表传进去这个默认表就是 C 语言的全局变量environ指向环境变量表的指针从父进程继承下来fork 时拷贝给子进程第三部分fork exec 经典模式重点3.1 为什么必须 fork exec 配合程序替换是自杀式的——exec 成功当前进程就没了变成新程序。如果你在主进程里直接 exec你的主程序就消失了。所以正确姿势是fork创建一个子进程子进程里调用 exec 做程序替换子进程牺牲自己变成新程序父进程继续干自己的事用 wait/waitpid 等子进程#includeunistd.h#includesys/wait.h#includestdio.hintmain(){pid_tidfork();if(id0){// 子进程执行程序替换execl(/usr/bin/ls,ls,-a,-l,NULL);// 走到这说明 exec 失败了perror(execl);exit(1);}// 父进程等子进程waitpid(id,NULL,0);printf(parent is running, child is replaced\n);return0;}现象父进程打印自己的话子进程变成了ls输出目录列表——父进程完全不受影响。3.2 父进程为什么不受影响写时拷贝这里要用到 fork 的**写时拷贝COW**知识fork 之后父子进程共享同一份代码和数据虚拟地址指向同一块物理内存谁要写才发生拷贝子进程 exec 时要覆盖代码和数据 → 触发写时拷贝 → 子进程拷贝出自己独立的一份然后在新拷贝上覆盖新程序父进程的代码和数据原封不动继续正常运行结论程序替换发生时代码和数据都会发生写时拷贝子进程与父进程彻底分离。这正是进程独立性的体现——进程之间互不干扰子进程想怎么折腾包括把自己整个换掉都影响不了父进程。3.3 fork exec 的本质意义为什么要创建子进程为什么 fork 要配合 exec因为多进程编程的最终目的就是让每个进程去执行不同的程序让子进程去跑别的程序干活。如果子进程只能执行和父进程一模一样的代码那多进程就失去意义了。fork exec 创建子进程 让子进程执行全新程序这才是完整的多进程编程模型。第四部分加载器Loader概念4.1 加载器是什么程序加载的本质是动态创建进程的过程。谁负责加载——加载器。exec 就相当于一种加载器把磁盘上的程序文件加载进内存替换当前进程的代码和数据。4.2 bash 就是一个加载器经典例子你在终端敲下ls -a -l回车bash 干了什么bash 是当前进程它fork出一个子进程子进程exec执行lsbash父进程用wait等待ls结束ls结束后bash 继续接受下一条命令所以你运行的所有程序都是 bash 的子进程命令行参数就是 bash 通过 exec 传给子进程的。Windows 类比Windows 下也有类似的 exec 接口。你在 VS2022 里运行程序时VS 就是你的父进程——它帮你 fork 创建子进程再用 exec 加载你的代码把你的命令行参数通过 execv 传进来。第五部分跨语言替换重点5.1 C 程序能替换执行任何语言的程序exec 的目标程序不限于可执行二进制文件可以是另一个 C 程序 ✅C 程序 ✅Python 脚本 ✅PHP 脚本 ✅Shell 脚本 ✅……任何能转化为进程的程序// 示例C 程序替换执行 Python 脚本execl(/usr/bin/python3,python3,mytest.py,NULL);// 示例C 程序替换执行 Shell 脚本execl(/bin/bash,bash,mytest.sh,NULL);5.2 原理真正执行的是解释器以 Python 脚本为例脚本本身不是可执行代码但解释器python3是可执行的exec 加载的其实是解释器程序python3解释器再去读脚本文件解释执行从进程角度看exec 依然是把解释器的代码和数据覆盖进来结论程序替换的程序是一个广义概念——任何能作为一个进程跑起来的程序都可以被替换执行不一定非得是同一种语言编译出来的。第六部分命令行参数传递v 接口的底层理解6.1 参数谁来传你写的程序运行时命令行参数argv和环境变量都是从哪来的答案父进程通过 exec 传的。你的程序是 bash 的子进程你在命令行输入的命令和参数就是 bash 通过 exec 的参数传进来的exec 底层会把命令行参数直接传进新程序的进程新程序的 main 函数才能收到 argv6.2 命令行参数和环境变量的存储命令行参数和环境变量存储在进程地址空间的独立内存区栈的上方/地址空间的高地址区域也是两个独立的区域fork 时这份内存区拷贝给子进程exec 时exec 的参数argv、envp会重新构建这片区域传给新程序第七部分总结各exec参数记忆7.1 代码例子// 1. execl —— 列表传参必须带路径execl(/usr/bin/ls,ls,-a,-l,NULL);// 2. execlp —— 列表传参PATH 查找execlp(ls,ls,-a,-l,NULL);// 3. execv —— 数组传参必须带路径char*argv[]{ls,-a,-l,NULL};execv(/usr/bin/ls,argv);// 4. execvp —— 数组传参PATH 查找execvp(ls,argv);// 5. execvpe —— 数组 PATH 自定义环境变量char*envp[]{MYENV100,NULL};execvpe(ls,argv,envp);// 6. fork exec 经典模式pid_tidfork();if(id0){execl(/usr/bin/ls,ls,-l,NULL);// 子进程替换exit(1);// 走到这 exec 失败}waitpid(id,NULL,0);// 父进程等待本篇总结程序替换exec解决的核心问题让一个进程去执行一个全新的程序且不创建新进程——把目标程序的代码和数据覆盖式载入当前进程的代码段和数据段。exec 函数族共 7 个接口只有 execve 是系统调用其余 6 个是库函数封装只有失败才返回 -1。实际使用中永远配合 forkfork 出子进程 → 子进程 exec 替换 → 父进程 wait 等待父进程靠写时拷贝保持独立性。环境变量传递遵循不传用 environ、传了全覆盖的规则增量新增用 putenv 补齐整表再传。下一篇自定义 shell 设计。