当前位置: 首页 > news >正文

进程程序替换

目录

  • 1. 单进程的程序替换
  • 2. 进程替换的基本原理
  • 3. 多进程的进程替换
  • 4. 验证各种进程替换接口
    • 4.1 再谈环境变量
  • 5. execve 系统调用

1. 单进程的程序替换

int execl(const char *path, const char *arg, ...);path:要替换的目标可执行程序的路径
其中的 ... 为可变参数列表,不同函数的参数列表可能是不一样的,有了可变参数,即可传递不同的参数个数
而不会被固定的形参列表所限制传递的实参个数。
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>int main()
{printf("before: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());// 标准写法execl("/usr/bin/ls", "ls", "-a", "-l", NULL);printf("after: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());return 0;
}

在这里插入图片描述

现象:excel 进行程序替换之后,execl 后续的代码并没有被执行。而类似 execl 这种调用我们就称为 进程替换。

2. 进程替换的基本原理

当一个程序执行起来,操作系统为其创建一个独有的 PCB、进程地址空间、页表等内核数据结构,并且把存储在磁盘中的该程序代码和数据加载到内存之后,这个进程的创建就完成了。而 execl 是一个系统调用函数,在调用这个系统调用之前,还是我们这个程序,执行 execl 时,由于替换目标也是一个可执行程序也是程序,那么它就一定也存储在磁盘中,在运行起来之前也必须加载到内存中。而替换发生时,操作系统简单粗暴的直接将原本程序的代码和数据 替换成 目标程序的代码和数据(单进程情况),这就是进程替换!

所以也就不难理解,为什么进程替换之后,位于 execl 系统调用之后的代码都不再被执行。进程替换,就相当于被夺舍!替换之后,就是另一个程序了,哪还有你原来程序的代码和数据,早就被踢出门了!替换后新的程序是看不到,也没办法看到你原本程序的代码和数据的!替换之后,进程的 PCB、进程地址空间,包括页表的虚拟地址这些都不需要变动,只需要修正一下映射的物理地址即可(因为每个程序的大小不一定都是一致的)。之后再从新程序的起始地址开始执行!

3. 多进程的进程替换

int main()
{pid_t id = fork();if(id == 0){printf("before: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());sleep(3);execl("/usr/bin/ls", "ls", "-a", "-l", NULL);printf("after: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());}pid_t ret = waitpid(id, NULL, 0);if(ret > 0) printf("wait success, ppid: %d, ret_id: %d\n", getppid(), ret);sleep(3);                                                                                                 return 0;
}

在这里插入图片描述

现象:fork 创建出一个子进程之后,父子进程同时运行,子进程进行进程替换,父进程照样执行,不受影响。并且父进程对子进程的进程等待也不受影响。而在进程监控中,执行 excel 进程替换之后,waitpid 返回的等待的子进程的 pid 并没有发生改变!换言之,进程替换不会创建出新的进程,只进行进程的代码和数据的替换工作!

  • 为什么子进程进程替换的时候不会影响父进程呢??
    因为进程直接具有独立性!进程替换的本质,就是对子进程的代码和数据进行修改,而子进程存在 写时拷贝 的策略。父子进程代码是共享的,数据层面上,必要时子进程会对数据进行写时拷贝。但是进程替换,不仅仅是数据的替换,代码也被替换成新程序的代码了。

  • 这么说的话,是不是代码也可以被写时拷贝??
    没错,代码也可以被写实拷贝!这与我们之前的认知有点不同,代码不是常量区的区域吗,怎么可以被修改了。其实,在物理内存中,没有所谓的只读不可写的区域,这都是进程地址空间上划分的,而之所以我们一直认为代码不可写,是因为有进程地址空间的存在,我们是用户,操作系统不让我们对代码常量区进行写入操作。但我们可以通过调用 execl 这样的函数,让操作系统帮我们做。换言之,你做不到,操作系统做得到,在它的世界里,它就是 root!

简言之,单进程的进程替换,新程序的代码和数据之间替换;多进程的进程替换时,代码和数据写时拷贝,代码和数据就都互相独立了,就算子进程进程替换了,父子进程也依旧互不影响,保持着进程之间的独立性!

  • 补充:
  • 程序替换成功之后,exec* 后续的代码不会被执行。只有替换失败了才有可能执行后续代码。所以这也就决定了,exec*函数,调用失败了才有返回值,调用成功是没有返回值的(成功了就代表进程替换了,原程序都被踢出门了,能返回给谁呢??)
  • Linux中形成的可执行程序,是有格式的,ELF, 可执行程序的表头,可执行程序的入口地址就记录在表头中。

4. 验证各种进程替换接口


在这里插入图片描述

在正式介绍各种进程替换的接口参数之前,我们需要先有一个前置知识。

  • 执行一个程序的第一件事,是找到这个程序
  • 找到程序之后,才是执行这个程序,而怎么执行,取决于涵盖哪些选项去执行。
int execl(const char *path, const char *arg, ...);
path: 进程替换的目标程序的路径(找到程序)
arg: 执行哪个程序
...: 可变参数列表,即如何执行该程序(如何在命令行中执行的,就如何传递参数即可)示例:
int execl("/usr/bin/ls", "ls", "-a", "-l", NULL);		// 一定要以 NULL 结尾,以示命令行参数的结尾
int execlp(const char *file, const char *arg, ...);
execlp 其中的 p 代表的 PATH 环境变量的意思,execlp 自己会在默认的 PATH 环境变量中查找
所有的子进程都会继承父进程的环境变量列表,因此进程替换后,也可以通过 PATH 环境变量找到相应的程序
与上面不同的是,第一个参数可以不用写路径(写了也能正常运行),只需要写可执行程序的程序名称即可示例:
int execlp("ls", "ls", "-a", "-l", NULL);	
int execv(const char *path, char *const argv[]);
execv 其中的 v 可以理解为 vector 向量的意思,
第一个参数还是传递路径
可变参数不再需要一个一个传递,可以使用字符串指针数组传递示例:
char* const myargv[] = {"ls", "-a", "-l", NULL};
int execv("/usr/bin/ls", myargv);	
  • 拓展:ls 也是一个程序,c/c++ 编译后的可执行程序,它也有 main 函数,其 main 函数也有命令行参数,其命令行参数就是通过 execv 系统调用中的 myargc 参数传递进来的。而类似 execl 这类进程替换函数的可变参数列表,最终都是转换成 myargc 这样的指针数组,再传入给指定的程序中。这也就是诸如 ls 这样的命令有命令行参数的原因。而在命令行中,所有的进程都是 bash 的子进程啊,换言之,所有的程序启动都是通过 exec* 这类函数启动的! 所以 ecec* 系列函数承担的是一个加载器的角色!
int execvp(const char *file, char *const argv[]);
这个可以理解为是 execv 和 execlp 的结合体
既可以直接传递替换目标程序的名称,也可以使用数组传递示例:
char* const myargv[] = {"ls", "-a", "-l", NULL};
int execvp("ls", myargv);	

所以,exec* 系列的系统调用能够执行系统命令,那自然也可以执行我们自己的可执行程序。(该demo采用 c 调 c++编译生成的可执行程序)

// test.c
int main()    
{    char* const myargc[] = {"ls", "-a", "-l", NULL};    pid_t id = fork();    if(id == 0)    {    printf("before: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());     execl("./otherExe", "otherExe", NULL);                                                              printf("after: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());    }                                                                  pid_t ret = waitpid(id, NULL, 0);                                  if(ret > 0) printf("wait success, ppid: %d, ret_id: %d\n", getppid(), ret);    return 0;    
}    // otherExe.cpp
int main()    
{    for(int i = 0; i < 5; ++i)    cout << "hello C++" << i << "\n";                                                                     
}   
# makefile
.PHONY:all    
all:test otherExe     
test:test.c    gcc -o $@ $^ -std=c99    
otherExe:otherExe.cpp    g++ -o $@ $^ -std=c++11                                                                                            
.PHONY:clean                
clean:                      rm -f test otherExe    

在这里插入图片描述

不仅可以 C 语言调用 C++ 编写的可执行程序,诸如 shell 脚本,python 等解释器语言都可以调用。换言之,运行起来,能被 cpu 调度的肯定是进程,所以不管是什么语言,运行起来最终在系统中都会变成进程,就能够被 exec* 类的函数调用。

换言之,我们自己写的可执行程序能被 exce* 调用,那么命令行参数、环境变量这些也能够通过一个程序传给另一个程序。

// test.c 核心调用代码
char* const myargv[] = {"otherExe", "-a", "-b", "-c", NULL};
execv("./otherExe", myargv);                                                              // otherExe.cpp
int main(int argc, char* argv[])
{cout << "这是命令行参数:\n";for(int i = 0; argv[i]; ++i)cout << "i: " << argv[i] << "\n";cout << "这是环境变量: \n";for(int i = 0; env[i]; ++i)cout << "i: " << env[i] << "\n";}

在这里插入图片描述

命令行参数在另一个程序中拿到了,没问题,因为 test 这个程序中调用了 exce* 系统调用,并且传递了一个 argv 数组。但是环境变量为什么也能获取呢??我可没有向另一个程序传递环境变量啊。

4.1 再谈环境变量

这就需要回归到一个问题,环境变量是何时传递给进程的?

在 C 库中有一个全局变量 *environ,在父进程的时候就已经被初始化,并且指向环境变量表了,当一个子进程被创建出来时,它是会以父进程为模板,拷贝其进程地址空间,页表等内核数据结构的,包括父进程的数据(环境变量也是数据)。而进程地址空间是由记录命令行参数、环境变量等信息的。换言之,即便不传参环境变量表,子进程也能通过继承下来的进程地址空间找到环境变量表,只要不发生写时拷贝,子进程指向的就是与父进程同一张表。并且在进程替换之后,环境变量信息不会被替换!

假设今天我有两种想要给子进程传递环境变量的场景,我该如何传递?

  1. 新增环境变量:
    可以直接在 shell 中 export 添加环境变量,因为环境变量具有全局属性,因此子进程也会继承来自 bash 的环境变量(包括新增的环境变量),而代码中 fork() 创建出来的子进程一样会继承父进程的环境变量,也包括新增的。

    在这里插入图片描述

    也可以通过 int putenv(char *string); 在代码层面上给进程新增环境变量,哪个进程调用,就给哪个进程 put 一个环境变量。

    putenv("PRIVATE_ENV=777");
    

    在这里插入图片描述

    现象:通过 putenv 往指定进程新增的环境变量,是该子进程 “独有” 的!其父进程是看不到这个新增的环境变量的。

  2. 彻底替换环境变量

    int execle(const char *path, const char *arg, ..., char * const envp[]);
    execle 其中的 e 即代表 env 环境变量的意思
    envp 不仅可以传递C库中的全局变量envrion,还可以传递自定义的环境变量
    并且在传递 envrion 时,程序中调用的 putenv 新增的环境变量也依旧有效
    而在传递自己定义的环境变量表时,则是采用直接覆盖的方式。示例:
    1. int execle("/usr/bin/ls", "ls", "-a", "-l", NULL, environ);
    2. char* myenv = {"ENV1=11111", "ENV2=2222",NULL};    execle("./otherExe", "otherExe", "-a", "-b", NULL, myenv);
    

    在这里插入图片描述

int execvpe(const char *file, char *const argv[], char *const envp[]);
execvp 再带一个环境变量的参数示例:
char* = {"ENV1=11111", "ENV2=2222",NULL};  
char* const myargv[] = {"ls", "-a", "-l", NULL};
int execvpe("ls", myargv, myenv);

5. execve 系统调用


在这里插入图片描述

上述讲的所有 exec* 类函数,都属于库函数,只有 exceve 是系统调用,而其它的 exec* 类函数与 exceve 这个系统调用唯一的不同点只是传数的不同,其它都是一样的。并且在底层,exec* 类函数最终都是调用的 execve 系统调用。


如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!

感谢各位观看!

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 亚马逊云(AWS)技术深度解析及代码使用案例
  • 华为od全面介绍!!!
  • Redis/ElaticSearch/kafka入门
  • 每日OJ_牛客_mkdir(排序+模拟)
  • android 离线的方式使用下载到本地的gradle
  • 【云原生系列之SkyWalking的部署】
  • 【QNX+Android虚拟化方案】112 - 获取 88Q5152 Switch Port1、Port2 端口的主从模式 / 传输速率 / 链路状态
  • C++系列-STL容器之list
  • C++中的异常处理与资源管理
  • 银河麒麟v10-sp3-x86系统安装k8s-1.30.4
  • 如何判断儿童是否患有自闭症
  • 数据结构--排序实现--C语言
  • uniapp解决app端不能用<web-view>将外部页面嵌入当前页面的问题
  • 如何查看在同一网段内的IP
  • 向量数据库Milvus源码开发贡献实践
  • 网络传输文件的问题
  • 【跃迁之路】【735天】程序员高效学习方法论探索系列(实验阶段492-2019.2.25)...
  • DOM的那些事
  • ES10 特性的完整指南
  • ES6 学习笔记(一)let,const和解构赋值
  • JavaScript设计模式系列一:工厂模式
  • JS进阶 - JS 、JS-Web-API与DOM、BOM
  • Magento 1.x 中文订单打印乱码
  • OpenStack安装流程(juno版)- 添加网络服务(neutron)- controller节点
  • Rancher-k8s加速安装文档
  • Redux 中间件分析
  • SpriteKit 技巧之添加背景图片
  • Xmanager 远程桌面 CentOS 7
  • 第十八天-企业应用架构模式-基本模式
  • 仿天猫超市收藏抛物线动画工具库
  • 干货 | 以太坊Mist负责人教你建立无服务器应用
  • 高程读书笔记 第六章 面向对象程序设计
  • 关于 Linux 进程的 UID、EUID、GID 和 EGID
  • 聊聊redis的数据结构的应用
  • 批量截取pdf文件
  • 适配iPhoneX、iPhoneXs、iPhoneXs Max、iPhoneXr 屏幕尺寸及安全区域
  • 体验javascript之美-第五课 匿名函数自执行和闭包是一回事儿吗?
  • 网络应用优化——时延与带宽
  • 用quicker-worker.js轻松跑一个大数据遍历
  • 【干货分享】dos命令大全
  • ionic入门之数据绑定显示-1
  • linux 淘宝开源监控工具tsar
  • 阿里云重庆大学大数据训练营落地分享
  • ​LeetCode解法汇总2304. 网格中的最小路径代价
  • #162 (Div. 2)
  • ()、[]、{}、(())、[[]]等各种括号的使用
  • (2)STM32单片机上位机
  • (二)springcloud实战之config配置中心
  • (附源码)spring boot公选课在线选课系统 毕业设计 142011
  • (亲测有效)推荐2024最新的免费漫画软件app,无广告,聚合全网资源!
  • (四)opengl函数加载和错误处理
  • (转)linux下的时间函数使用
  • .DFS.
  • .NET 4.0中的泛型协变和反变
  • .NET 6 Mysql Canal (CDC 增量同步,捕获变更数据) 案例版