先前fork说创建子进程执行代码,如何让子进程执行和父进程完全不一样的代码?程序替换。
一 单进程替换演示
1 execl函数使用
最近转到在vs code下写代码,之前也在xhell下用过execl函数,所以才想写篇博客总结总结,没想到在vs code下写了一句简单的execl却总是替换失败。想来想去也不知为何,感觉翻车了。
后来才知道execl的路径没给对,usr前面要带/,因为usr是根目录下的,路径少了个根目录系统也不知道去哪找usr,因为可能有很多目录都叫usr,可是当我改了后。
嗯?还是失败了,我记得早上就是加了/没反应,我才奇怪的,临近中午我才意识到vs code的一个坑人的点,你修改了代码,不保存,编译的还是旧代码,只有ctrl + s保存一下,再编译才是新程序。最最重要的是这个ctrl + s不能在输入终端的时候保存,必须得在代码处保存。
哪里是终端呢,下面这就是终端,ctrl+~可进入。
二 替换原理
替换原理
单进程下是直接把原程序的所有代码替换,那如何从头开始执行,cpu上的寄存器是有保存下一句指令的地址的,现在代码要更换了,你说这个地址还有用吗,那如何拿到新代码的地址呢,先来聊聊替换时做了什么,如上图,execl函数的其中一个参数是/usr/bin/ls,此时系统会把这个ls可执行文件的代码加载到内存。
由编译原理的内容可得,代码在编译的时候就已经分段了,因为要方便操作系统拿去分段映射,也已经给每条数据代码生成了地址,这个地址是逻辑地址,和虚拟地址几乎没区别,应该可以直接填入页表左侧,而且还会生成一个表头,让系统知道各段的起始位置在哪,而这样cpu拿到表头就可以从代码段开始执行了,此时页表,将新程序的物理地址填入,替换成功后,先前的代码和数据占的内存按理说是要被释放的,会不会没成功,然后也被释放了,os应该不会不会干出还没替换完就断自己后路的事。
三 多进程替换
当我们了解了单进程的替换工作后,我们就知道此时父子进程一定会发生写时拷贝,因为父子进程代码和数据是独立的,我们还发现pid一直不变,这说明进程替换没有创建新进程。
补充 1
为什么子进程进程内的下一句代码为啥不执行,因为已经被替换了,替换成功就不执行,失败了呢?继续往后执行。
补充 2 exec函数总结
第一个参数是为了找到程序,要么写全路径,要么带P,去PATH变量里存的路径去找,这个PATH变量也是怪怪的,有时候我把自己的可执行程序的路径写入了,然后我第一个参数就不传具体路径了,结果还是说没找到。
第二个参数是和选项有关,我之前好奇明明我第一个参数/usr/bin/ls不是已经传了ls吗(那为什么第二个参数还要传ls呢,不知道啊,大佬就是这么设计的,我们直接用就好了)如果是带l的,给main函数(这个main函数就是替换程序ls的main函数)传的参数就像命令行参数一个一个传,以NULL结尾,带v的,则是把这些命令行参数归成数组传参。所以说exec函数带l和带v是冲突的。带e的则与环境变量有关
exec函数还可以用自己写的程序,甚至是其它语言的代码来替换。
先前说了替换本质是修改页表和加载代码文件到内存,这个工作肯定是调用了系统调用完成的,而且任意语言写的代码本质都是01数据,那就可以用统一的方式加载到内存,修改页表不就是填地址吗,这和语言一点关系都没有,所以在同一平台下,execl函数可以调用任意语言写的可执行程序,使其变成进程的代码。所以在execl函数看来,都是一样的文件,都是先加载到内存然后修改页表即可。
补充 3 exec函数导环境变量
既然exec系列函数是可以调用任意的可执行文件,那如果我要传环境变量如何传呢? 也就是说exec系列函数如何传环境变量给另一个可执行文件。由于程序替换,不改变环境变量这些数据,所以执行另一个可执行文件还是能拿到原先的环境变量。如下代码,打印显示即可。
注:test.cpp就是被编译成了test2这个可执行文件。
exec系列函数中只要是带e的函数,就是要导入新的环境变量,经过测试,这个新的环境变量表会覆盖当前进程原有的环境变量。
四 xshell实现
在xhell下也用了不少的命令,而随着进程的学习,我们也就可以初步实现xhell了。每次在linux下下的vim写代码,都要包含好多头文件,下面代码为了简洁,就不显示头文件了,大家到时候一个个man,熟悉熟悉man的使用以及提高看手册的能力。
先从主函数入手。第一步是一些变量的初始化,在3 解析指令,分割字符串再解释变量保存的什么的,我们直接看2。
int main()
{while(1){//1 初始化rdir = 0;filename = NULL;2: 人机互动,接收指令 int argc = interact();if(!argc)//argc为零,无指令{continue;}//3: 分割字符串splitstring();//4 内建命令int ret = BuildCommand(argc); //5 执行普通指令if(!ret)NormalExecute();}return 0;
}
2 人机互动 接收指令
当我们在xshell下输入指令,总会显示这一行,然后就阻塞着等待输入指令。
所以我们的第一步是先打印bash命令行[USR+主机名+路径]$
这个路径的获取有点绕,我是用getcwd获取放入数组,再打印的,因为如果是直接获取环境变量,这个路径是一直不变的,至于为什么不变呢?如下测试。
printf("%s\n",getenv("PWD")); chdir("/home");printf("%s\n",getenv("PWD"));execl("/usr/bin/pwd","pwd",NULL);
如下,环境变量PWD并不会改变,所以我猜测chdir改变的目录是当前进程的工作目录-cwd,在进程运行时用ls /proc/+pid才可以显示。
还有就是PWD要显示当前目录,ls显示目录下的文件信息,这两个命令用的cwd变量存的目录,根本就不用环境变量PWD,所以我猜测cd命令改变的也是工作目录,只是顺便更新了pwd环境变量。代码里我就没改这个环境变量了,就直接用个数组来保存当前路径了。
#define LEFT "["
#define RIGHT "]"
#define LABLE "# "
#define LINE_SIZE 1024
char pwd[LINE_SIZE];
const char*getusr()
{return getenv("USER");
}
const char*gethost()
{return getenv("HOSTNAME");
}
void getpwd()
{getcwd(pwd,LINE_SIZE);getcwd是获取当前工作目录,并拷贝到pwd数组中,数组长度为1024
}
int interact()
{getpwd();封装成三个函数分别获取USR,Host和Pwdprintf(LEFT"%s@%s %s"RIGHT LABLE,getusr(),gethost(),pwd);接收指令,保存到command数组中,用于后面解析指令fgets(command,LINE_SIZE,stdin);//ls -a\n去除\n字符command[strlen(command) - 1] = '\0';检查重定向符号 check_command(command); 这个函数我放在3 解析指令一同说明return strlen(command)-1;
}
3 解析指令,分割字符串
因为我们输入的ls -a -l被当成“ls -a -l”整个字符串了,所以我们需要用空格分离成一个一个字符,后面要用于execl的传参。
#define DELIM " \t" 分隔符的宏定义,包括空格和Tab键
#define OUT_RDIR 1
#define IN_RDIR 2
#define APPEND_RDIR 3
#define LINE_SIZE 1024
#define OPTION_SIZE 32char* filename;
int rdir = 0;
char command[LINE_SIZE];
char* argv[OPTION_SIZE];void check_command(char * pos)
{while(*pos) 遍历指令数组{if(*pos == '<')//输入重定向{rdir = IN_RDIR;*pos++ = '\0';while(isspace(*pos)) pos++; 跳过空格 cat < test.txtfilename = pos;break;}else if((*pos)== '>') 输出重定向 cat >test.txt{*pos++ = '\0'; if((*pos)== '>')追加重定向 cat >>test.txt{ rdir = APPEND_RDIR;*pos++ = '\0';while(isspace(*pos)) pos++;//跳过filename = pos;break;}rdir = OUT_RDIR;while(isspace(*pos)) pos++;//跳过filename = pos; break;}else {;}pos++;}}void splitstring()
{//"ls -a -l"int i = 0;strtok函数的使用有些奇怪,第一次要传字符数组,之后的解析就传NULL空指针第二个参数是DELIM,这个参数是分隔符集合,strtok只要碰到" \t"内的字符
一次切割完成,然后赋值给argv数组。argv[i++] = strtok(command,DELIM); while(argv[i++] = strtok(NULL,DELIM));}
如上rdir和filename记录了重定向的类型以及,重定向的文件名,所以每次循环是一次指令输入到处理,下一次输入新指令,这两个变量的信息就要清空。
4 执行内建命令
此时我们终于可以解开内建命令的面纱了,还是那句话,真的就只是一个函数而已。
#define LINE_SIZE 1024
#define OPTION_SIZE 32int lastcode = 0; 保存的是退出码char command[LINE_SIZE];
char* argv[OPTION_SIZE];
char penv[OPTION_SIZE];int BuildCommand(int argc)
{if(strcmp(argv[0],"ls")==0 ) 在ls -a -l命令中加上颜色选项,我们平时输入的ls -a都是bash默认加上去的{argv[argc++] = "--color";argv[argc] = NULL;}if(strcmp(argv[0],"cd") == 0) chdir改进程的工作目录,当然不能创建子进程执行了。{chdir(argv[1]);改工作目录,并保存到pwd数组中getpwd();return 1;}else if(strcmp(argv[0],"echo")== 0) {if(strcmp(argv[1],"$?")==0) 输出退出码{printf("%d\n", lastcode);打印完后,退出码归零,所以多次echo,第二次为0lastcode = 0; return 1;}"echo $PATH"else if(argv[1][0] == '$') 输出环境变量{printf("%s",getenv(argv[1]+1));获取环境变量并打印return 1;}}export MYVALUE=10000else if(strcmp(argv[0],"export")== 0) 设置环境变量{memcpy(penv,argv[1],strlen(argv[1]));//putenv(argv[1]);不能直接put argv存的是指向command字符数组的指针下一次输入命令就被覆盖了,那环境变量就不存在了。putenv(penv); //这种保存在一个数组内的方式,只能存一个环境变量,懒得再实现了实现主要是为了理解内建命令,不太想完完整整地造一遍 }return 0;
}
5 执行普通命令
void NormalExecute()
{//创建子进程int id = fork();if(id < 0){perror("fork:");}else if(id == 0)//子进程{if(rdir == OUT_RDIR) 用dup2函数做重定向{int fd = open(filename,O_WRONLY|O_CREAT|O_TRUNC,0660);dup2(fd,1);}else if(rdir == IN_RDIR){int fd = open(filename,O_RDONLY,0660);dup2(fd,0);}else if(rdir == APPEND_RDIR){int fd = open(filename,O_WRONLY|O_CREAT|O_APPEND,0660);dup2(fd,1);}//程序替换execvp(argv[0],argv); 这一步才是真正的执行指令,argv[0]是指令名称,选项都在argv数组中,直接传入即可,这就是exec函数中带p的传的是选项数组exit(2);}else//父进程{int status = 0;int ret = waitpid(id,&status,0);//阻塞等待lastcode = WIFEXITED(status);}
}