进程程序替换

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 系统调用。


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

感谢各位观看!

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值