建设工程造价管理协会网站,中国企业网银,国家信用信息公示系统官网,哪家公司做企业网站四、进程程序替换 之前用fork创建子进程后#xff0c;父子进程执行同一个程序的不同代码段。 如何使子进程执行另一个不同的程序呢#xff1f;子进程需要进行程序替换#xff01; 程序替换#xff0c;就是通过特定的接口#xff0c;将磁盘上一个全新的程序#xff08;包…四、进程程序替换 之前用fork创建子进程后父子进程执行同一个程序的不同代码段。 如何使子进程执行另一个不同的程序呢子进程需要进行程序替换 程序替换就是通过特定的接口将磁盘上一个全新的程序包括代码和数据加载到调用进程的地址空间中。 4.1 程序替换的原理 在进行程序替换时操作系统会将新程序的代码和数据加载到调用进程的地址空间中。这个过程通常包括以下几个步骤 加载数据操作系统会将新程序的代码和数据从磁盘或其他存储介质中读取到内存中。这些代码和数据会被加载到物理内存中的合适位置。 调整进程地址空间重定位由于新的程序可能与原有程序的地址空间不同因此需要进行地址重定位。操作系统会根据新程序的要求将程序中的地址引用进行调整使其指向正确的物理内存位置。 更新页表操作系统会更新进程的页表以映射新程序的代码和数据所在的物理内存页。这样进程就可以通过虚拟内存地址访问到正确的物理内存。 清理旧程序操作系统会释放原有程序占用的物理内存页以便为新程序腾出空间。这些旧的物理内存页会被标记为可重用以供其他进程使用。
总的来说程序替换是通过加载新程序的代码和数据到物理内存中并进行地址重定位和页表更新来完成的。这样进程就可以执行新的程序了。 4.2 程序替换函数
子进程往往要调用exec*函数进行程序替换以执行另一个程序exec*函数是加载器的底层调用接口。当进程调用exec*函数时,该进程用户空间的代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的pid并未改变。
下面是exec*系统调用 4.2.1 execl函数
参数
path待替换程序的所在路径文件名arg, …命令行参数以字符串的形式一个个传入最后以NULL结尾标识参数传递完毕。
返回值 exec函数只有发生错误失败时才会返回返回值是-1。 exec函数一旦调用成功后续的所有代码都不会执行也根本不需要有返回值。 提示 execl拆解速记exec(execute) - l(list)命令行参数表和环境变量表都是以NULL结尾用于表示结束。 测试代码
#include iostream
#include unistd.h
using namespace std; int main(){ cout 当前进程的开始代码 endl; execl(/usr/bin/ls, ls, -a, -l, --colorauto, NULL); cout 当前进程的结束代码 endl; //程序被替换所以不会被打印
} 测试结果 4.2.2 execv函数
与execl比较只是第二个参数argv不同。
argv命令行参数表字符指针数组数组元素同样要以NULL结尾。
其他特性和execl一模一样 execv拆解速记exec(execute) - v(vector) 测试代码
#include iostream
#include unistd.h
#include sys/wait.h
using namespace std; int main(){ pid_t id fork(); if(id 0) { //子进程执行流 cout Im child process! child_pid: getpid() endl; char *const argv[] {(char*)ls, (char*)-a, (char*)-l, (char*)--colorauto, NULL}; execv(/usr/bin/ls, argv); } else if(id 0) { //父进程执行流 int status 0; pid_t ret waitpid(id, status, 0); if(ret 0) { if(WIFEXITED(status)) { cout 子进程正常退出child_pid: ret; cout exit_code: WEXITSTATUS(status) endl; }else{cout 子进程崩溃child_pid: ret;cout exit_signal: (status 0x7F) endl;} }else if(ret -1){cout 等待子进程失败 endl;}}else{perror(子进程创建失败);return 1;}
}
测试结果 4.2.3 execlp函数
与execl比较只是第一个参数file不同。
file待替换的程序名该程序的所在路径必须在PATH环境变量中。
其他特性和execl一模一样 execlp拆解速记exec(execute) - l(list) - p(PATH) 测试代码
//...... if(id 0) { //子进程执行流 cout Im child process! child_pid: getpid() endl; execlp(ls, ls, -a, -l, NULL);
//...... 运行结果 当然也可以执行我们自己编写的代码
可以使用绝对路径或相对路径
绝对路径execv(/home/zty/code/Linux/20230721/test, argv);相对路径execv(./test, argv);
测试代码这里测试的是之前写过的一个接收命令行选项参数的简单程序
测试结果 补充内容makefile一次性构建多个可执行程序 .PHONY:all
all:myproc test myproc:myproc.cc
g $^ -o $ -stdc11
test:test.cc
g $^ -o $ -stdc11 .PHONY:clean
clean:
rm -f myproc 甚至还可以执行其他语言编写的程序
shell脚本
#! /usr/bin/bash echo hello shell!运行命令bash test.sh
将进程替换为shell程序execlp(bash, bash, test.sh, NULL);
python脚本
#! /usr/bin/python3.6print(hello Python/n)运行命令python test.py
将进程替换为Python程序execlp(python, python, test.py, NULL);
如果python脚本文件具有可执行权限execlp(./test.py, test.py, NULL); 注意 shellPythonJava等编程语言拥有自己的解释器通过解释器可以直接运行所编写的程序无需编译生成可执行程序。可以给脚本文件加上可执行权限然后直接./test.py运行程序。实际仍然是通过解释器执行程序的。 4.2.4 execle函数
与execl相比新增了第三个参数envp
envp环境变量表字符指针数组数组元素同样要以NULL结尾。
其他特性和execl一模一样 execle拆解速记exec(execute) - l(list) - e(environment) 测试代码
//...... if(id 0) { //子进程执行流 cout Im child process! child_pid: getpid() endl; char *const _env[] {MYENV122, NULL}; //设置MYENV环境变量execle(./test, test, -e, NULL _env);//test -e选项打印MYENV环境变量
//...... 测试结果 提示环境变量具有全局属性是因为可以通过类似于execle的系统调用将父进程环境变量表传递给子进程。 4.2.5 execve函数 execve是一个系统调用函数用于在Linux系统中执行一个新的程序。它的参数解释如下 const char *filename要执行的程序的路径。可以是绝对路径也可以是相对路径。 char *const argv[]一个字符串数组用于传递给新程序的命令行参数。数组的最后一个元素必须为NULL表示参数列表的结束。 char *const envp[]一个字符串数组用于传递给新程序的环境变量。数组的最后一个元素必须为NULL表示环境变量列表的结束。如果envp为NULL则新程序将继承当前进程的环境变量。
execve函数的返回值是一个整数如果执行成功它不会返回而是直接在当前进程中加载并执行新程序。如果发生错误返回值为-1并设置errno来指示具体的错误类型。 注意 execve函数会替换当前进程的代码和数据将其替换为新程序的代码和数据。因此execve函数之后的代码将不会被执行。如果希在执行新程序后继续执行其他操作可以使用fork函数创建一个子进程在子进程中调用execve函数而在父进程中继续执行其他操作。 execve是真正的系统调用而上面的6个接口实际上是系统提供的基本封装。 他们会将接收到的参数进行合并处理最后底层还是会调用execve 总结exec*函数的命名方式 l/v命令行参数以可变参数列表或者指针数组传入p是否在环境变量PATH中查找程序路径e是否自己维护环境变量表 4.3 简单的命令行解释器
#include stdio.h
#include stdlib.h
#include string.h
#include unistd.h
#include sys/wait.hchar cmd_line[1024]; //用于接收存储整条命令
char* cmd_param[32]; //将整条命令拆解成一个个参数
char env_buffer[64]; //环境变量缓冲区//shell运行原理父进程接收并解析命令创建子进程执行命令父进程等待。
int main(){//0.命令行解释器是常驻内存进程不退出while(1){//1.打印提示信息[rootlocalhost myshell]#printf([rootlocalhost myshell]# );fflush(stdout); //[1]//sleep(1);//2.获取用户输入包括指令和选项ls -a -l -imemset(cmd_line, \0, sizeof(cmd_line)); if(fgets(cmd_line, sizeof(cmd_line), stdin) NULL) continue; //[2]if(strcmp(cmd_line, \n) 0) continue; //[2]cmd_line[strlen(cmd_line)-1] \0; //[3]//printf(%s\n, cmd_line);//3.命令行字符串解析ls -a -l -- ls -a -lcmd_param[0] strtok(cmd_line, ); //[4]int i 1;while(cmd_param[i] strtok(NULL, ));//for(int i 0; cmd_param[i]; i)//{// printf(%s ,cmd_param[i]);//}//printf(\n);//4.内置命令让父进程shell自己执行的指令又叫内建命令//内建命令本身就是shell中的一个函数调用if(strcmp(cmd_param[0], cd) 0) //cd指令切换父进程(shell)工作目录{if(cmd_param[1] ! NULL)chdir(cmd_param[1]); //[5]continue;}if(strcmp(cmd_param[0], export) 0) //export指令导出环境变量使其可被子进程继承{ if(cmd_param[1] ! NULL) { strcpy(env_buffer, cmd_param[1]); //[6]putenv(env_buffer); //[7] } continue; } //5.创建子进程执行命令int id fork();if(id 0){printf(Im child process! pid:%d ppid:%d\n, getpid(), getppid());execvp(cmd_param[0], cmd_param);exit(1);}//6.父进程shell等待子进程获取退出状态回收资源int status 0;int ret waitpid(-1, status, 0); //阻塞等待if(ret 0){if(WIFEXITED(status)){//正常退出返回退出码printf(normal exit! child_pid:%d exit_code:%d\n, ret, WEXITSTATUS(status));}else{//异常退出返回退出信号printf(abnormal exit! child_pid:%d exit_signal:%d\n, ret, status0x7F);}}else if(ret 0){printf(Waiting failed!\n);}}
}
设计流程
命令行解释器是常驻内存进程不退出打印提示信息[rootlocalhost myshell]#获取用户输入包括指令和选项“ls -a -l -i”命令行字符串解析“ls -a -l” -- “ls” “-a” “-l”内置命令让父进程shell自己执行的指令又叫内建命令内建命令本身就是shell中的一个函数调用创建子进程执行命令父进程shell等待子进程获取退出状态回收资源
解释
[1] 由于打印的提示信息不带’\n’所以缓冲区中的数据不会自动刷新到显示器需要手动fflush(stdout)强制刷新使其立马显示在屏幕上。[2] 如果没有获取到任何字符或者获取失败直接continue需要注意的是’\n’也会被读取如果只读取到’\n’也要continue;[3] 需要将获取到的最后一个换行符替换为’\0’否则会被当做命令的一部分处理。[4] C库函数strtok的用法C 库函数 – strtok() | 菜鸟教程 (runoob.com)[5] strtok工作原理将指定的分隔符替换为’\0’将原字符串分割。依次返回子串的首字符地址。[6] Linux系统调用chdir 功能用于改变当前进程的工作目录这意味着后续的文件操作如打开文件、读写文件将在新的当前目录下进行。头文件unistd.h参数切换的路径返回值成功返回0失败返回-1 [7] cmd_param保存的是cmd_line中各命令行参数的首字符地址。memset会将cmd_line清空导致环境变量丢失。因此需要将环境变量拷贝到缓冲区。[8] Linux系统调用putenv 功能用于设置环境变量头文件stdlib.h参数环境变量键值对字符串返回值设置成功返回0失败返回非0值 提示 shell执行的命令通常有两种 外部命令第三方提供的在磁盘中有具体二进制文件的可执行程序由子进程执行如ls,ps,pwd,我们编写的程序内置命令shell内部自己实现的方法由父进程(shell)自己执行这些命令就是要影响shell本身如cd,export 进程的环境变量会被子进程继承并且子进程进行程序替换并不会替换环境变量相关的内容shell的环境变量是从哪里来的 最初的环境变量是保存在配置文件(shell脚本)中的。shell启动的时候通过读取配置文件获得起始环境变量。 为什么要程序替换
和应用场景有关有时候我们必须让子进程执行新的程序。
为什么要创建子进程执行程序
为了不影响父进程如上面的shell程序我们想让父进程聚焦在读取命令解析命令指派进程执行程序的功能上
如果不创建子进程我们就只能替换父进程程序了