目录

替换原理

 替换函数

为什么要程序替换

 各个exec命令的理解

-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做了不同种类的封装,呈现出了不同的调用方式。

这些函数之间的关系如下图所示: