目录
-int execl(const char *path, const char *arg, ...)
- int execv(const char *path, char *const argv[])
替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。exec就是将磁盘中的程序加载到内存,然后让他运行。
也就是磁盘中的数据和代码由A被替换为B,并对应到物理内存,页表,虚拟内存,PCB的过程,就是进程的替换。程序本质就是一个文件!
文件=程序代码+程序数据,如果想要进程执行一个全新的代码和数据,将要替换的文件加载到磁盘中,将原来进程的代码和数据替换掉,左侧相关的空间没有发生变化,这就意味着用一个老的进程的壳子,执行新的代码和数据段。这里没有创建新的程序。
替换函数
为什么从printf("aaaaaaaa\n");之后的代码都没有被执行呢?因为程序已经被替换啦。但是第一条函数还没有被替换,所以会被执行。、
程序替换的本质就是把程序进程的代码+数据,加载到特定的进程的上下文中。那么我们曾经在C/C++中,要将程序运行起来,必须先将代码和数据加载内存中,而这个加载使用的是加载器,加载器我们就可以理解成exec*(*代表所有以exec开头的函数)系列的程序替换函数。
为什么要程序替换
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值
下面这段代码,创建子进程后,父子进程会各自运行。当子进程进行程序替换的时候,父进程并没有受到影响。这是因为进程具有独立性,但是我们直到父子进程的代码是共享的,但是这里将子进程代码+程序替换,并没有影响到父进程,这是因为进程程序替换会更改代码区的代码,会发生写时拷贝。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
void test1()
{
pid_t id=fork();
if(id==0)//子进程
{
printf("I am a process,pid:%d\n",getpid());
sleep(5);
execl("/usr/bin/ls","ls","-n","-l",NULL);
//execl("/usr/bin/top","top",NULL);
printf("hahahahahaha\n");
printf("hahahahahaha\n");
printf("hahahahahaha\n");
printf("hahahahahaha\n");
exit(0);
}
while(1)//父进程一直在打印语句
{
printf("I am a father!\n");
sleep(1);
}
waitpid(id,NULL,0);//status不关心,设置等待状态为阻塞状态
printf("wait success!\n");
}
int main()
{
test1();
return 0;
}
//运行结果
[wjy@VM-24-9-centos 16]$ ./mycode
I am a father!
I am a process,pid:3840
I am a father!
I am a father!
I am a father!
I am a father!
total 40
-rw-rw-r-- 1 1001 1001 1290 Apr 16 22:18 !
-rw-rw-r-- 1 1001 1001 120 Apr 17 16:17 makefile
-rwxrwxr-x 1 1001 1001 8696 Apr 17 18:12 mycode
-rw-rw-r-- 1 1001 1001 1000 Apr 17 18:12 mycode.c
-rwxrwxr-x 1 1001 1001 8720 Apr 17 15:50 myproc
-rw-rw-r-- 1 1001 1001 2586 Apr 17 15:50 myproc.c
I am a father!
I am a father!
I am a father!
I am a father!
^Z
所以,当我们创建子进程的目的,在if else语句中,会让子进程执行父进程代码的一部分,如果我们想让子进程执行一个全新的程序,那么我们进行程序替换操作。
程序替换中,只要进程的程序替换成功,就不会执行后续代码,意味着exec*函数,调用成功后,不需要返回值。但只要exec*返回了,就一定是因为调用失败了!!
各个exec命令的理解
程序替换需要包含头文件#include <unistd.h>
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
可用man exec查看exec系列函数的用法,查看exec系列命令由7个
-int execl(const char *path, const char *arg, ...)
参数列表方式
path:要执行的目标程序的全路径,所在路径/文件名
arg:要执行的目标程序,在命令行上怎么执行,在传参的时候就怎样一个一个的传进去。命令行上怎么跑的,就怎么传。
... :可变参数列表,传参时可以传数量可变的若干参数。必须以NULL作为参数传递的结束。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
void test2()
{
if(fork() == 0)
{
//child
execl("/usr/bin/ls","-a","-l","-n","-i",NULL);
exit(1);
}
waitpid(-1,NULL,0);
printf("wait success!\n");
}
int main()
{
test2();
}
//运行结果
[wjy@VM-24-9-centos 16]$ ./mycode
total 40
655905 -rw-rw-r-- 1 1001 1001 1290 Apr 16 22:18 !
655893 -rw-rw-r-- 1 1001 1001 120 Apr 17 16:17 makefile
655908 -rwxrwxr-x 1 1001 1001 8776 Apr 17 18:56 mycode
655904 -rw-rw-r-- 1 1001 1001 1186 Apr 17 18:56 mycode.c
655896 -rwxrwxr-x 1 1001 1001 8720 Apr 17 15:50 myproc
655791 -rw-rw-r-- 1 1001 1001 2586 Apr 17 15:50 myproc.c
wait success!
int execlp(const char *file, const char *arg, ...)
file:因为p是自动搜索环境变量PATH,所以在写文件名的时候,就不用加文件的路径。因为p命令会直接搜索文件的路径。
const char* arg:因为execlp函数带有l和p,l是列表,所以后面需要加入可变参数。
execlp("ls","ls","-a","-l","-d",NULL);
//这里第一个和第二个ls不冲突,因为意义不同
int execle(const char *path, const char *arg, ..., char * const envp[]);
path:传入文件的路径+文件名
arg:可变参数列表,传入参数。
char* const envp[]:程序执行全路径,因为e代表自己维护环境变量,我们不想用系统给的默认环境变量,想自己传入环境变量。那么我们可以把环境变量信息传递给指定的被替换的进程。
【扩:想要用一个make命令执行两个文件的方法】
当我们想要执行两个可执行文件,但是指向执行一个make命令,那么怎么输入一个命令,就能执行两个文件呢?
因为make会自动执行makefile文件的第一个命令,所以我们用一个依赖文件,这个依赖文件命令是all,可以同时执行多个文件。
[wjy@VM-24-9-centos 17]$ cat makefile
.PHONY:all
all:myexe myload
myload:myload.c
gcc -o $@ $^ -std=c99
myexe:myexe.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -f myload myexe
我们在myexe.c文件中写入一些简单的打印语句并执行。那么我们是否可以在myload.c文件中的子进程中,将myexe.c文件中的信息打印出来呢?
//myexe中的文件
[wjy@VM-24-9-centos 17]$ cat myexe.c
#include <stdio.h>
int main()
{
printf("ahahahaha,I am your exe\n");
return 0;
}
//myload.c中的文件
execl("./myexe","myexe",NULL);//路径+文件名,要执行的文件名
//其他不变
//这样打印出myexe.c文件中的内容
//运行结果
[wjy@VM-24-9-centos 17]$ ./myload
commend begin...
ahahahaha,I am your exe
wait child success!
wait child success!
那么我们回归正题,execle函数比execl多了一个e,那么我们就可以传自定义环境变量。
//myload.c文件
char* env[]={
//自定义环境变量,但是平时不会这么写,这里只是举例子
"MYENV=hahahahahaha",
"MYENV1=hahahahahaha",
"MYENV2=hahahahahaha",
"MYENV3=hahahahahaha",
"NULL"
};
//执行路径,要执行什么,... ,环境变量
execle("./myexe","myexe",NULL,env);//NULL要放在env前面,参数列表这么写的
//myexe.c文件
[wjy@VM-24-9-centos 17]$ cat myexe.c
#include <stdio.h>
int main()
{
extern char**environ;//将环境变量传入,
//如果单执行myexe文件,导入的是系统环境变量
//如果将myexe.c文件传入myload中的execle函数,环境变量变成自定义的
for(int i=0;environ[i];i++)
{
printf("%s\n",environ[i]);
}
printf("my exe running ... done\n");
return 0;
}
//运行结果
[wjy@VM-24-9-centos 17]$ ./myload
commend begin...
MYENV=hahahahahaha
MYENV1=hahahahahaha
MYENV2=hahahahahaha
MYENV3=hahahahahaha
my exe running ... done
wait child success!
wait child success!
- int execv(const char *path, char *const argv[])
参数用数组
- path:要替换的参数路径/文件名
- char* const argv[]:指针数组,放的是execl中可变参数列表中的参数,将参数都放在数组中,之后将数组传给execv函数。
事实上,execv和execl本质上是一样的,execv就是把参数列表都放在一个数组中,这样比较方便维护。
//execl("/usr/bin/ls","ls", "-a","-l","-i",NULL);
//等价于
char* argv[]={"ls","-a","-l","-i","-n",NULL};
execv("/usr/bin/ls",argv);
int execvp(const char *file, char *const argv[])
file:不用传路径的文件名
argv:指针数组,写一个数组变量,里面放参数列表的名称,之后将数组传入execvp函数。
char* argv[]={"ls","-a","-l","-i",NULL};
execvp("ls",argv);
int execve(const char *file, char *const argv[],char *const envp[]);
根据以上的函数讲解,我们大概了解了exec后面参数的作用,execve也不难理解
file:无需写路径的文件名
argv:指针数组,数组中存放要执行的文件或命令
envp:自定义环境变量
通过以上对这些代码的学习,我们来实现一个python脚本,并用execl函数实现对另一个进程的替换。
//python代码
//test.py
[wjy@VM-24-9-centos 17]$ cat test.py
#!/ust/bin/python3
print("hello world");
//python代码运行结果
[wjy@VM-24-9-centos 17]$ python test.py
hello world
//myload.c代码
[wjy@VM-24-9-centos 17]$ cat myload.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
if(fork()==0)//子进程
{
printf("commend begin...\n");
execl("/usr/bin/python3","python","test.py",NULL);//路径,怎样执行,要执行的脚本
printf("command end...\n");//execl函数正确执行,这句话根本不能执行
exit(1);
}
waitpid(-1,NULL,0);
printf("wait child success!\n");
return 0;
}
//运行结果
[wjy@VM-24-9-centos 17]$ ./myload
commend begin...
hello world
wait child success!
总结:所有的接口,看起来没有太大差别,只有一个参数的不同。为什么会有这么多的接口?是为了满足不同的应用场景。事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。3的手册叫做库,2的手册叫做系统调用。别人把execve做了不同种类的封装,呈现出了不同的调用方式。
这些函数之间的关系如下图所示: