进程的程序替换
文章目录一、进程的程序替换exec系列的接口简单认识全部接口1. execl2. execlp3. execv4. execvpe二、Shell(命令行解释器)1. shell的核心工作流程2. 小知识点补充3. 显示命令提示符接口4. 读取用户输入的命令5. 解析命令行strtok的详细解析6. 执行命令7. 检查与处理内建命令一、进程的程序替换exec系列的接口在Linux中进程除了能通过fork创建子进程外还可以通过exec系列函数进行进程替换。为什么需要程序替换之前学习的fork()创建的子进程执行的还是原来的代码。想让子进程运行新的程序运行就要用程序替换exec 系列函数。进程不变PCB 不变只是代码和数据全换掉。所谓程序替换就是让一个正在运行的进程丢掉原来的程序映像转而执行另一个可执行文件。替换简单理解就是将磁盘中全新的程序(代码和数据)覆盖式的加载到当前进程代码和数据的位置然后修改页表即可。程序替换本质上并不会创建新的进程验证可以在子输出一次pid在替换之后再输出一次pid一旦程序替换成功就去执行新代码。原来的代码将不再执行已经不存在了exec系列函数失败时才会返回-1 并继续向下执行成功时没有返回值所以不需要做返回值判断返回即失败简单认识全部接口补充1v是将一个个的参数放在字符指针数组里然后将数组整个的传给函数2exec系列函数所有接口的调用关系execve是系统调用头文件为unistd.h其他exec函数是库函数头文件为stdlib.h由C语言进行封装底层会去调用系统调用execve1. execlexecl 会将当前进程的程序替换为新程序原进程的代码和数据被覆盖PID保持不变替换成功后不再返回后续代码无法执行。int execl(const char *pathname, const char *arg,...);它的第一个参数const char* pathname表明我要执行谁要有路径程序名剩下的参数表明如何执行那个程序(简单方法命令行怎么写我就怎么传。看下图execl中l可以理解为list风格的「变长参数列表」。将命令行的字符串一个一个传给参数即参数以逐个列举的形式传入。而list 风格逐个列举(...代表可变参数列表。最后一个参数必须是以NULL结尾(表明参数传递完成)execl程序替换之后的将不会再执行#include stdio.h#include unistd.hintmain(){printf(replace begin\n);//这个效果和ls-a-l效果一样 execl(/usr/bin/ls,ls,-a,-l,NULL);// 程序路径 arg0,arg1,arg2,结束标记 //这个不会被执行因为上一步是程序替换中的execl //原来的代码从 execl 往下直接丢掉不再执行 printf(replaxe end\n);return0;}程序替换的弊端就是一个程序替换的代码如果它一旦替换就会影响到当前进程会转而执行新程序旧的后面就不再执行。解决办法让父进程安安心心执行这个程序它常见其他子进程执行新的程序通过fork创建一个子进程(if(fork()0))让子进程去执行新程序父进程在这里进行进程等待之后继续执行原来的程序fork()用于创建子进程。子进程和父进程最初运行的是相同的程序exec()用于替换子进程的内存映像加载并运行新程序。子进程一般会立刻调用 exec以执行一个全新的程序子进程的代码数据都拷贝父进程的同时这里父子进程也不再共享子进程有自己新的代码。—父子进程彻底分离任何一个程序进入内存必须先变为进程。所以加载程序的本质就是动态创建进程的过程。我们自己写的进程可以被替换吗--------可以只要是能转换为进程的程序都可以进行程序替换例子原本的程序是C写的(test.c)现在想在c的程序中将C的程序(other.cc)调起来。可以在pro.c的程序中写execl(./myother,myother,NULL);。这里没有选项所以不需要写5. 程序替换是系统级行为。以下是原因1任何程序要运行必须先加载为进程。只要是进程就可以通过exec系列函数完成程序替换。2程序替换的代码不是自己随便换而是操作系统帮你把进程里的代码、数据全换掉。程序替换是操作系统提供的能力。程序替换是系统级行为程序运行必为进程凡是进程均可替换。程序替换是系统级能力C 语言进程可替换执行一切能跑成进程的程序跨编译型 / 脚本型语言都支持。程序替换是操作系统层面的机制。在一个 C 语言进程里可以通过程序替换执行任何编译型语言、脚本语言编写的程序—— 只要它最终要在系统里运行就必须被加载为进程而只要是进程系统都支持对其进行程序替换从而实现跨语言调用前端页面类语言除外。程序替换的核心 exec 本质就是系统加载器能加载并运行各类程序对编译型语言直接加载执行对脚本语言则加载对应的解释器来运行实现一个进程执行任意程序。2. execlpint execlp(const char *file, const char *arg,...);execlp中p是PATH因为execlp会自动在环境变量PATH中查找指定命令所以不用路径只需要文件名即可。l的含义同上将参数以列表的形式传递execlp(ls,ls,-a,-l,NULL);3. execv没有p(path)就说明这里需要写路径绝对/相对路径v表示vector数组第二个参数以数组呈现即字符指针数组曾经命令行参数以列表形式传现在将这些字符串“ls,“a”,“l”一个一个放在数组整体传进来。也以NULL结尾#includestdio.h#includeunistd.hintmain(){char*constargv[]{(char*const)ls,(char*const)-l,NULL};printf(开始\n);execv(/usr/bin/ls,argv);printf(结束\n);}知识点argv所有命令参数的指针数组整体。argv[0]数组首元素固定为当前程序名问题1为什么是字符指针数组而不是字符数组呢因为我们的命令行参数“ls,“a”,“l”它们是n个独立字符串。如果用字符数组char argv[] “ls -l /home”;内核根本分不清哪一段是命令名/参数 1/参数 2它只会当成一整个参数。。而字符指针数组 char *argv[]是多个独立字符串的地址列表内核一看就懂第 0 项命令名。第 1 项参数 1。第 2 项参数 2问题2char *argv[] { ls, -l, /home, NULL };中括号里的是字符串为什么类型是字符指针问题3ls是不是一个二进制程序是的。先了解什么是二进制文件代码.c给程序员看的文字。经过gcc编译后生成的文件比如 a.out、ls就是电脑CPU能直接跑的机器码这叫做编译好的二进制程序。不是文本打开是乱码不能直接编辑修改电脑加载进去就能直接跑是ELF格式Linux、EXE 格式Windows1当在命令行敲 ls就是运行这个二进制程序。它不是脚本、不是别名就是真正的程序本体。问题4我们自己程序的命令行参数谁传的怎么传任何一个进程的命令行参数 父进程通过 exec 传进来的在bash里敲命令 → bash把参数打包好 → 通过 exec 扔给你的程序。你的程序 main 函数的int main(int argc, char *argv[])1用户输入命令(终端运行bash父进程)./myprog a b c2bash(父进程)解析命令构造参数数组argv[]char *argv[]{./myprog,a,b,c, NULL};3bash 调用 fork() 创建子进程4子进程调用 exec 系列函数。exec 替换子进程内存空间变成新程序5内核把 argv 数组直接塞进新程序的 main 函数里同时这也是LInux/Unix 程序启动的唯一标准流程问题5所有程序都是 bash 的子进程父进程用 exec 传参给子进程在终端敲的所有命令全是 bash 的子进程命令行里的所有程序都是 bash 生的子进程参数全靠 exec 传递。4. execvpeexecvpe中v的意思是将命令行参数以(字符指针)数组的形式传递。p的意思是会去环境变量找路径不用自己写路径直接传文件名。e的意思是使用带e的exec系列函数会舍弃继承的环境变量而强制子进程仅使用自定义传入的环境变量。详解ee 对应环境变量environment接收char* const envp[]环境指针数组无 e 函数默认继承父进程环境全局表携带 e 的 exec 系统调用会完全替换环境上下文子进程独立使用自定义envp不再继承父进程环境。如果自己传了envp就是用自己的。没传就是用默认的语言级别的environ即从父进程的进程地址空间继承的表问题1什么是环境上下文环境上下文 该进程能看到的所有环境变量的整体比如PATHHOMEUSERLANGPWD。这些变量合在一起就叫环境上下文。问题2新增环境变量我们还可以以新增的方式将env传给子进程将传进去的环境变量加在原来的环境变量列表后面问题3创建子进程时为什么父进程不传环境变量和命令行参数子进程也能获得在 Linux 系统中每个进程都拥有独立于堆、栈的专属内存区域用来存放命令行参数与环境变量并通过全局指针extern char** environ维护环境变量表调用fork()创建子进程时会完整拷贝父进程的地址空间自动复刻环境变量与参数信息。不带e的 exec 函数会默认沿用全局environ让新进程天然继承父进程全部环境上下文而携带e的 exec 调用会舍弃继承的所有环境配置以开发者手动传入的envp[]字符指针数组作为新进程唯一的环境变量集合完成环境上下文的完全替换。查看环境变量可以用env得到环境变量那个值可以使用getenvint putenv(char* string);putenv是在该进程的环境变量表里新增一个环境变量1改自身进程环境全局继承式在当前进程直接用putenv()/setenv()新增环境变量不会覆盖是追加 / 修改父进程原有环境后续调用不带 e的 exec系列函数子进程自动继承原环境 你新增的变量。即我们直接在本进程里使用putenv()新增环境变量之后创建子进程调用exec后子进程就自动继承了父的所有环境变量包括旧的新增的部分)把xx用yy替换%s/xx/yy/g二、Shell(命令行解释器)1. shell的核心工作流程while(true){1. 显示命令提示符2. 读取用户输入的命令3. 解析命令分割成程序名和参数4. fork创建子进程5. 子进程exec执行命令6. 父进程wait等待子进程}while(1){//1. 显示提示符 printf([userhost dir]$ );//2. 读取命令 fgets(command, sizeof(command), stdin);//3. 解析命令 parse(command, argv);//4. 创建子进程 pid_tidfork();if(id0){//5. 子进程执行命令 execvp(argv[0], argv);exit(1);}else{//6. 父进程等待 waitpid(id,status,0);}}2. 小知识点补充什么是内建命令外部命令内建命令不需要跑到硬盘找独立二进制程序直接藏在 Shellbash内部、自带的小命令。输入命令时不创建子进程Shell 自己当场执行。ls外部命令是 /usr/bin/ls 独立二进制文件bash 要 forkexec 建子进程运行。cd、exit、export内建命令没有单独的程序文件就在 bash 代码里写死的直接本机执行不开子进程。外部命令必须 fork exec 替换子进程内建命令不走 fork、不走 exec直接在当前 bash 进程里运行内建命令是 Shell 程序内部内置的指令无独立可执行文件执行时不fork子进程、不调用exec系列函数由当前Shell进程直接解析运行。为什么cd不是外部命令而是内建设想一个fork一个子进程去执行切换路径的命令确实是可以切换。但是子进程执行cd那肯定是cd自己的子进程退出后父进程丝毫不会改变还在原来的目录。所以cd命令不应该由子进程执行应该由shell亲自执行3. 显示命令提示符接口获取用户名的函数可通过环境变量表知道。获取环境变量的内容char *getenv(const char *name);其他同理再将函数进行封装达到将命令行提示符打印出来的效果4. 读取用户输入的命令一旦有命令行之后bash就卡在这里不动了在等什么等待用户输入在现有的shell中用户可以让shell做几次事情无数件 shell完成一件事情就等着下一件所以等待用户输入是需要循环的 当用户输入一条命令回车shell完成后会继续等待用户输入知识点用户输入ls -a -lbash需要理解为“ls -a -l”字符串scanf是以空格为分隔符。scanf会弄成3个字符串“ls” “-a” “-l”所以完成任务不使用scanf使用fgetsfgets简介char *fgets(char *str, int n, FILE *stream);从指定的文件流键盘stdin或文件中读取一行字符最多读取n-1个字符自动在末尾添加\0构成合法C字符串。输入的命令带回车那显式打印一下也会有回车如何去掉修改被写入的数组将数组最后一个成员回车变成 0即清理\n)5. 解析命令行走到这里说明用户输入的命令已经获取成功我们获取到的是一串字符串ls -a -l在之前程序替换中参数可以以”列表“传以数组传但是不能一个字符串传所以第一步将字符串拆分这个命令不能直接在shell中进行需要创建子进程去完成命令所以这一步为后续的程序替换做铺垫了系统自带的shell会默认去获取用户的命令输入再把用户输入的信息构建成一张命令行参数表在shell内部维护所以第二步将之前拆分的命令行参数放入到全局的命令行参数表中。首先先在myshell中创建一个全局的命令行参数表并将参数个数初始化为0修改将return true修改为return g_argc 0 ? true : false;)通过这一步获取到了命令函参数表strtok的详细解析strtok 函数首次调用时第一个参数传入待分割的原始字符串函数从字符串起始位置开始扫描直至遇到指定分隔符后截取当前位置之前的子串并返回同时内部记录本次分割结束的位置后续对同一字符串的继续分割第一个参数必须传入 NULL函数会基于上一次记录的位置继续向后扫描、截取剩余部分直至字符串末尾。一直切一直切最后会切到NULL。切到NULL就会去将argv最后一个内容置为NULL符合命令行参数表的设定。赋值之后会将argv[argc]作为while的判断条件最后退出循环无数次的拆分通过循环来继续拆分直到结束6. 执行命令获取到了命令函参数表接下来执行。但不是让当前进程去执行命令当前进程有自己的任务。7. 检查与处理内建命令如果是内建命令在封装的函数中直接让shell执行即可执行完回到main函数里直接continue。因为后面调用的是外部命令的执行方法查看命令是否为内建命令看g_argv[0]是否为内建命令之一即可一个进程更改自己的工作路径调用chdir参数是绝对/相对让shell进程执行chdir