创建子进程的目的有两个,一个是想让子进程执行父进程代码的一部分比如说下面的代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<sys/wait.h>
5 int main()
6 {
7 pid_t id=fork();
8 if(id==0)
9 {
10 //执行子进程的内容
11 }
12 else
13 {
14 //执行父进程的内容
15 wait(NULL);
16 }
17 return 0;
18 }
另外一个就是让子进程执行一个全新的程序,比如说我们在其他路径下写的一些可执行程序,因为系统指令本质上也是可执行程序所以我们也可以让子进程执行系统中的指令。第一种方法本质上就是让子进程执行父进程对应的磁盘代码中的一部分,而第二种用法本质上就是让子进程想办法加载磁盘上指定的程序让子进程执行新的代码和程序 。这里我们就把第二种方法称之为进程替换,这里大家要分清楚进程切换是不同的进程都有一个属于自己的时间片,cpu每次只能执行一个进程的数据和代码,所以为了保证多个进程能够正常的运行,cpu每次执行一个进程都只会执行时间片长度的时间,时间到了就换一个进程到cpu里面执行这叫进程切换,而进程替换是父进程创建子进程之后让子进程执行其他程序的代码而不是执行父进程代码的一部分,这里大家要注意一下,那如何来实现进程替换呢?这里就带着大家来先见见猪跑。
实现进程替换得用到函数execl这个函数的参数和返回值如下:
我们平时在执行程序的时候得告诉命令行这个可执行程序在哪里?以及采用什么样的方式来执行这个指令?那么这里也是同样的道理,在子进程中使用execl函数执行其他可执行程序时也得告诉函数可执行程序在哪个位置,以及以什么样的方式来执行这个程序(cmd 选项1 选项2…),那么参数path表示的意思就是可执行程序所在的位置,后面的可变参数列表表示的意思就是执行程序所用的方法,可变参数列表的意思就是该函数的参数个数由传参的个数决定,你传过来5个值那么此时的函数就有5个参数如果传过来3个值那么此时的函数就有3个参数,知道这些之后我们就可以写段代码来使用这个函数执行其他的程序,通过which指令可以查看指令所在的路径:
所以函数的第一个参数就可以填入这里的路径并且是以字符串的形式填入:
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 printf("process is running.....\n");
6 execl("/usr/bin/ls",)
7 return 0;
8 }
平时使用ls指令时可以添加一些选项比如说:-a -l -1等等,那调用函数时就得把指令和选项分开当成一个个字符串传递给这个函数并且参数的最后必须以空指针进行结尾,比如说下面的代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 printf("process is running.....\n");
6 execl("/usr/bin/ls","ls","-a","-l",NULL);
7 return 0;
8 }
程序替代完成之后就再使用printf函数打印一句话告诉使用者此时的程序已经完成,那么完整的代码就如下:
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 printf("process is running.....\n");
6 execl("/usr/bin/ls","ls","-a","-l",NULL);
7 printf("process running done....\n");
8 return 0;
9 }
这段代码的运行结果如下:
命令行中ls -al指令的运行结果如下:
虽然这里执行的都是ls -al指令但是运行的结果却有点点不一样,命令行中的运行结果带有演示而execl函数运行的结果却不带颜色,这是因为命令行中的ls自带–color =auto这个选项,把这个选项加上就可以实现颜色的变化,那么这里的代码如下:
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 printf("process is running.....\n");
6 execl("/usr/bin/ls","ls","-a","-l","--color=auto",NULL);
7 printf("process running done....\n");
8 return 0;
9 }
代码的运行结果如下:
这里运行的结果就出现了颜色。这里execl执行的是系统自带的程序,我们也可以用该函数执行我们自己写的程序,当前程序所在的路径为:
该用户的家目录的树状图如下:
在这个用户的家目录下也有个可执行程序process,那这里是不是就可以使用execl函数里面执行这个可执行程序啊,这个程序所在的路径如下:
并且这个程序没有选项所以这个程序执行的方法就是./process
当然这里的方法也可以直接写成process
不需要添加上面的相对路径,那么修改之后的代码就是:
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 printf("process is running.....\n");
6 execl("/home/xbb/process","./process",NULL);
7 printf("process running done....\n");
8 return 0;
9 }
这段代码的运行结果如下
那么这就证明execl函数既可以让程序执行系统自带的指令,也可以让程序执行我们自己写的可执行程序。但是大家仔细观察这个运行结果就会发现这里的执行结果好像不大对吧,在execl函数后面我们还使用了一个printf函数但是程序把execl函数执行完之后并没有执行printf函数那这是为什么呢?是printf函数和execl函数不兼容吗?还是execl函数使用的方法出现了问题呢?那要想知道这个问题我们就看看execl函数的原理。
我们先来聊聊进程替换的返回值,使用进程替换函数也是有返回值的,如果替换失败了就会返回-1比如说下面的代码,我们给execl函数传递一个不存在的路径和执行方法就可以发现execl函数的返回值是-1
1 #include<stdio.h>
2 #include<unistd.h>
#include
#include
3 int main()
4 {
5 pid_t id=fork();
6 if(id==0)
7 {
8 //子进程的代码
9 int x= execl("/abcdefg/hi","hi",NULL);
10 if(x==-1)
11 {
12 printf("execl的返回值为-1\n");
13 }
14 }
15 else
16 {
17 //父进程的代码
18 printf("我是父进程\n");
wait(NULL);
19 }
20 return 0;
21 }
因为程序不存在这个文件,所以函数execl的返回值就为-1,那么这段代码的运行结果如下:
好!进程替换失败的返回值我们知道了,那进程进程替换成功的返回值是多少呢?我们来看看下面的代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<sys/wait.h>
5 int main()
6 {
7 pid_t id=fork();
8 if(id==0)
9 {
10 //子进程的代码
11 int x= execl("/usr/bin/ls","ls","-a","-l",NULL);
12 printf("execl的返回值为%d\n",x);
13 }
14 else
15 {
16 //父进程的代码
17 printf("我是父进程\n");
18 wait(NULL);
19 }
20 return 0;
21 }
这段代码的运行结果如下:
这里好像没有打印出来execl函数的返回值,并且连printf函数都没有正常执行那这是为什么呢?原因很简单首先在没有执行fork函数之前操作系统中就只有一个父进程,这个父进程有对应的数据区和代码区:
当执行fork函数之后操作系统中就会多出来一个子进程,并且操作系统也为这个子进程创建页表,虚拟地址空间和PCB但是这些东西的内容都和父进程一样,所以子进程的页表也指向物理空间上的那块区域,也就是说此时的子进程和父进程是共用内存上同一块数据区和代码区
当子进程调用execl函数时会将磁盘上的程序B加载进内存并替换原来的子进程所指向的数据区和代码区,但是进程之间是由独立性的,当前内存中的程序A的数据区和代码区不仅仅子进程在使用父进程也再使用,所以这时execl函数再发生替换时会发生写诗拷贝,操作系统会在内存上再开辟一个空间用来存放程序B的数据和代码,再将子进程的页表指向新的空间,这样子程序就能执行程序B的代码:
当子进程执行execl函数时操作系统会再开辟一个空间,并且在新空间中只存放着新程序中的代码,那这个空间还和原来父进程的数据代码有关系吗?答案是没有任何关系的,他是一个全新的内容所以执行完execl函数之后就就不会执行execl函数后面的内容,所以execl函数替换成功之后的返回值是啥也就不重要了反正都被替换了对吧,那这里大家可以通过下面的代码来理解理解
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<sys/wait.h>
5 int main()
6 {
7 pid_t id=fork();
8 if(id==0)
9 {
10 //子进程的代码
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
11 int x= execl("程序A的地址","程序A的使用方法",NULL);
12 printf("execl的返回值为%d\n",x);
13 }
14 else
15 {
16 //父进程的代码
17 printf("我是父进程\n");
18 wait(NULL);
19 }
20 return 0;
21 }
execl函数执行完之后程序就会变成这样:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<sys/wait.h>
5 int main()
6 {
7 pid_t id=fork();
8 if(id==0)
9 {
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
10 //程序A的代码和数据
.......
//程序A的代码执行完子进程就结束
11
12
13 }
14 else
15 {
16 //父进程的代码
17 printf("我是父进程\n");
18 wait(NULL);
19 }
20 return 0;
21 }
那么这就是程序替换的原理希望大家能够理解。
实现程序替换不止execl函数还有execlp,execv,execvp,execle,execve函数,那接下来我将一个一个介绍这些函数的用法。
这个函数所需要的参数类型如下:
通过上面的学习我们知道使用这个函数得告诉这个函数程序所在的地址和这个程序所需要的方法,这里传方法的时候得将文件名和选项作为字串进行传递,但是最后得以NULL结尾,比如说下面的代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 printf("process is running.....\n");
6 execl("/usr/bin/ls","ls","-a","-l",NULL);
7 return 0;
8 }
这个函数的参数如下:
这个函数相较于execl多了一个p,这个p表示的意思就是path也就是环境变量中的path
环境变量中的PATH记录着各种操作系统指令所在的地址,所以使用这个函数替换程序时不需要传地址,直接传程序名就行,因为这个函数会自动在PATH所记录的路径查找这个程序,这个函数的第一个参数就对应着程序名,比如说下面的代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 printf("process is running.....\n");
6 execlp("ls","ls","-a","-l",NULL);
7 return 0;
8 }
这个函数里面有两个ls字符串,虽然内容相同但是这两个字符串表示的意思是不一样的,第一个ls表示的是程序名第二个ls表示的是执行程序的方法,那么这段代码的运行结果如下:
这个函数的参数如下:
这个函数的v表示的是数组的意思也就是说将程序的所有指令全部放入到数组中比如说下面的代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 printf("process is running.....\n");
6 char *const arr[]={"ls","-a","-l",NULL};
7 execv("/usr/bin/ls",arr);
8 return 0;
9 }
这个函数的参数如下:
这个就是将v,p组合到一起也就是说这个函数需要传的指令的名称和指令用法所组成的数组就可以了,比如说下面的代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 printf("process is running.....\n");
6 char *const arr[]={"ls","-a","-l",NULL};
7 execvp("ls",arr);
8 return 0;
9 }
这段代码的运行结果也和上述一致,这里就不多展示。
这个函数的参数如下
这个函数就多一个参数第三个参数表示的意思是环境变量,如果子进程要用到自定义环境变量或者系统的环境变量的话就可以用到第三个参数,比如说在当前路径下再创建一个文件:
1 #include<stdio.h>
2 #include<stdlib.h>
3 int main()
4 {
5 printf("MYENV : %s\n",getenv("MYENV"));
6 printf("PATH : %s\n",getenv("PATH"));
7 printf("PWD : %s\n",getenv("PWD"));
8 return 0;
9 }
这个可执行程序的文件名为:mybin,然后myproc文件里面的代码如下:
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 printf("process is running.....\n");
6 char *const envp[]={
7 "MYENV=112233445566",
8 NULL
9 };
10 execle("./mybin","mybin",NULL,envp);
11 return 0;
12 }
这里创建了一个envp数组这个数组里面装的就是自定义环境变量,使用execle函数时就可以把这个数组作为参数那这段代码的运行结果如下:
我们可以看到当我们以数组的方式将自定义函数传给execle函数时,被替代的程序里面只有自定义环境变量,原本的系统环境变量就不见了,这里的参数是字符指针数组,传参的时候传的是首元素的地址,所以这里可以将系统提供的environ指针作为参数传递给替换的程序,那这里的代码如下:
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 extern char** environ;
6 printf("process is running.....\n");
7 char *const envp[]={
8 "MYENV=112233445566",
9 NULL
10 };
11 execle("./mybin","mybin",NULL,environ);
12 return 0;
13 }
代码的运行结果如下:
把环境变量指针作为参数传递给函数时这里的运行结果就又不一样了,系统的环境变量可以打印出结果可是自定义环境变量就无法打印出结果,所以大家要是想使用自定义环境变量和系统的环境变量的话就得先使用putenv函数将自定义环境变量加载进系统环境变量里面去,然后再传environ指针,putenv函数的参数如下:
那么改进之后的代码如下:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 int main()
5 {
6 extern char** environ;
7 printf("process is running.....\n");
8 putenv("MYENV=11223344");
9 execle("./mybin","mybin",NULL,environ);
10 return 0;
11 }
这段代码的运行结果如下:
我们可以看到这里即打印了自定义环境变量的值,又打印了环境变量的值。那这里就存在一个问题execle函数的作用就是就是将我们的程序加载到内存当中,那这里是如何加载的呢?答案是linux中使用exec类的接口将程序加载进的内容,我们把这些接口称之为加载器,main函数也是一个函数,main也得被别人调用被别人传参,所以在加载程序的同时exec类的接口就会给main函数进行传参,main函数中的第一个参数表示指令生成字串的个数,第二个参数表示字串的内容,第三个参数表示环境变量,那这不就是execle函数参数吗?execl的第二个参数可以得到main函数所需的字串个数和字串的内容,execl的第三个参数就是环境变量,那么这就是execle函数的内容,希望大家理解,虽然前几个exec*类的函数没有环境变量参数,但是这些函数执行的过程中程序依然是可以得到环境变量的内容,原因就是environ指针变量的存在,在创建子进程的时候在将进程加载进内存的时候操作系统都会给子进程分配虚拟地址空间,而虚拟地址空间上就有一个地方专门存储着环境变量的内容,而environ指针就刚好指向这个空间的开始,通过对指针的加减就可以得到环境变量的所有内容,这里希望大家能够理解。
有了上面的基础这个函数如何使用简直易如反掌e表示环境变量,p表示直接传环境变量名即可,v表示这里需要方法组成的数组,那这个函数的参数就如下:
这里就不多赘述。
上面讲了那么多的函数,但是这些函数都有一个共同的特点就是这些函数都是c语言函数提供的,系统提供了一个函数调用接口execve,这个函数的参数如下:
我们可以看到这个函数来自于二号手册,这个手册里面的函数都是系统调用函数也就是系统提供的函数,有了上面的经历那这个函数的参数一眼就能看懂,第一个参数是文件名,第二个参数是文件的使用方法所组成的数组,第三个参数就是 环境变量 ,所以 上面所用到的那么多函数其实底层实现都来自于execve函数,之所以有那么多不同的功能和参数其最终目的就是方便我们日常的使用,那这就是本篇文章的全部内容希望大家能够理解。