网站建设完整方案,百事通做网站,建网站麻烦吗,抚顺网站制作目录
序言
#xff08;一#xff09;替换原理
1、进程角度——见见猪跑 1️⃣ 认识 execl 函数
2、程序角度——看图理解
#xff08;二#xff09;替换函数
1、命名理解 2、函数理解
1️⃣execlp
2️⃣execv
3️⃣execvp
4️⃣execle
5️⃣execve
6️⃣execve…
目录
序言
一替换原理
1、进程角度——见见猪跑 1️⃣ 认识 execl 函数
2、程序角度——看图理解
二替换函数
1、命名理解 2、函数理解
1️⃣execlp
2️⃣execv
3️⃣execvp
4️⃣execle
5️⃣execve
6️⃣execve
三自制shell
总结 序言
在前面的文章中我已经详细的讲解了进程的创建。但是大家是否知道创建子进程的目的是什么呢
其实很简单无非就是让子进程帮我 (父进程) 执行特定的任务而已
此时又有一个问题被衍生出来了那就是子进程如果指向一个全新的程序代码时呢
基于上述这样的问题就需要用到本节讲到的 — 程序替换 一替换原理
1、进程角度——见见猪跑 用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。 1️⃣ 认识 execl 函数
execl 是一个在操作系统中用于进程替换的系统调用函数它允许将当前的进程映像替换为另一个可执行文件的映像。下面是关于 execl 函数的详细解释
int execl(const char *path, const char *arg0, ... /* (char *) NULL */);参数说明
path用于指定要替换为的新程序的路径。arg0新程序的名称。该参数在新程序中被作为 argv[0] 参数传递。
返回值
如果调用成功execl 函数不会返回因为进程被替换为了新的程序映像若发生错误该函数将返回 -1并设置全局变量 errno 以指示错误类型。 当我们到 man 手册中去查找时查询如下 接下来我简单的写段代码对 execl 函数的返回值进行介绍 #include stdio.h#include unistd.h#include stdlib.h#include sys/wait.hint main(){pid_t id fork();if(id 0){//childprintf(我是子进程: %d\n, getpid());int n execl(/bin/lssssss, lssssss, -a, -ln, NULL); //lsssss: 不存在 printf(you can see me : %d\n,n);exit(0);}sleep(5);//父进程printf(我是父进程: %d\n, getpid());waitpid(id, NULL, 0);return 0 ;}程序执行如下 【说明】 在子进程中我们打印出子进程的进程ID并调用 execl() 函数来将子进程的映像替换为 /bin/lssssss 这个不存在的命令;由于该命令不存在所以 execl() 函数会失败execl() 函数将返回 -1。因此打印出 you can see me 后的返回值将是 -1。 【结论】
如果替换成功不会有返回值如果替换失败一定有返回值 如果失败了必定返回只要有返回值就失败了因此不用对该函数进行返回值判断只要继续向后运行一定是失败的 有了上述对 execl 函数的认识我相信下面这段代码对大家来说就小菜一碟了 #include stdio.h#include unistd.h#include stdlib.h#include sys/wait.hint main(){printf(begin...\n);printf(begin...\n);printf(begin...\n);printf(begin...\n);// 执行进程替换execl(/bin/ls, ls, -l, NULL);printf(end...\n);printf(end...\n);printf(end...\n);printf(end...\n); return 0;} 程序输出执行结果如下 【说明】 在上述示例中execl 函数被调用来将当前进程替换为 /bin/ls并传递 -l 参数给 ls 命令。如果 execl 函数执行成功当前进程的映像将被替换为新的 ls 程序的映像。如果 execl 函数执行失败将打印相应的错误信息。
需要注意的是在调用 execl 函数时需要指定新程序的完整路径并确保该路径下的程序可执行。同时还可以传递其他命令行参数给新程序后续的参数通过参数列表传递以 NULL 结束。 2、程序角度——看图理解 上诉这张图我来给大家隆重介绍一下 那么实际上呢我曾经讲过当你出启动一个进程时那你是不是就要有PCB啊会创建一个PCB对于一个进程也要有自己的虚拟利空间也要有自己的列表那么当前进程的代码数据它都要经过页表映射到物理内存的特定区所以呢那么当我们当前的这个进程它在执行代码时如果执行了你刚刚所调用的系统调用exec等这样的接口时它就会根据你所传入的程序的路径和你要执行的程序的名称及选项把磁盘当中的一个其他的程序加载到我们对应的内存用新程序的代码来替换此时当用我们对应的当前进程去替换我们老进程的数据和代码把数据和代码用新的程序全部给你重新替换一遍其中上图中右侧那部分基本不变啊当然了你替换的时候如果空间要增多那你就重新再去调整页面就行了反正呢我们在替换时就相当于当前我们的进程的内核数据结构不变而把这个进程所匹配的代码和数据用新的程序它的代码数据来进行替换这个行为就叫做程序替换。 接下来回答一个大家可能关心的问题那就是进程进行程序替换有没有创建新进程呢
答案是没有创建新的进程为什么没有创建新的进程呢很简单因为我只是把一个新的程序加载到我们当前进程所对应的代码和数据段然后呢我让CPU去调度当前进程它就可以跑起来了在这其中我们并没有创建新的进程因为当前进程的内核、PCB、地址空间尤其是PCB的pid没有变化 而站在程序的角度我们可以这样去进行理解 假设当前我是一个进程我家里闲的没事儿干躺在那儿看电视呢突然有一个人把我拉走了让我就给它办一些它对应的事情那么站在程序的角度呢它是不是就相当于被动的被加载到的到了内存当中站在程序的角度那么其中就相当于这个程序被加载了是不是相当于这个程序就直接被加载到内存了所以呢我们也可以称我们对应的exec以及之后学习到的这些程序替换函数我们就可以称它为叫做加载器。 二替换函数
在操作系统中有几个常用的进程替换函数可以使用包括 exec 系列函数。下面是对它们的简要介绍
exec 系列函数用于执行一个新的程序映像将当前进程替换为新程序。这些函数包括
execl接收可变数量的参数作为命令行参数传递给新程序。execv接收参数数组其中第一个元素是新程序的路径后续元素是命令行参数。execle与 execl 类似但额外接收一个环境变量数组作为参数。execve与 execv 类似但额外接收一个环境变量数组作为参数。execlp与 execl 类似但允许通过环境变量 PATH 自动搜索可执行文件的路径。execvp与 execv 类似但允许通过环境变量 PATH 自动搜索可执行文件的路径。
这些函数在调用成功时不会返回因为进程映像已被替换为新程序。如果调用失败它们将返回 -1并设置全局变量 errno 指示错误类型。 接下来我们通过 man手册去对其进行查询 1、命名理解
这些函数原型看起来很容易混,但只要掌握了规律就很好记
l(list) : 表示参数采用列表v(vector) : 参数用数组p(path) : 有p自动搜索环境变量PATHe(env) : 表示自己维护环境变量 2、函数理解 在上述我们已经对 execl 函数进行了详解接下来我逐个对剩余的函数进行解释。 1️⃣execlp
execlp 函数是 exec 系列函数之一用于执行一个新的程序映像并替换当前进程。execlp 函数通过在系统的标准路径由 PATH 环境变量指定中搜索可执行文件来确定要执行的程序。
execlp 函数的原型如下
int execlp(const char *file, const char *arg, ...);file参数是要执行的程序文件名称或路径。如果在 PATH 中找到匹配的可执行文件则只需提供文件名即可。arg参数是要传递给新程序的命令行参数列表。需要以空指针结尾。
下面是一个使用 execlp 函数的示例
#include stdio.h
#include unistd.h
#include stdlib.h
#include sys/wait.hint main()
{pid_t id fork();if(id 0){//childprintf(我是子进程: %d\n, getpid());char *const myargv[] {ls,-a,-l,-n,NULL};execlp(ls,-ls, -a,-l,-n,NULL); exit(1);}sleep(1);int status 0;//父进程printf(我是父进程: %d\n, getpid());waitpid(id, status, 0);printf(child exit code: %d\n, WEXITSTATUS(status));return 0 ;
}输出展示 2️⃣execv
execv 是一个系统调用函数用于在当前进程的上下文中执行一个新的程序。
函数原型如下
int execv(const char *path, char *const argv[]);参数说明
path 是一个字符串表示要执行的程序的路径。argv 是一个以 NULL 结尾的字符串数组表示要传递给执行的程序的命令行参数。 execv 执行成功时不会返回而是直接将当前进程替换为新的程序。如果 execv 调用失败它会返回 -1并且当前进程的状态不会改变。 下面是一个使用 execv 的示例
#include stdio.h
#include unistd.h
#include stdlib.h
#include sys/wait.hint main()
{pid_t id fork();if(id 0){//childprintf(我是子进程: %d\n, getpid());char *const myargv[] {ls,-a,-l,-n,NULL};execv(/bin/ls, myargv); //lsssss: 不存在 exit(1);}sleep(1);int status 0;//父进程printf(我是父进程: %d\n, getpid());waitpid(id, status, 0);printf(child exit code: %d\n, WEXITSTATUS(status));return 0 ;
}输出展示 【说明】
使用 execv 执行了 ls a l -n 命令。/bin/ls 指定了要执行的程序的路径而 myargv 数组包含了命令行参数。当 execv 成功执行时当前进程就会被 ls 程序所替代并且输出文件列表。需要注意的是execv 函数需要提供完整的可执行文件路径并且命令行参数在数组 argv 中以 NULL 结尾。 3️⃣execvp
execvp 是一个系统调用函数与 execv 类似用于在当前进程的上下文中执行一个新的程序。它的参数形式稍有不同主要是在指定程序路径时可以省略路径。
函数原型如下
int execvp(const char *file, char *const argv[]);参数说明
file 是一个字符串表示要执行的程序的路径。如果 file 不包含斜杠字符/那么系统会按照标准的搜索路径规则来查找可执行文件。argv 是一个以 NULL 结尾的字符串数组表示要传递给执行的程序的命令行参数。
与 execv 不同的是execvp 可以在当前进程的环境变量 PATH 指定的路径中搜索要执行的程序而不需要提供完整的路径。
下面是一个使用 execvp 的示例
#include stdio.h
#include unistd.h
#include stdlib.h
#include sys/wait.hint main()
{pid_t id fork();if(id 0){//childprintf(我是子进程: %d\n, getpid());char *const myargv[] {ls,-a,-l,-n,NULL};execvp(ls, myargv); exit(1);}sleep(1);int status 0;//父进程printf(我是父进程: %d\n, getpid());waitpid(id, status, 0);printf(child exit code: %d\n, WEXITSTATUS(status));return 0 ;
}输出展示 【说明】
我们使用 execvp 执行了 ls a l -n 命令。由于 ls 是一个简单的命令而不是一个具体的可执行文件路径所以 execvp 会在系统的 PATH 环境变量中搜索 ls 可执行文件并执行该文件。当 execvp 成功执行时当前进程就会被 ls 程序所替代并且输出文件列表。与 execv 相比execvp 更加灵活因为它可以直接使用程序名称而不需要指定完整路径。 4️⃣execle execle 是一个系统调用函数用于在当前进程的上下文中执行一个新的程序并且可以指定环境变量。
函数原型如下
int execle(const char *path, const char *arg0, ..., const char *argn, char *const envp[]);参数说明
path是一个字符串表示要执行的程序的路径。arg到 argn 是一系列以 NULL 结尾的字符串表示要传递给执行的程序的命令行参数。envp:是一个以 NULL 结尾的字符串数组表示要设置给新程序的环境变量。
与 execv 和 execvp不同的是execle 可以显式地指定环境变量而不是继承当前进程的环境变量。 下面是一个使用 execle 的示例 首先为了跟上述的代码区分开我另外在创了一个文件在里面放入了相应的信息目的就是通过我们的【myproc】去调用other目录下的【otherproc】 otherproc.cc代码如下
#include iostream
#include unistd.h
#include stdlib.husing namespace std;int main()
{for(int i 0; i 5; i){cout ---------------------------------------------------------------- endl;cout 我是另一个程序我的pid是: getpid() endl;cout MYENV: (getenv(MYENV)NULL?NULL:getenv(MYENV)) endl;cout PATH: (getenv(PATH)NULL?NULL:getenv(PATH)) endl;cout ---------------------------------------------------------------- endl;sleep(1);}return 0;
}
【说明】
用于输出当前程序的PID和环境变量。程序会循环输出这些信息并每秒钟输出一次程序会使用 getpid() 函数获取当前进程的PID并使用 getenv() 函数获取环境变量的值需要注意的是getenv() 函数用于获取指定环境变量的值。在程序中使用了 MYENV 和 PATH 作为要获取的环境变量名而对于 PATH来说在系统中默认是有的
输出展示 接下来我们退出 other 目录对【myproc.c】进行改造
#include stdio.h
#include unistd.h
#include stdlib.h
#include sys/wait.hint main()
{pid_t id fork();if(id 0){//childprintf(我是子进程: %d\n, getpid());char *const myenv[]{MYENVYouCanSeeMe,NULL};execle(./other/otherproc,otherproc,NULL,myenv); exit(1);}sleep(1);int status 0;//父进程printf(我是父进程: %d\n, getpid());waitpid(id, status, 0);printf(child exit code: %d\n, WEXITSTATUS(status));return 0 ;
}输出展示 上述不难看出当我们调用 execle函数时发生的是覆盖式的调入。老的数据会被覆盖掉此时即传入了我自己定义的环境变量 但是有一天我不想传自己的环境变量了。此时我想传系统的环境变量此时我们可以怎么做呢 此时我们需要引入一个概念extern char **environ;
在 POSIX 标准中全局环境变量是一个字符串指针数组其中每个指针指向一个以 keyvalue 格式表示的环境变量字符串通过使用 extern char **environ; 的声明我们可以在程序中访问这个全局环境变量数组。 接下来我们改动一下代码 输出展示 【说明】
我们再进行程序此时呢我们可以发现用我们的程序去运行 other 时此时【myenv】当前是没有的但是【PATH】此时还是有的。确实交给子进程了 当我不仅想传系统的环境变量还想把自己的环境变量都传给子进程时该怎么做呢 此时我们需要在认识一个接口putenv putenv 是一个 C 语言标准库函数用于设置环境变量的值。它可以添加新的环境变量或修改已存在环境变量的值。
函数原型如下
int putenv(char *string);参数 string 是一个以 keyvalue 格式表示的字符串key 是要设置或修改的环境变量的名称value 是要将该环境变量设置为的值。 代码展示 输出展示 除了上述这样的做法之外我们还可以像下述这样去进行操作
我们在 myproc.c 中不进行 putenv 操作我们在当前命令行中进行 export操作 输出展示 有了上述的理解。接下来解释一个问题 在之前学习环境变量时我们知道 环境变量具有全局属性可以被子进程继承下去但是是怎么办到的呢
很简单因为所有的指令都是bash的子进程而bash执行所有的指令都可以直接通过exec去执行我们要给子进程把bash的环境变量交给子进程只需要调用 【execle】然后再把我们指定的环境变量直接以最后一个参数的形式传给子进程此时子进程就拿到了 5️⃣execve
execve 是一个系统调用函数它会替换当前进程的映像将其替换为新程序的映像并开始执行新程序。
函数原型如下
int execve(const char *filename, char *const argv[], char *const envp[]);参数说明
参数 filename 是要执行的程序的路径第二个参数 argv[] 是一个字符串数组它包含了传递给新程序的命令行参数而 envp[] 是一个字符串数组它包含了传递给新程序的环境变量。 这个跟上述的 execle 函数是类似的。在这里就不做过多演示。 6️⃣execve 事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册第2节,其它函数在man手册第3节 这些函数之间的关系如下图所示 三自制shell 我们可以用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表它随着时间的流逝从左向右移动。shell从用户读入字符串ls。shell建立一个新的进程然后在那个进程中运行ls程序并等待那个进程结束。 然后shell读取新的一行输入建立一个新的进程在这个进程中运行程序 并等待这个进程结束。 所以要写一个shell需要循环以下过程:
1. 获取命令行2. 解析命令行3. 建立一个子进程fork4. 替换子进程execvp5. 父进程等待子进程退出wait
根据这些思路和我们前面的学的技术就可以自己来实现一个shell了实现代码:
#include stdio.h
#include string.h
#include stdlib.h
#include assert.h
#include unistd.h
#include sys/types.h
#include sys/wait.h#define MAX 1024
#define ARGC 64
#define SEP int split(char *commandstr, char *argv[])
{assert(commandstr);assert(argv);argv[0] strtok(commandstr, SEP);if(argv[0] NULL) return -1;int i 1;while((argv[i] strtok(NULL, SEP)));return 0;
}void debugPrint(char *argv[])
{for(int i 0; argv[i]; i){printf(%d: %s\n, i, argv[i]);}
}int main()
{while(1){char commandstr[MAX] {0};char *argv[ARGC] {NULL};printf([zhangsanmymachine currpath]# );fflush(stdout);char *s fgets(commandstr, sizeof(commandstr), stdin);assert(s);// 保证在release方式发布的时候因为去掉assert了所以s就没有被使用// 而带来的编译告警, 什么都没做但是充当一次使用(void)s;commandstr[strlen(commandstr)-1] \0;int n split(commandstr, argv);if(n ! 0) continue;//debugPrint(argv);// version 1pid_t id fork();assert(id 0);(void)id;if(id 0){//childexecvp(argv[0], argv);exit(1);}int status 0;waitpid(id, status, 0);}
} 整体代码进程替换代码 总结 以上便是关于进程程序替换的全部内容。接下来简单回顾下本文都讲了什么 进程程序替换是指在一个正在运行的进程中用另外一个可执行程序替换当前进程的执行内容从而使新的程序代码开始执行。
进程程序替换通常用于实现进程的动态更新、功能扩展或进程间通信。在 Linux 系统中常用的进程程序替换函数是 exec 函数族包括 execl、execle、execlp、execv、execvp 等。
这些函数可以加载新的可执行文件并用其替换当前进程的执行内容从而运行新的程序。替换后新的程序将继承原进程的一些属性如进程 ID、文件描述符等。
需要注意以下几点
替换后原有的程序代码、数据和堆栈信息都会被新的程序取代因此原有进程的状态将完全丢失。替换的新程序需具备执行权限并与原进程使用相同的用户身份执行否则可能会导致权限问题。替换后新程序的命令行参数、环境变量等可以与原进程不同从而实现不同的功能。 到此本文便讲解完毕了。感谢大家的观看与支持