程序替换效果演示
c
#include<stdio.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;
}
我们可以看到,通过这一段代码,我们能够在 C
语言的程序中执行系统的命令!
其中 execl
函数就是程序替换的接口,我们发现程序替换成功之后,后续的代码没有被执行:printf("after: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
程序替换的概念及原理
在Linux中,程序替换是指一个正在运行的进程用另一个程序来替代的过程。新程序取代了原始程序的内存空间和执行上下文。这个过程中,原始程序的代码、数据和堆栈都被新程序替代。
上图中是一个程序替换的示意图,对一开始的代码做出分析!我们将刚才演示的代码编译成可执行程序之后,会被放在磁盘中!./test
执行这个可执行程序,他就会被加载到内存中!当执行到 execl
函数时,原进程的代码和数据就会被 ls
这个可执行程序替换,从头开始执行 ls
这个可执行程序的代码!
其中绿色框框的那一部分是不会改变的,页表中虚拟地址到物理地址的映射可能会改变!因为当新程序的代码和数据比较大时就会多申请一些内存空间,当新程序的代码和数据比较小时就会释放一部分空间!
-
CPU 如何知道新程序从哪里开始执行代码?
- 在编译形成可执行程序的时候,程序代码执行的起始地址就已经编译到了可执行程序中了!程序替换之后就能找到程序的入口并正确向后执行!具体会在以后讲程序加载的时候详解!
-
子进程执行程序替换,会影响父进程吗?
- 答案显然是不会哈!子进程在调用程序替换的接口时,本质上就是在对父进程的代码做出修改,操作系统检测到子进程在修改父进程的代码,就会发生写时拷贝!写时拷贝的本质就是申请内存空间,写时拷贝之后就能进行程序替换啦!这么分析的确不会影响父进程!可以写个代码来验证:
c#include <stdio.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 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()); } else if (id > 0) { pid_t ret = waitpid(id, NULL, 0); if(ret > 0) { printf("wait %d success\n", ret); } } else { perror("fork"); } return 0; }
可以看到:子进程发生程序替换之后,父进程的代码并没有收到影响,必然父进程不可能成功等待子进程!
-
代码也能发生写时拷贝?
- 的确可以,代码是只读的只是针对上层用户而言的!操作系统完全有这个权限,有这个能力去修改进程的代码,因为操作系统是进程的管理者嘛!这也侧面印证了物理内存根本没有权限管理的概念!
-
程序替换是否有创建新的进程?
- 这个是没有的!程序替换只进行程序代码和数据的替换,不会创建新的进程,不会释放
task_struct
等内核数据结构,只需要对页表等做一定层度上的修改即可!
从第二个问题中父进程等待子进程成功的例子就能看出,如果创建了新的进程父进程是不可能等待子进程成功的!
- 这个是没有的!程序替换只进行程序代码和数据的替换,不会创建新的进程,不会释放
-
为什么
execl
之后的代码没有被执行呢?- 这个应该很简单,
execl
之后的代码属于原来进程的代码和数据,execl
之后代码和数据就被新程序替换了,不被执行是很正常的事情!如果说程序替换的时候发生了失败,那么原来的进程execl
之后的代码才会被执行!这就意味着只有当程序替换的函数调用失败之后才会有返回值!
- 这个应该很简单,
-
一个小知识:
Linux
中形成的可执行程序是有一定格式的:ELF
格式,可执行程序的入口地址就在这个表的表头中,表头中还有虚拟内存区域划分的起始地址等等!
学习各种程序替换接口
我们看到程序替换的接口还是比较多的哈!不过他们之间还是有一定的规律的!可以通过他们的名字记忆怎么使用!
这些函数的本质:第一个参数:找到可执行程序;第二个参数:可执行程序的执行方法。
execl
可以看到这些所有的接口都是 exec
开头的!
execl
的第一个参数是你要执行的可执行程序的路径!execl
的第二个参数是一个可变参数哈!我们在学习 C 语言的时候,scanf
printf
不就是两个有可变参数的函数嘛!第二个参数表示你想怎么执行这个可执行程序。比如你在命令行执行ls
命令的时候:ls -a -l
那么这第二个参数就是"ls", "-a", "-l", NULL
。- 为什么要加一个 NULL 呢?你还记得我们在讲命令行参数的时候提到的命令行参数表吧!那个表的结尾就有一个 NULL。我们用的程序替换的接口中,传入执行方式的参数都必须加 NULL,其实传入的执行方式就是传给你要执行程序的
char* argv[]
,即给新的程序传入命令行参数!所以要传入一个NULL
。
怎么记忆呢?
execl
中除了 exec
还有一个 l
我们可以把 l
理解为 list
,list
在 C++
就是链表嘛,链表不是一个一个的节点,表明我们要将新程序的执行方式拆分称为一个一个的 选项嘛!
这个函数的效果在一开始就演示了,这里就不再重复演示啦!
execlp
这个函数中除了 l
还多了一个 p
这个 p
可以理解为 PATH
表示这个函数会在 PATH
环境变量中寻找你要执行的可执行程序!这个 l
就跟刚才那个一毛一样!
c
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("before: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
execlp("ls", "ls", "-a", "-l", NULL);
printf("after: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
return 0;
}
可以看到也是成功执行了程序替换!execlp
的第一个参数也可以传路径哈,只不过我们讲的是这个函数的标准使用方法!
请结合这些函数的本质理解!
execv
这个函数没有 l
了,变成了一个 v
这个 v
可以理解为 vector
,vector
在 C++
中表示一个数组嘛!表示新程序的执行方式要通过一个字符串数组传递!这个数组的末尾也要带上 NULL 哦!请结合传递执行方式的本质来理解
并且这个函数中没有 p
说明第一个参数需要传递可执行程序的路径!
c
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("before: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
char* argv[] = {
"ls",
"-a",
"-l",
NULL
};
execv("/usr/bin/ls", argv);
printf("after: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
return 0;
}
execvp
这个函数中有 v
表示执行新程序的方式需要用数组传递,这个函数中还有 p
表示新的程序会在 PATH
环境变量中查找,我们只需要指定新程序的名称就可以啦!
c
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("before: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
char* argv[] = {
"ls",
"-a",
"-l",
NULL
};
execvp("ls", argv);
printf("after: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
return 0;
}

execle
这个函数中有个 l
说明传递新程序执行方法时需要一个一个选项地传递;这个函数中没有 p
说明传递第一个参数时需要指定可执行程序的路径!
我们还发现这个函数中多了一个 e
这个 e
就是环境变量的意思,也就是说通过这个函数我们可以自定义地为新程序传入环境变量!我们这里就以 char** environ
进行演示啦!environ
变量是库提供的一个全局变量,指向父进程的环境变量表,environ
在讲解环境变量的时候讲过,环境变量表在 Linux
的命令行参数讲过!
c
#include <stdio.h>
#include <unistd.h>
extern char** environ;
int main()
{
printf("before: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
execle("/usr/bin/ls", "ls", "-a", "-l", NULL, environ);
printf("after: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
return 0;
}

execvpe
这个函数带了字母 v
, p
, e
想必你已经知道该怎么使用了吧!这里就不在做讲解了哦!
exec 系列函数执行其他可执行文件
我们看到说明文档的描述是:执行一个文件!(execute a file) 这就说明不是非要执行可以由 C
语言形成的可执行文件哇!
execl 执行脚本文件
脚本文件就是一个文本文件,只不过需要特定的解释器来解释文本文件的字符串!脚本文件的后缀可以是 .sh
,我们就可以写一点 Linux
下的命令,然后用 bash
命令行解释器来执行这个脚本文件!
这是 test.sh
中的代码:
bash
echo "这是一个脚本文件"
ls -a -l
这是 C
语言代码:
c
#include <stdio.h>
#include <unistd.h>
int main()
{
execl("/usr/bin/bash", "bash", "test.sh");
return 0;
}
我们找到这个可以执行的 bash
命令行解释器,然后用 bash
命令行解释器来执行这个脚本文件:
execl 执行 .py
文件
python
是一种解释型语言,需要用 python
解释器来一行一行地解释执行 python
代码!
test.py
中的代码:
python
def function():
for i in range(5):
print("hello python!")
function()
test.c
中的代码:
c
#include <stdio.h>
#include<unistd.h>
int main()
{
execl("/usr/bin/python", "python", "test.py", NULL);
return 0;
}
我们可以看到也是成功执行起来了 .py
文件好吧!
运行 C++
形成的可执行文件
这是一个 C++
的代码:
cpp
#include<iostream>
using namespace std;
int main()
{
cout << "hello linux said by cplusplus" << endl;
return 0;
}
我们先用 g++ -o testcpp test.cpp
编译形成一个可执行程序!
然后我们来使用 execl
调用这个可执行程序!
这里为什么执行新程序的方式不是 ./testcpp
呢?这就得弄清楚我们之前执行我们自己写的可执行程序为什么要加 ./
了!这个的原因已经在讲环境变量的时候讲过啦!就是为了找到这个可执行程序了嘛!execl
的第一个参数已经能够找到 testcpp
这个可执行程序了,第二个参数就没必要加啦!
c
#include <stdio.h>
#include<unistd.h>
int main()
{
execl("./testcpp", "testcpp", NULL);
return 0;
}

原因
无论是可执行程序,还是脚本,为什么能够跨语言调用呢?
- 所有的语言不管他借助什么东西运行起来,在现代操作系统中他一定是进程!是进程就一定能够别其他进程调用!只要一个语言提供了调用进程的接口,那么理论上他就可以调用任何一个进程,表现出来就是可以调用其他语言形成的可执行程序 (执行其他语言的代码)!
传递命令行参数
前面在讲程序替换的接口时提到,传递执行方式的本质就是给新的程序传递命令行参数,如何验证呢?
很简单,我们可以利用命令行参数表将命令行参数打印出来嘛!
我们用刚刚的 C++
文件来吧,C++
跟 C
一样都有命令行参数表和环境变量表哈!
cpp
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
cout << "test.cpp 文件的命令行参数:" << endl;
for (int i = 0; argv[i]; i++)
{
cout << i << " : " << argv[i] << endl;
}
return 0;
}
我们在 test.c
文件中就多传一点命令行参数,看他能不能被 testcpp
拿到哈:
c
#include <stdio.h>
#include<unistd.h>
int main()
{
execl("./testcpp", "testcpp", "-a", "l", "-w", NULL);
return 0;
}
这里我们就用到了 makefile
编译多个源文件生成多个可执行程序的编写方法,忘记了的 uu
可以复习一下哦!➡️一文搞懂 makefile
bash
All:test testcpp
test:test.c
gcc -o $@ $^
testcpp:test.cpp
g++ -o $@ $^
.PHONY:clean
clean:
rm -f clean

这个有什么用呢?你想想这个是我们自己写的可执行程序嘛!如果我们要执行的新的程序是系统的命令,我们就能自己模拟实现一个命令行解释器啦!
传递环境变量
前面提到可以给新程序传递环境变量!那我们不传环境变量,新程序还会有环境变量嘛?
test.c
的代码:可以看到我们没有为 testcpp
传递环境变量哦!
c
#include <stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
execl("./testcpp", "testcpp", "-a", "l", "-w", NULL);
}
else if(id > 0)
{
waitpid(id, NULL, 0);
}
return 0;
}
我们在 testcpp
中打印环境变量:
cpp
#include <iostream>
using namespace std;
int main(int argc, char *argv[], char* env[])
{
cout << "test.cpp 文件的命令行参数:" << endl;
for (int i = 0; argv[i]; i++)
{
cout << i << " : " << argv[i] << endl;
}
cout << "test.cpp 文件的环境变量: " << endl;
for(int i = 0; env[i]; i++)
{
cout << env[i] << endl;
}
return 0;
}
我们并没有传递环境变量,testcpp
还真就将环境变量打印出来了!
这是为什么呢?在学习环境变量的时候,我们知道一个进程的环境变量来自于两个地方:
- 要么是通过
main
函数传递进来的! - 要么就是从父进程那里继承过来的!
这个例子中我们没有传递环境变量表,那么只能说明新程序的环境变量是从父进程那里继承过来的!
这能说明什么问题呢?
- fork创建的子进程会继承
test
进程的环境变量,当我们在子进程中使用程序替换接口,能在新程序中打印出来环境变量,只能说明这个环境变量是来自原来的子进程!因为我们并没有给新程序传递环境变量!这就说明程序替换并不会替换原进程的环境变量
为新程序添加环境变量
为 bash
添加环境变量
我们自己写的可执行程序一旦运行起来就是 bash
的子进程!我们在自己写的程序中创建子进程,然后在子进程中调用程序替换接口!这就意味着只要为 bash
添加环境变量之后,环境变量会通过 bash->自己写的程序->子进程
的顺序一路继承给子进程,最后给到新的程序!
为父进程添加环境变量
根据上面个的原理,我们可以拦腰截断,为父进程添加环境变量,然后继承给子进程,最终给到新程序!
在学习环境变量的时候,我们学到了为当前进程添加环境变量的函数 putenv
这里就可以用起来啦!
我们使用 putenv
为父进程导入一个环境变量,看看能否被新程序拿到哈!
c
#include <stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
putenv("MY_VALUE_1=123456");
pid_t id = fork();
if(id == 0)
{
execl("./testcpp", "testcpp", "-a", "l", "-w", NULL);
}
else if(id > 0)
{
waitpid(id, NULL, 0);
}
return 0;
}
环境变量比较多哈,我们使用 grep
命令过滤一下,发现新程序中的确有新导入的环境变量呢!
使用程序替换接口
在程序替换接口的使用部分,我们使用 execle
给新程序传递了 environ
指向的环境变量表,可是我们就是想传递自己的环境变量应该怎么做呢!
很简单哈,我们看到 execle
的最后一个参数是字符串数组哈,我们只需要传递一个字符串数组过去就行,环境变量表传递嘛,数组最后必须🉐有 NULL
哦!
c
#include <stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
char* env[] = {
"MY_VALUE_2=123455",
"MY_VALUE_3=123213123",
"MY_VALUE_4=2343243",
NULL
};
execle("./testcpp", "testcpp", "-a", "l", "-w", NULL, env);
}
else if(id > 0)
{
waitpid(id, NULL, 0);
}
return 0;
}

可以看到这么传递之后,原来的一大坨的环境变量就没啦!可以看出通过程序替换接口传递环境变量是通过替换的方式传递哒!
- Linux 程序替换的本质
- Linux 程序替换的接口以及参数理解
- 如何为新程序传递命令行参数
- 如何为新程序传递环境变量