你有没有想过当你在shell上执行命令时,unixshell会做什么?shell是如何理解和解释这些命令的?屏幕后面发生了什么?例如,当我们执行ls-l*.py时,shell做了什么?了解了这些,就可以更好的使用类Unix操作系统。今天我们就来一探究竟。0.什么是shellshell通常是一个命令行界面,它将操作系统的服务暴露给人类或其他程序使用。shell启动后,通常会显示提示以等待用户输入。下图描述了基本的UNIX和Windowsshell提示符。因此shell会提示用户输入命令。现在是用户输入命令的时候了。那么shell是如何获取用户输入的命令并进行解释的呢?为了理解这一点,我们把它们分为4个步骤,分别是:获取并解析用户输入识别命令和命令的参数找到命令执行命令现在详细展开:1.获取并解析用户输入对于例如,如果你在shell中输入ls-l*.py,回车,shell会调用一个名为getline()的函数“在#include中声明,下同”来读取用户输入的命令,然后用户输入的命令字符串被用作标准输入流。一旦按下Enter,就意味着一行结束,getline()会将输入的字符串存储在缓冲区中。ssize_tgetline(char**restrictlineptr,size_t*restrictn,FILE*restrictstream);函数参数说明:lineptr:buffern:buffersizestream:stream,这里是标准输入流下面我们来看代码:char*input_buffer;size_tb_size;b_size=32;//sizeofthebufferinput_buffer=malloc(sizeof(char)*b_size);//thebuffertostoretheuserinputgetline(&input_buffer,&b_size,stdin);//getsthelineandstoresitinput_buffer一旦用户按下回车,就会调用getline()将用户输入的字符串或命令存入input_buffer。那么现在shell已经获得了用户的输入,那么下一步呢?2.识别命令和命令的参数现在shell已经知道你输入的字符串是'ls-l*.py',但是你还需要知道哪个是Command,也就是命令的参数,谁会这样做?那就是函数strtok()"#include"。strtok()将字符串标记为分隔符,在本例中为空格。所以一个空格告诉strtok()它是一个单词的结尾。因此input_buffer中的第一个标记或单词是命令(ls),其余单词或标记(-l和*.py)是命令的参数。因此,一旦shell对字符串进行标记化,它就会将它们存储在一个变量中供以后使用。char*strtok(c??har*restrictstr,constchar*restrictdelim);参数说明:str:待标记字符串delim:定界符函数strtok()接受字符串和定界符作为参数,返回指向标记字符串的指针。具体执行代码如下:char*input_buffer,*args,*delim_args,*command_argv[50];inti;i=0;delim_args="\t\r\n\v\f";//thedelimetersargs=strtok(input_buffer,delim_args);//storesthetokeninsideargswhile(args){command_argv[i]=args;//storesthetokenincommand_argvargs=strtok(NULL,delim_args);i++;}command_argv[i]=NULL;//setsthetokeninsideargv[i]=NULL;//设置command_argv的最后一个实体为NULLcommand_argv保存命令字符串,其内容如下:command_argv[0]="ls"command_argv[1]="-l"command_argv[2]="*.py"command_argv[3]=NULL那么,command_argv[0]就是命令,其他都是它的参数,最后一个是NULL,表示命令结束。现在已经反汇编了命令字符串,下一步就是找到命令。3、查找命令第二步知道用户要执行的命令是ls,那么到哪里去找这个命令呢?shell返回到环境变量PATH中找到它。PATH环境变量是存储可执行命令的位置。但是,一个PATH中可以存放多个路径:如何在这么多路径中高效的查找ls命令呢?这需要access()"#include"函数:intaccess(constchar*pathname,intmode);参数及返回值说明:pathname:文件/可执行文件的路径mode:mode,我们使用X_OK检查文件是否存在返回值:如果文件存在,返回0,否则返回-1{char*path_buff,*path_dup,*paths,*path_env_name,*path[50];inti;i=0;path_env_name="PATH";path_buff=getenv(path_env_name);/*getthevariableofPATHenvironment*/path_dup=_strdup(path_buff);/*thisfunctionisfoundbelow*/paths=strtok(path_dup,":");/*tokenizesit*/while(paths){path[i]=paths;paths=strtok(NULL,":");i++;}path[i]=NULL;/*terminatesitwithNULL*/***_strdup-duplicateastring*@from:thestringtobeduplicated**返回:pontertotheduplicatedstring*/char*_strdup(char*from){inti,len;char*dup_str;len=_strlen(from)+1;dup_str=malloc(sizeof(int)*len);i=0;while(*(from+i)!='\0'){*(dup_str+i)=*(from+i);i++;}*(dup_str+i)='\0';return(dup_str);}上面代码中的path数组存储了所有PATH位置,以NULL结尾。因此,每个PATH位置都可以与命令连接,并且可以使用access()函数执行存在性检查:{char*command_file,*command_path,*path[50];inti;i=0;command_path=malloc(sizeof(char)*50);while(path[i]!=NULL){_strcat(path[i],command_file,command_path);/*这个函数在下面找到*/stat_f=access(command_path,X_OK);/*andchecksifitexists*/if(stat_f==0)return(command_path);/*如果找到则返回连接的字符串*/i++;}returnNULL;/*否则返回NULL*/}/***_strcat-连接两个字符串和将其保存为空白字符串*@path:thepathstring*@command:thecommand*@command_path:thestringentostoretheconmand**返回:始终为void*/void_strcat(char*path,char*command,char*command_path){inti,j;i=0;j=0;while(*(path+i)!='\0'){*(command_path+i)=*(path+i);i++;}*(command_path+i)='/';i++;while(*(command+j)!='\0'){*(command_path+i)=*(command+j);i++;j++;}*(command_path+i)='\0';}一次找到命令,它将返回命令的完整路径,否则它会返回NULL,然后shell会显示命令不存在的错误。现在,如果找到该命令,那么会怎样?4.执行命令一旦找到命令,就该执行它了。问题是如何执行?要执行命令,需要使用函数execve()"#include":intexecve(constchar*pathname,char*constargv[],char*constenvp[]);参数说明:pathname:可执行文件的完整路径argv:命令的参数envp:环境变量列表execve()会执行找到的命令,返回一个整数表示执行结果。但是现在如果shell只是运行execve(),就会出现问题。execve()调用后不返回标准输出信息,这样不好,因为用户需要执行的结果。所以为了解决这个问题,shell在一个子进程中执行命令。因此,一旦子进程内的执行完成,父进程就会收到信号,程序流程将继续。因此,为了执行命令,shell使用fork()创建了一个子进程。(在#include中声明的分支)pid_tfork(void);fork()通过复制调用进程创建一个新进程。新进程称为子进程。调用进程称为父进程。fork()返回子进程在父进程中的进程ID,在子进程中返回0:{char*command,*command_argv[50],**env;pid_tchild_pid;intstatus;get_each_command_argv(command_argv,input_buffer);/*这个函数在下面找到*/child_pid=fork();if(child_pid==-1)return(0);if(child_pid==0){if(execve(command,command_argv,env)==-1)return(0);}elsewait(&status);}/***get_each_command_argv-storesallthearguments\*oftheinputcommandtothelist*@command_argv:thecommandargumentlist*@input_buffer:theinputbuffer**返回:Alwaysvoid*/voidget_each_command_argv{char**command_argv,char*e??rinput)delim_args;inti;delim_args="\t\r\n\v\f";args=strtok(input_buffer,delim_args);i=0;while(args){command_argv[i]=args;args=strtok(NULL,delim_args);i++;}command_argv[i]=NULL;}shell使用wait()(#include中声明的函数)等待子进程中的状态更改,然后程序继续运行并再次向用户显示提示。pid_twait(int*wstatus);wstatus:是一个指向整数的指针,可以用来标识子进程是如何终止的。shell在子进程内部执行命令,然后wait()等待子进程完成。因此,通过这种方式,用户可以获得命令的结果,并可以在shell显示其提示符后输入另一个命令。所以最后ls-l*.py的结果会在子进程结束时显示出来,因为我们一直在等待子进程结束,这意味着命令的结果已经给出。所以现在shell可以再次显示它的提示以等待用户再次输入。这将继续循环,除非用户键入退出。
