> 技术文档 > 【linux】linux进程控制(三)(进程程序替换,exec系列函数)

【linux】linux进程控制(三)(进程程序替换,exec系列函数)


小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
linux系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
在这里插入图片描述


目录

    • 前言
    • 一、单进程版——最简单的看程序替换
      • execl
    • 二、谈进程替换的原理
    • 三、多进程版——验证各种程序替换接口
      • execlp
      • execv
      • execvp
      • execle
        • 如何给子进程传递环境变量
          • putenv
          • execle自定义环境变量传参
      • execvpe
      • execve
    • 四、拓展
      • 进程替换shell脚本
      • 进程替换python语言
    • 总结

前言

【linux】linux进程控制(二)(进程等待wait/waitpid)——书接上文 详情请点击<——
本文由小编为大家介绍——【linux】linux进程控制(三)(进程程序替换,exec系列函数)


fork创建子进程,对于创建的子进程有两种场景,第一种子进程和父进程使用if-else分流,让子进程执行不同的代码块,实现代码的分流,第二种调用exec系列函数,让子进程执行不同的程序,本文就来介绍如何让单进程以及子进程执行不同的程序

一、单进程版——最简单的看程序替换

execl

【linux】linux进程控制(三)(进程程序替换,exec系列函数)
【linux】linux进程控制(三)(进程程序替换,exec系列函数)

  1. 上面是七种进程替换函数,要使用这些函数要包头文件#include,其中函数名中带e的函数需要声明一个全局变量environ,其中我们首先看第一个execl函数,这个execl,要进行调用首先要传入路径,即要替换的可执行文件的路径,接下来是可执行的文件名,接下来是跟选项,选项要以NULL结尾,由于一个命令的选项可能有多个,所以execl的第三个形参是可变参数,即选项可以有任意个,但是一定要以NULL结尾,execl中的 l 是list的意思,代表列举,即将选项列举在execl上
  2. 例如,我们在bash命令行中调用ls -a -l,我们写了可执行文件名ls,以及它对应的选项-a -l,其实也就是命令行如何写,我们就如何调用execl这个函数,只不过要添加一个文件路径,使用\"\"将命令以及选项引起来,最后使用NULL结尾即可
  3. 其实我们要执行一个程序,首先就是找到这个程序,其实是执行这个程序,如何执行这个程序,要不要涵盖选项,涵盖的选项有哪些,那么对于exec系列接口中,路径是如何找到程序,命令加选项加NULL就是如何执行这个程序
  4. 下面我们就在单进程中,实际调用一下execl执行程序替换,看看会发生什么现象
  5. 同时我们再使用shell脚本检测一下程序替换的过程中有没有创建新进程
//脚本代码while :; do ps ajx | head -1 && ps ajx | grep mycommand | grep -v grep; sleep 1; echo \"-----------------------------\"; done
#include #include #include int main(){ printf(\"before, pid: %d, ppid: %d\\n\", getpid(), getppid()); execl(\"/usr/bin/ls\", \"ls\", \"-a\", \"-l\", NULL); printf(\"after, pid: %d, ppid: %d\\n\", getpid(), getppid()); return 0;}

运行结果如下
【linux】linux进程控制(三)(进程程序替换,exec系列函数)

  1. 在这个运行过程中,观察右边脚本监视,显示我们的进程运行起来,创建了进程,休眠2秒后们进行execl程序替换,程序替换的过程中并没有新进程,那么我们可以得出进程替换过程中,并不会创建新进程
  2. 观察左边的打印以及执行情况,对于打印仅仅打印了execl程序替换前的before的内容,程序替换后的打印after的工作并没有做,所以我们可以得出程序替换成功后,后续的代码并不会执行
  3. 当我们输入一个不存在的指令的路径的时候,此时exec系列函数就会找不到可执行程序的路径,就会失败,即程序替换失败的时候,exec系列函数会返回-1,后续的代码才会执行。所以exec系列函数只有失败的返回值,没有成功的返回值

二、谈进程替换的原理

其实上面的程序替换是不准确的,更准确的叫法是进程替换

【linux】linux进程控制(三)(进程程序替换,exec系列函数)

  1. 当我们的进程在命令行运行起来之后,本质是bash去fork了一个子进程作为我们的进程,其中操作系统要为我们这个进程创建PCB,地址空间以及页表,并且将磁盘上进程对应的代码和数据加载到物理空间中,将物理地址和虚拟地址在页表上建立映射,此时进程会被操作系统放到CPU的运行队列中,CPU开始调度进程,所以exec系列函数的其实起到了一个加载器的效果,当我们要执行可执行程序,bash去fork子进程后,将可执行程序使用exec系列函数加载到内存,变成进程然后CPU进行调度运行
  2. 当进程调用一种exec系列函数的时候,此时该进程在物理内存上的代码和数据被新进程完全替换,并且重新建立页表上新的物理地址和虚拟地址的映射关系,此时虚拟地址仍然为原虚拟地址,地址空间仍然是原地址空间,只不过对应地址空间上的一些字段需要进行调整,PCB仍然为原PCB,所以这个过程中并不会创建新进程,完成上面的操作后,会继续从执行新程序,即从新程序的起始代码开始执行
  3. 这里有一个疑问,操作系统如何得知要从哪个位置开始执行代码,换句话来说,CPU如何得知我们程序的入口地址?
  4. 其实在linux中,我们的文件被编译成可执行程序的时候,这个可执行程序是有ELF格式的,编译时会将代码的入口地址放在可执行程序的头部(这个可执行程序的头部也就是表头),所以当可执行程序启动,进程开始被调度的时候,将表头加载到CPU中,CPU就可以得知我们程序的入口地址,代码就可以被执行了

三、多进程版——验证各种程序替换接口

execlp

【linux】linux进程控制(三)(进程程序替换,exec系列函数)

  1. 那么下面我们来学习execlp函数,对于execlp,相对于execl,它的函数名中多了一个p,其实这个p是path路径的意思,表示当我们调用execlp的时候,它会自动的去系统的环境变量PATH的路径中(PATH中的路径使用:进行分隔)去搜索当前指令的路径,注意,这个搜索的指令须是系统的指令,系统指令的路径都被放在了PATH中了,如果是我们自己的程序那么它并不会存在PATH中,所以execlp将会搜索失败,即程序替换失败,会返回-1
    【linux】linux进程控制(三)(进程程序替换,exec系列函数)
  2. 那么我们使用fork创建子进程的方式,让子进程进行程序替换,让子进程执行命令,而父进程则一直wait阻塞式等待子进程即可
#include #include #include #include #include int main(){ printf(\"i am father, pid: %d, ppid: %d\\n\", getpid(), getppid()); pid_t id = fork(); if(id == 0) { printf(\"before, pid: %d, ppid: %d\\n\", getpid(), getppid()); execlp(\"ls\", \"ls\", \"-a\", \"-l\", NULL); printf(\"after, pid: %d, ppid: %d\\n\", getpid(), getppid()); exit(0); } else { int status = 0; pid_t ret = wait(&status); if(ret > 0) { printf(\"等待子进程成功\\n\"); if(WIFEXITED(status)) { printf(\"子进程代码跑完了, pid: %d, 退出码: %d\\n\", ret, WEXITSTATUS(status)); } else { printf(\"子进程出现异常\\n\"); } } else { printf(\"等待子进程失败\\n\"); } } return 0;}

运行结果如下
【linux】linux进程控制(三)(进程程序替换,exec系列函数)

  1. 此时子进程被execlp程序替换,子进程去执行了命令,其实bash也是如此,当我们需要在命令行执行自己的可执行程序或系统的命令的时候,bash首先会创建子进程,然后进程程序替换,此时子进程就被替换成为了我们的可执行程序或系统的命令

execv

【linux】linux进程控制(三)(进程程序替换,exec系列函数)

  1. 接下来我们学习execv函数,并且我们观察一下,对于execv它的函数名并没有 l 而是v,这个v我们可以理解为vector,即将我们的命令加选项的字符串,同时最后仍然需要以NULL结尾,去传入一个字符指针数组,进行传参,由于函数名中没有p,所以需要我们手动传入指令的路径
  2. 那么我们使用fork创建子进程的方式,让子进程进行程序替换,让子进程执行命令,而父进程则一直wait阻塞式等待子进程即可
#include #include #include #include #include int main(){ printf(\"i am father, pid: %d, ppid: %d\\n\", getpid(), getppid()); char* const myargv[] = { \"ls\", \"-a\", \"-l\", NULL }; pid_t id = fork(); if(id == 0) { printf(\"before, pid: %d, ppid: %d\\n\", getpid(), getppid()); execv(\"/usr/bin/ls\", myargv); printf(\"after, pid: %d, ppid: %d\\n\", getpid(), getppid()); exit(0); } else { int status = 0; pid_t ret = wait(&status); if(ret > 0) { printf(\"等待子进程成功\\n\"); if(WIFEXITED(status)) { printf(\"子进程代码跑完了, pid: %d, 退出码: %d\\n\", ret, WEXITSTATUS(status)); } else { printf(\"子进程出现异常\\n\"); } } else { printf(\"等待子进程失败\\n\"); } } return 0;}

运行结果如下
【linux】linux进程控制(三)(进程程序替换,exec系列函数)

execvp

【linux】linux进程控制(三)(进程程序替换,exec系列函数)

  1. 对于execvp函数中,函数名中有v,代表指令加选项加NULL需要以数组形式传参,并且函数名中有p代表我们不需要传入路径,直接传入命令(可执行程序名)即可
  2. 那么我们使用fork创建子进程的方式,让子进程进行程序替换,让子进程执行命令,而父进程则一直wait阻塞式等待子进程即可
#include #include #include #include #include int main(){ printf(\"i am father, pid: %d, ppid: %d\\n\", getpid(), getppid()); char* const myargv[] = { \"ls\", \"-a\", \"-l\", NULL }; pid_t id = fork(); if(id == 0) { printf(\"before, pid: %d, ppid: %d\\n\", getpid(), getppid()); execvp(\"ls\", myargv); printf(\"after, pid: %d, ppid: %d\\n\", getpid(), getppid()); exit(0); } else { int status = 0; pid_t ret = wait(&status); if(ret > 0) { printf(\"等待子进程成功\\n\"); if(WIFEXITED(status)) { printf(\"子进程代码跑完了, pid: %d, 退出码: %d\\n\", ret, WEXITSTATUS(status)); } else { printf(\"子进程出现异常\\n\"); } } else { printf(\"等待子进程失败\\n\"); } } return 0;}

运行结果如下
【linux】linux进程控制(三)(进程程序替换,exec系列函数)

execle

【linux】linux进程控制(三)(进程程序替换,exec系列函数)

  1. execle函数的函数名中有 l 代表指令加选项加NULL需要以参数的形式传入,并且execle函数名中没有p,那么表示需要我们传入指令的路径,观察execle函数名中还有e,这个e其实是环境变量的意思,代表我们自定义环境变量替换原来的环境变量
  2. 那么我们思考一下,既然exec系列函数可以执行系统命令,那么能不能执行我们自己的命令呢?
  3. 其实答案是可以的,下面小编创建一个c++编写的程序,让exec系列函数,先以execl为例执行我们自己的命令,之后再以execle为例执行我们自己的命令
  4. 对于c++程序,在linux中c++文件的后缀可以是cxx,可以是cc,同样也可以是cpp,但是由于cpp更为熟悉,所以小编这里使用cpp为后缀进行讲解,感兴趣的读者友友可以自行尝试一下使用cxx或者cc为后缀的文件名编写c++程序,这里小编编写的c++程序可以打印命令行参数和环境变量
#include using namespace std;int main(int argc, char* argv[], char* env[]){ cout << \"这是命令行参数\" << endl; for(int i = 0; argv[i]; i++) { cout << \"[\" << i << \"]\" << argv[i] << \" \"; } cout << endl << endl; cout << \"这是环境变量\" << endl; for(int i = 0; env[i]; i++) { printf(\"[%d]: %s\\n\", i, env[i]); } return 0;}
  1. 在我们自己的c语言程序中,那么我们使用fork创建子进程的方式,让子进程进行程序替换,让子进程执行我们自己的编译好的c++程序,而父进程则一直wait阻塞式等待子进程即可
#include #include #include #include #include int main(){ printf(\"i am father, pid: %d, ppid: %d\\n\", getpid(), getppid()); pid_t id = fork(); if(id == 0) { printf(\"before, pid: %d, ppid: %d\\n\", getpid(), getppid()); execl(\"./otherExe\", \"otherExe\" ,\"-a\", \"-b\", \"-c\", NULL); printf(\"after, pid: %d, ppid: %d\\n\", getpid(), getppid()); exit(0); } else { int status = 0; pid_t ret = wait(&status); if(ret > 0) { printf(\"等待子进程成功\\n\"); if(WIFEXITED(status)) { printf(\"子进程代码跑完了, pid: %d, 退出码: %d\\n\", ret, WEXITSTATUS(status)); } else { printf(\"子进程出现异常\\n\"); } } else { printf(\"等待子进程失败\\n\"); } } return 0;}
  1. 下面就是自动化构建工具的编写,在之前小编都是构建一个可执行文件,思考一下我们该如何同时构建两个可执行文件?
  2. 其实采用依赖关系的推导即可
add:otherExe mycommand otherExe:otherExe.cppg++ $^ -o $@ -std=c++11mycommand:mycommand.cgcc $^ -o $@ -std=c99.PHONY:cleanclean:rm -f mycommand otherExe

运行结果如下
【linux】linux进程控制(三)(进程程序替换,exec系列函数)
【linux】linux进程控制(三)(进程程序替换,exec系列函数)

  1. 如上,我们就可以在我们的c语言程序中,fork创建子进程,对于这个子进程可以替换为c++程序执行
  2. 但是对于这个c++程序,其实我们是使用的execl进行替换调用的,即我们并没有显示传入环境变量,但是为什么这个c++程序却可以打印出环境变量呢?
  3. 其实是由于环境变量具有全局属性,环境变量也是数据,在地址空间中环境变量的位置在栈的上方,由于父进程fork创建子进程的时候,由于子进程并没有自己的代码和数据以及内核数据结构,所以子进程会将父进程的内核数据结构大部分内容全部cv一份,所以对应的地址空间上的环境变量对应的虚拟地址,以及页表中环境变量对应的虚拟地址和物理地址的映射关系全部被子进程cv了一份,所以环境变量即使我们不传入,子进程中也会继承父进程的环境变量,即进程替换的时候,环境变量并不会被替换
如何给子进程传递环境变量
  1. 有两种方式,第一种是新增环境变量,第二种是完全替换
putenv

【linux】linux进程控制(三)(进程程序替换,exec系列函数)

  1. 第一种方式,使用库函数putenv的方式(putenv可以新增环境变量到父进程中,这样fork子进程后,子进程自然而然继承父进程后也继承到了新增环境变量,同样可以在if-else代码分流的子进程中在子进程的代码区域putenv新增环境变量,putenv父进程和子进程上的区别就是是否修改父进程环境变量),给子进程新增环境变量
#include #include #include #include #include int main(){ printf(\"i am father, pid: %d, ppid: %d\\n\", getpid(), getppid()); pid_t id = fork(); if(id == 0) { putenv(\"MYVALUE=11111111111111111111111111111111111111111111111111111111111\"); printf(\"before, pid: %d, ppid: %d\\n\", getpid(), getppid()); execl(\"./otherExe\", \"otherExe\" ,\"-a\", \"-b\", \"-c\", NULL); printf(\"after, pid: %d, ppid: %d\\n\", getpid(), getppid()); exit(0); } else { int status = 0; pid_t ret = wait(&status); if(ret > 0) { printf(\"等待子进程成功\\n\"); if(WIFEXITED(status)) { printf(\"子进程代码跑完了, pid: %d, 退出码: %d\\n\", ret, WEXITSTATUS(status)); } else { printf(\"子进程出现异常\\n\"); } } else { printf(\"等待子进程失败\\n\"); } } return 0;}

运行结果如下
【linux】linux进程控制(三)(进程程序替换,exec系列函数)

  1. 此时子进程的环境变量就在原来继承的父进程的环境变量的基础上新增了MYVALUE
execle自定义环境变量传参
  1. 第二种方式,创建自定义的字符串指针数组作为自定义环境变量,自定义环境变量通过程序替换中的execle或者execvpe接口进行自定义环境变量的传参
#include #include #include #include #include int main(){ printf(\"i am father, pid: %d, ppid: %d\\n\", getpid(), getppid()); char* const myenv[] = { \"MYVALUE1=555555555555555555555555555555\", \"MYVALUE2=666666666666666666666666666666\", \"MYVALUE3=777777777777777777777777777777\", NULL }; pid_t id = fork(); if(id == 0) { printf(\"before, pid: %d, ppid: %d\\n\", getpid(), getppid()); execle(\"./otherExe\", \"otherExe\" ,\"-a\", \"-b\", \"-c\", NULL, myenv); printf(\"after, pid: %d, ppid: %d\\n\", getpid(), getppid()); exit(0); } else { int status = 0; pid_t ret = wait(&status); if(ret > 0) { printf(\"等待子进程成功\\n\"); if(WIFEXITED(status)) { printf(\"子进程代码跑完了, pid: %d, 退出码: %d\\n\", ret, WEXITSTATUS(status)); } else { printf(\"子进程出现异常\\n\"); } } else { printf(\"等待子进程失败\\n\"); } } return 0;}

运行结果如下
【linux】linux进程控制(三)(进程程序替换,exec系列函数)
此时子进程的环境变量就被我们的自定义环境变量完全替换了

execvpe

【linux】linux进程控制(三)(进程程序替换,exec系列函数)

  1. 对于execvpe函数,其函数名中有v,代表我们需要将指令加选项加NULL以字符串指针数组的形式传参
  2. 其函数名有p,代表我们不需要写入路径,只需要写入文件名即可,但是这里不同,由于小编要执行的是我们自己的程序,而我们自己的程序的路径并不在系统的路径中,如果让execvpe函数去PATH环境变量中的系统路径中查找会找不到可执行文件的路径,会返回-1表示程序替换失败,所以这里仍然需要我们手动传入可执行程序的路径
  3. 其函数名中有e,代表我们需要传入环境变量,其实这个环境变量也可以是父进程原封不动的环境变量,即使用第三方环境变量extern char** environ即可,下面小编就来演示一下
#include #include #include #include #include extern char** environ;int main(){ printf(\"i am father, pid: %d, ppid: %d\\n\", getpid(), getppid()); char* const myargv[] = { \"otherExe\", \"-a\", \"-b\", \"-c\", NULL }; pid_t id = fork(); if(id == 0) { printf(\"before, pid: %d, ppid: %d\\n\", getpid(), getppid()); execvpe(\"./otherExe\", myargv, environ); printf(\"after, pid: %d, ppid: %d\\n\", getpid(), getppid()); exit(0); } else { int status = 0; pid_t ret = wait(&status); if(ret > 0) { printf(\"等待子进程成功\\n\"); if(WIFEXITED(status)) { printf(\"子进程代码跑完了, pid: %d, 退出码: %d\\n\", ret, WEXITSTATUS(status)); } else { printf(\"子进程出现异常\\n\"); } } else { printf(\"等待子进程失败\\n\"); } } return 0;}

运行结果如下
【linux】linux进程控制(三)(进程程序替换,exec系列函数)
【linux】linux进程控制(三)(进程程序替换,exec系列函数)

execve

【linux】linux进程控制(三)(进程程序替换,exec系列函数)
【linux】linux进程控制(三)(进程程序替换,exec系列函数)

  1. 其实还有一个进程替换函数,这个execve是在2号手册中,即它是一个系统调用
  2. execl,execlp,execv,execvp,execle,execvpe上述六个进程替换函数是库函数,实际上上面六个进程替换函数的底层都是调用的execve,都是对execve的封装,所以对于上述六个进程替换函数的区别只是传参的不同,最终这六个库函数它们在实现上最终都要转化为系统调用execve实现

四、拓展

进程替换shell脚本

  1. shell脚本是一门解释性语言,在linux中由bash进行解释,shell脚本的文件后缀需要以.sh结尾,并且文件内容的开头需要加上#!/usr/bin/bash
  2. 其实我们仔细看一下这个#!是固定格式,其中/usr/bin/bash好像是一个路径,也就是可执行程序bash解释器的路径
  3. shell脚本的大部分内容都是我们在bash命令行中的指令,额外再加上一些语法特性,shell脚本的程序,本质就是纯文本文件,只不过是由bash进行逐行解释,在运行的时候我们使用bash 文件名即可运行
#!/usr/bin/bash echo \"hello linux\"ls -a -l 

运行结果如下
【linux】linux进程控制(三)(进程程序替换,exec系列函数)

  1. 那么接下来我们使用exec系列函数,以execl为例将fork的子进程程序替换后执行,这里的路径就应该是可执行程序bash的路径,即/usr/bin/bash,选项中带上bash加shell脚本文件加NULL即可
#include #include #include #include #include extern char** environ;int main(){ printf(\"i am father, pid: %d, ppid: %d\\n\", getpid(), getppid()); pid_t id = fork(); if(id == 0) { printf(\"before, pid: %d, ppid: %d\\n\", getpid(), getppid()); execl(\"/usr/bin/bash\", \"bash\", \"test.sh\", NULL); printf(\"after, pid: %d, ppid: %d\\n\", getpid(), getppid()); exit(0); } else { int status = 0; pid_t ret = wait(&status); if(ret > 0) { printf(\"等待子进程成功\\n\"); if(WIFEXITED(status)) { printf(\"子进程代码跑完了, pid: %d, 退出码: %d\\n\", ret, WEXITSTATUS(status)); } else { printf(\"子进程出现异常\\n\"); } } else { printf(\"等待子进程失败\\n\"); } } return 0;}

运行结果如下
【linux】linux进程控制(三)(进程程序替换,exec系列函数)

进程替换python语言

  1. python也是一门解释性语言,解释器是python系列,这里我们使用python3,python程序的文件后缀是.py
  2. python程序的内容开头的格式同样是要使用#!后面跟上解释器的路径,即/usr/bin/python3
  3. python程序运行的时候,我们使用python系列运行即可,这里我们使用python3
#!/usr/bin/python3print(\"hello linux\\n\")

运行结果如下
【linux】linux进程控制(三)(进程程序替换,exec系列函数)

  1. 那么接下来我们使用exec系列函数,以execl为例将fork的子进程程序替换后执行,这里的路径就应该是可执行程序python3的路径,即/usr/bin/python3,选项中带上python3加python程序文件加NULL即可
#include #include #include #include #include extern char** environ;int main(){ printf(\"i am father, pid: %d, ppid: %d\\n\", getpid(), getppid()); pid_t id = fork(); if(id == 0) { printf(\"before, pid: %d, ppid: %d\\n\", getpid(), getppid()); execl(\"/usr/bin/python3\", \"python3\", \"test.py\", NULL); printf(\"after, pid: %d, ppid: %d\\n\", getpid(), getppid()); exit(0); } else { int status = 0; pid_t ret = wait(&status); if(ret > 0) { printf(\"等待子进程成功\\n\"); if(WIFEXITED(status)) { printf(\"子进程代码跑完了, pid: %d, 退出码: %d\\n\", ret, WEXITSTATUS(status)); } else { printf(\"子进程出现异常\\n\"); } } else { printf(\"等待子进程失败\\n\"); } } return 0;}

运行结果如下
【linux】linux进程控制(三)(进程程序替换,exec系列函数)

  1. 其实不论是c++,shell脚本以及python还是其它的所有语言,这些语言编写的文件,经过编译后形成的可执行程序要执行都要以进程的形式执行,那么可执行程序就要加载到内存中才能变成进程被CPU调度执行,如何加载,那么就是bash通过fork子进程,利用exec系列(exec系列函数就是加载器的原理),即利用加载器将可执行程序通过程序替换的原理加载到内存
  2. 所以我们可以得出,有了exec系列函数,我们即使是使用的c语言,那么也可以调用任何语言的可执行程序,并且任何语言中都会有类似于exec系列的函数,这样语言之间都可以进行调用

总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!