创建子进程的目的
创建子进程的第一个目的是让子进程执行父进程对应的磁盘代码中的一部分, 第二个目的是让子进程想办法加载磁盘上指定的程序,让子进程执行新的代码和程序
一是让子进程执行父进程代码的一部分, 比如:
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每次执行一个进程都只会执行时间片长度的时间, 时间到了就硬件上下文把信息保存在PCB, 然后换下一个进程到cpu里执行这叫进程切换.
如何实现进程替换
如何来实现进程替换呢?
实现进程替换得用到函数execl:
平时执行程序的时候需要告诉bash可执行程序在哪里, 以及采用什么样的方式调用执行这个命令, 这里也是同样的道理, 在子进程中使用execl函数执行其他可执行程序时也需要给函数传相应的参数如可执行程序位置, 以及以什么样的方式来执行这个程序(cmd 选项1 选项2…)
参数path就是可执行程序所在的位置, 后面的可变参数列表是可执行程序所用的方法, 后面的...是可变参数列表, 该函数的参数个数由传参的个数决定(命令行里怎么写的,就怎么传参),最后必须以NULL结尾,表示参数传递完毕..
单进程版的程序替换代码:
系统指令本质上是可执行程序所以我们也可以让子进程执行系统中的指令:
1 #include<stdio.h>2 #include <unistd.h>3 int main()4 {5 printf("pid: %d, exec command begin\n",getpid());6 execl("/usr/bin/ls","ls","-l","-a",NULL); 7 printf("pid: %d, exec command end\n",getpid());8 return 0;9 }
平时使用ls指令时可以添加一些选项比如说:-a -l等, 那调用函数时就得把指令和选项分开当成一个个字符串传递给这个函数并且参数的最后必须以NULL进行结尾:
命令行中ls是ls --color=auto的别名, 所以会显示颜色:
在代码中加上"--color=auto":
也可以执行自己写的可执行程序:
myprocess.cc:
1 #include <iostream>2 3 int main()4 {5 std::cout << "hello C++" << std::endl;6 std::cout << "hello C++" << std::endl;7 std::cout << "hello C++" << std::endl;8 std::cout << "hello C++" << std::endl; 9 return 0;10 }
myprocess.cc生成的可执行文件命名为myprocess, 在 test.c中进行程序替换
test.c
1 #include<stdio.h>2 #include <unistd.h>3 int main()4 {5 printf("pid: %d, exec command begin\n",getpid());6 //execl("/usr/bin/ls","ls","-l","-a","--color=auto",NULL);7 execl("./myprocess","./myprocess",NULL); 8 printf("pid: %d, exec command end\n",getpid());9 return 0;10 }
myprocess这个程序没有选项所以这个程序执行的方法就是./myprocess, 这里的方法也可以直接写成
process
不需要添加上面的相对路径
makefile:
结果:
这就说明execl函数既可以让程序执行系统自带的指令, 也可以让程序执行我们自己写的可执行程序
但是这个运行结果执行结果好像"不太对", 在execl函数后面我们还使用了一个printf函数, 但是程序执行完execl函数之后并没有执行printf函数, 那就要了解一下进程替换的原理.
多进程版程序替换代码
进程替换的原理
关于进程替换的返回值,execl只在发生错误的时候有返回值, 如果替换失败了就会返回-1:
比如说下面的代码, 我们给execl函数传递一个不存在的路径, 就可以发现execl函数的返回值是-1 :
如果正常执行呢?
这里好像没有打印出来execl函数的返回值, 并且连printf函数都没有正常执行.
原因分析:
首先在没有执行fork函数之前操作系统中就只有一个父进程, 这个父进程有对应的数据区和代码区:
当执行fork函数之后操作系统中就会多出来一个子进程, 并且操作系统会以父进程的部分数据结构为这个子进程创建页表,虚拟地址空间和PCB, 所以子进程的页表也指向物理空间上的那块区域, 也就是说此时的子进程和父进程是共用内存上同一块数据区和代码区
当子进程调用execl函数时会将磁盘上的程序B加载进内存并替换原来的子进程所指向的数据区和代码区, 如果此时子进程的这块数据区和代码区只有它自己使用, 则B程序的数据区的代码区会直接覆盖原来的, 但是进程之间是有独立性的, 当前内存中的程序A的数据区和代码区不仅仅子进程在使用父进程也再使用, 所以此时execl函数再发生替换时会发生写时拷贝, 操作系统会在内存上再开辟一个空间用来存放程序B的数据和代码, 再将子进程的页表指向新的空间, 这样子程序就能执行程序B的代码:
这个新空间和原来父进程的数据代码就没有任何关系了, 他是一个全新的内容所以执行完execl函数之后就不会再执行原来程序中execl函数后面的内容, 所以execl函数替换成功之后的返回值是什么也就不重要了.
有关进程替换的函数
实现程序替换不止execl函数还有execlp,execv,execvp,execle,execve函数
execl
调用接口:int execl(const char *path, const char argv, ...);
头文件:unistd.h
参数:path表示新程序在磁盘的地址, argv是一个可变参数列表, 需要将我们执行这个程序时向控制台输入的东西以空格为分割一段一段字符串传参, 最后以NULL结尾.
功能:通过程序位置和程序运行名加选项进行进程替换.
execlp
调用接口:int execlp(const char *file, const char *argv, ...);
头文件:unistd.h
参数:file表示新程序的文件名,argv与上面一样(注意传空指针),此时这个函数会在环境变量的位置中寻找相应可执行程序
功能:通过程序名和程序名加选项进行进程替换
这个函数相较于execl多了一个p, 这个p表示的意思就是path也就是环境变量中的PATH.
环境变量中的PATH记录着各种操作系统指令所在的地址, 所以使用这个函数替换程序时不需要传地址, 直接传程序名就行, 因为这个函数会自动在PATH所记录的路径查找这个程序, 这个函数的第一个参数就对应着程序名, 比如:
1 #include <stdio.h>2 #include <unistd.h>5 int main()6 {7 printf("exec command begin:\n");8 execlp("ls","ls","-l","-a",NULL); 9 return 0;10 }
第一个ls表示的是程序名, 第二个ls表示的是执行程序的方法, 也就是可变参数列表中的第一个参数
execv
调用接口:int execv(const char *path, char *const argv[]);
头文件:unistd.h
参数:path表示新程序在磁盘的地址. 这里的argv就不太一样了, 是一个指针数组, 每一个指针指向一个字符串, 最后的位置也必须是NULL, const修饰的是这个字符串数组, 数组内的元素不能改变.
功能:通过程序位置、程序名加选项组成的数组进行进程替换
这个函数的v表示的是vector, 也就是说将程序的所有指令全部放入到数组中比如说下面的代码:
1 #include <stdio.h>2 #include <unistd.h>5 int main()6 {7 char *const arr[]={"ls","-a","-l",NULL};8 printf("exec command begin:\n");9 execv("/usr/bin/ls",arr);10 return 0;11 }
execvp
调用接口:int execvp(const char *file, char *const argv[]);
头文件:unistd.h
参数:file表示新程序的文件名, argv与上面的execv一样。
功能:通过程序名和程序名加选项进行进程替换和环境变量表进行进程替换
1 #include<stdio.h>2 #include<unistd.h>3 int main()4 { 5 char *const arr[]={"ls","-a","-l",NULL}; 6 printf("exec command begin:\n"); 7 execvp("ls",arr);8 return 0;9 }
几个函数之间非常相似, 可以通过字母辅助记忆:
字母p表示该函数取文件名file作为参数, 并且用环境变量寻找可执行文件, 不需要传递它在磁盘的位置
字母l表示该函数取一个list
字母v表示该函数取一个数组,可以理解为vector
字母e表示该函数取环境变量envp[]数组
execle
这个函数的参数如下
这个函数就多一个参数第三个参数表示的意思是环境变量,如果子进程要用到自定义环境变量或者系统的环境变量的话就可以用到第三个参数,比如说在当前路径下再创建一个文件:
先来看这样一段程序替换的代码:
//mytest.c 1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/types.h>4 #include <sys/wait.h>5 #include<stdio.h>6 #include<unistd.h>7 8 9 int main()10 {11 pid_t id = fork();12 if(id == 0)13 {14 //child15 printf("pid :%d,exec command begin:\n",getpid()); 16 int ret = execl("./myprocess","myprocess",NULL);17 if(ret == -1)18 printf("process replace fail\n");19 printf("pid : %d,exec command end,return value is %d.\n",getpid(),ret);20 }21 else22 {23 //father24 pid_t rid = waitpid(-1,NULL,0);25 if(rid > 0)26 printf("wait success, rid : %d\n",rid);27 }28 return 0;29 }
//myprocess.cc 1 #include <iostream>2
W> 3 int main(int argc, char* argv[],char* env[])4 {5 for(int i = 0; env[i];i++)6 {7 std::cout << i << ":" << env[i] << std::endl; 8 }9 return 0;10 }
问题1: 执行后进程替换如期打印出了环境变量表, 那我们程序替换时, 子进程(myprocess)的环境变量从哪里来的? 是从父进程来的,它是父进程调用的, 也只能由父进程传递.
那父进程的环境变量从哪里来? 是从bash来的, mytest创建进程首先要成为bash的子进程, bash传递环境变量给它, 然后它才作为父进程去创建自己的子进程.
所以bash, mytest, myprocess 用的是一套环境变量:
可以看到孙子进程确实打印出来了自己在bash导入的环境变量.
现在用putenv在父进程中导入一个环境变量:
可以看到myprocess继承了mytest的环境变量, 但bash并没有更新MYVAL, 因为mytest中的MYVAL是在bash传入环境变量之后才更改的, bash不会有影响:
问题2: 环境变量被子进程继承下去是一种默认的行为, 不受程序替换的影响, 为什么?
父进程在创建子进程时, 子进程继承父进程的部分PCB,进程地址空间和页表, 进程地址空间上的虚拟地址, 通过页表映射到物理内存, 所以其实环境变量被子进程继承下去是一件很正常的事, 所以是通过地址空间让子进程继承父进程环境变量数据.
环境变量和命令行参数也是进程的数据, exec*程序不是会替换进程的代码和数据吗? 程序替换只替换新程序的代码和数据, 环境变量不会被替换.
所以其实不用所谓的execle之类的函数传递环境变量环境变量也是默认传递下去的.
问题3:子进程执行的时候获得的环境变量.
a. 将父进程的环境变量原封不动的传递给子进程.
1.直接用
environ变量也是可以直接打印出进程所对应的环境变量的, 不用env参数也能访问环境变量.
2.直接传参
environ是char**类型的, execle的第三个参数是char* []类型, 是匹配的.
b.我们想传递自己的环境变量 , 我们可以直接构造自己的环境变量表, 给子进程传递,这个传递不是新增而是覆盖式地传递.
c.新增传递
新增之前已经实现过, 想新增哪些环境变量在父进程中putenv添加即可.
结论: 程序替换可以将命令行参数和环境变量通过自己的参数, 传递给被替换的程序的main函数中.
execvpe
这个函数的使用就和之前完全类似了,e表示环境变量, p表示直接传环境变量名即可, v表示这里需要方法组成的数组, 那这个函数的参数就如下:
execve
上面讲了那么多的函数,但是这些函数都有一个共同的特点就是这些函数都是c语言函数提供的,系统提供了一个函数调用接口execve,这个函数的参数如下:
上面介绍的六个函数最后都会被转化成execve:
最终底层调的是一个接口, 那为什么还有这么多不一样的呢? 虽然接口形式是不一样的, 但是本质还是一样的, 主要还是为了满足各种调用的场景, 不一定自己在进程程序替换时, 程序名这个字符串就替代好了, 环境变量也不一定都是以表的形式呈现好了, (比如execl传的是一个个的选项,底层execve调用时就封装成了一个数组),所以直接选择合适的就可以.