本章代码gitee仓库:进程替换
文章目录
🌼1. 单进程的程序替换
在Linux中,存在一批接口,可以进行程序的替换
先多的不说,先来看看猪跑
cpp
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("before:I am process,pid:%d,ppid:%d\n",getpid(),getppid());
execl("/usr/bin/ls","ls","-a","-l",NULL);
printf("after:I am process,pid:%d,ppid:%d\n",getpid(),getppid());
return 0;
}
我们发现,在excel
之后的代码,没有执行,而是执行了我们替换的ls
指令
🌻2. 进程替换的原理
在fork
之后创建的子进程会执行父进程的代码接下里的,而调用exec
函数会直接替换当前的代码和数据,并不会再创建一个新的进程。
当我们调用execl
时,会先将/usr/bin/ls
目录下的ls
加载到内存当中,这个进程就会将自己的代码和数据直接替换为ls
的代码和数据,重新开始运行ls
🌴3. 多进程程序替换
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("before:I am process,pid:%d,ppid:%d\n",getpid(),getppid());
execl("/usr/bin/ls","ls","-a","-l",NULL);
printf("after:I am process,pid:%d,ppid:%d\n",getpid(),getppid());
exit(1);
}
pid_t ret = waitpid(id,NULL,0);
if(ret>0) printf("wait success,father pid:%d,ret id:%d\n",getpid(),ret);
return 0;
}
运行发现,子进程的程序替换并不影响父进程的运行,这是因为写时拷贝不仅将数据进行了写时拷贝,同时也将代码进行了写时拷贝。
另外,我们看到子进程的先前的进程pid
为1047 ,最后父进程回收子进程的时候,回收子进程的pid
也是1047 ,这就能够充分的说明进程替换并不会创建新的进程!
Tips:
我们这里发现,
exec*
系列的函数,程序替换成功之后,后续的代码不会执行,如果替换失败了,才会执行后续的代码,这就代表着exec*
函数,只有失败的返回值,并没有成功的返回值
🌳4. 程序替换接口 -- exec*
关于exec*
系列的接口一共有7个,他们的开头都说exec
我们目前常用的是
man
的1、2、3号手册:
- 1号手册:用户命令手册
- 2号手册:系统调用
- 3号手册:C语言库函数
🌿execl
c
int execl(const char *path,const char *arg,...);
这里的l
可以理解成list
,这里传参是可变参数,从第二个参数开始,我们在命令行中如何写的,那么我们就如何将参数传给我们的函数,只不过是将空格换成逗号,然后最后一个参数传NULL
。
我们要执行这个程序,首先第一件事肯定是找到这个程序在哪儿,所以,第一个参数就表示这个程序的路径。
🌿execlp
c
int execlp(const char *file,const char *arg,...);
这里的p
就是我们的path
环境变量,我们可以不指定路径,它会直接去PATH
环境变量里面找有没有这个可执行程序。
当然了,这里我们第一个参数和第二个参数虽然是一样的,但是含义不一样,第一个表示先找到这个程序,第二个表示如何执行。
🌿execv
c
int execv(const char *path,char* const argv[]);
这里的v
就可以理解为vector
,第一次参数同样是表示路径;第二个参数是一个字符指针数组,将命令行参数的地址直接填到这个数组里面。
那这样我们就能将命令行参数填到这个数组里面,然后直接把这个数组进去即可
**Tips:**例如指令
ls
,它也是一个被编译好的程序,那它也是有main
函数的,也有对应的命令行参数,那它这个命令行参数,其实也是从execv
的第二个参数传进来的;另外,execl
我们已列表的方式传进来,最后也是会被变成char* const argv[
这个样子。然后再将这个argv
参数传到对应的main
函数当中。在**命令行当中所有的进程都是
bash
的子进程**,那么所有进程都是采用exec*
系列函数进行启动执行的。所以
exec*
系列的函数,起到的是一个代码级别的加载器作用,将我们的可执行程序导到内存里面
🌿execvp
c
int execvp(const char *file, char *const argv[]);
有了前三个函数的例子,我们看到这个p
,其实就能猜到,这里的p
就表示PATH
环境变量。这里就不再过多赘述了
🌿execle && execvpe
c
int execle(const char *path, const char *arvg, ..., char *envp[]);
int execvpe(const char *file, char *const argv[], char *const envp[])
这里带的e
,其实就是表示environment
环境变量。
如果
exec*
系列函数能够执行系统命令,那可否执行我们的自己的程序呢?我们用C语言程序来调用C++做一个示范
makefile
会自顶向下扫描遇到的第一个目标文件是mycommand
,所以会执行mycommand
的方法,而我们的cppExe
和先扫描到的没有任何关联,那么就结束了。所以我们让第一个目标文件是一个伪目标,然后再建立依赖
makefile.PHONY:all all:mycommand cppExe mycommand:mycommand.c gcc -o $@ $^ -std=c99 cppExe:cppExe.cpp g++ -o $@ $^ -std=c++11 .PHONY:clean clean: rm -f mycommand cppExe
这里补充一点:前面讲到,命令行怎么输,那么我们的参数就如何传,可是我们执行
cppExe
的时候,命令行是./cppExe
,而这里传的参数确实cppExe
,这里是因为我们命令行中带上./
是要告诉bash
这个可执行程序在哪儿,而在execl
中,第一个参数已经说明了在当前目录,所以第二个参数就不需要再带上./
了。当然了,这也不只是可以调用C++,类似Java、python这些都是可以进行调用的
这些跨语言调用,其实也说明了,所以的语言运行起来,本质上都是进程!
有了上面C语言调用其他语言的例子,我们就能验证,一个程序是否可以给另一个程序传入环境变量参数和命令行参数了。
验证发现,这里传入的命令行参数拿到了,而环境变量压根没有传入,但是也能拿到。
这是因为环境变量也是数据,创建子进程的,环境变量就已经被子进程继承了。
当我们不写这些
int argc
、char *argv[]
、char *env[]
的时候,也能传进来。有个
extern char **environ
第三方变量,它直接指向了环境变量信息,当它被定义的时候,就已经被父进程初始化指向了自己的环境变量表,所以再创建子进程的时候,这个变量就被继承下去了。
同时,我们这里做的是程序替换,而程序替换是直接替换代码和数据,而环境变量并没有被替换掉,所以程序替换中,环境变量信息不会被替换。
如果我们想自己手动给子进程传递环境变量,也是可以的,这分为2种:
- 新增环境变量
- 我们可以从
shell
直接导入环境变量;
- 也可以通过父进程创建环境变量导入子进程,采用库函数
putenv
给进程导入环境变量,当然,我们这里的导入的环境变量和父bash
并没有关系。
- 我们可以从
- 直接替换环境变量
如果需要直接替换掉,那么就可以用execle
或者execvpe
,这里采用execle
举例子,因为这个更为复杂
如果我们需要自己的环境变量,我们就可以自定义环境变量,不要父进程的环境变量
以上这6个都是C语言封装的库函数,还一个接口,是系统提供的,本质上就是上面这6都都是调用的这个接口: