目录
1.2、char*fgets(char *str, int size, FILE *stream)
2.2.2、char*strtok(char* str,const char* delim)
2.5、在myshell中指令输错了,按BACK键删除不了怎么办?
1、fgets函数和gets函数
虽然用 gets() 时有空格也可以直接输入,但是 gets() 有一个非常大的缺陷,即它不检查预留存储区是否能够容纳实际输入的数据,换句话说,如果输入的字符数目大于数组的长度,gets 无法检测到这个问题,就会发生内存越界,所以编程时建议使用 fgets()。
1.1、char* gets(char*str)
需要头文件<stdio.h>,从输出缓冲区中读取一个字符串,存储到str指向的空间中,调用成功则返回一个指针,指向字符串中第一个字符的地址,调用失败返回NULL。
1.2、char*fgets(char *str, int size, FILE *stream)
1,在头文件<stdio.h>中,函数每次只从stream流中读取一行字符串,存储到指针str指向的空间中。stream流可以是标准输入stdin,stdin也是一个文件,或者其他文件,所以简单理解:stream流就是某一个文件。
2,函数结束的条件:当读取到换行符\n或者文件结束符(EOF)时,又或者读取的字符数量已经有n-1个时。
3,当size为n时,fgets读取的一行字符串中只能存在0到n-1个字符,因为在str指向的字符串结尾处会自动添加一个字符串结束符\0。即函数读取到\n之后,在末尾处添加\0构成字符串,换句话说就是每当fgets函数读完一行,会自动在字符串的末尾添加一个\0。
4,\n会作为一个合法的字符被fgets函数读取,所以如果fgets()函数读到一个换行符\n,会把它储存在str指向的字符串中,这点与gets不同,gets会丢弃换行符。
5,调用成功则返回一个指针,指向字符串中第一个字符的地址,调用失败返回NULL。
1.2.1、那么如何用fgets函数读取整个文件呢?
如上图,通过循环,如果fgets把文件的第一行读完了,那么下次调用fgets时,文件指针就会指向第二行的开头,下次调用fgets就会从第二行开始读,可以理解成fgets函数内部有改变文件指针的指向的逻辑。fgets每次读取到一行后,即遇到\n后会自动在字符串的末尾加入字符串结束标志\0,这样就读取到了一个符合c语言规定的字符串,然后将字符串包括\0放进缓存,即数组line中,然后就可以将数组中的字符串打印。下一次循环的时候,由于已经调用了一次fgets,文件指针现在指向文件的第二行,所以继续fgets读取到第二行,将第二行的字符串包括\0放入数组line中,第二行的字符串会将原数组line中表示第一行字符串的内容覆盖一部分,这个一部分有多少取决于第二行字符串的长度,由于第二行字符串中也有\0作为字符串结束标识符(fgets函数自动加的\0),所以原数组中的字符串对第二行的字符串不会产生任何影响,然后再次打印出第二行的字符串,经过同样的操作即可打印整个文件的内容。
2、编写myshell程序
2.1、打印用户提示输入符,并存储用户输入的指令的文本。
代码如下:
运行结果如下:
2.1.1、存在的问题
代码的那张图中最后一行的printf函数只有一个\n,为什么运行结果换了两行呢?
2.1.2、问题原因
这是因为fgets函数等待用户输入时,比如输入ls -al,此时需要按回车键结束输入,这个回车键\n也是会被记录的,所以输入缓冲区中实际上存储了ls -al\n,所以最后数组cmd_line中存储的是ls -al\n,所以最后打印cmd_line的时候先换一行,然后用户在printf末尾加了\n,再换一行,所以换了两行。
2.1.3、问题的解决方案
如何让运行结果只换一行呢?因为字符串以\0结尾,编译器默认会加入\0,此时输入ls -al回车,数组存储的是ls -al\n\0,使用strlen(数组名)计算数组长度时,\0是不算字符的,但\n算,所以用计算的长度减1,就得到了数组中最后一个字符的下标,即\n的下标,将\n手动修改成\0即可,如下图。
代码如下:
运行结果如下:
2.1.4、换行符和printf的注意事项
换行符注意事项:
1. \n只算一个字符,strlen(“\n”)结果为1,sizeof(“\n”)的结果为2是因为编译器在\n后自动补了\0,所以也能得知\0也只算一个字符。
printf函数注意事项:
1.读取到字符\n就会换行,比如char c【】=“123\n\n”,printf(“%s”,c)打印完123后发现有两个\n,所以换两行,最后读取到编译器自动补的\0,打印结束。
2.当printf函数遇到\0,就不会继续打印了,比如char c【】=“123\0\n”,因为printf(“%s”,c)打印完123后遇到\0,打印就直接结束了,所以\n就没有生效,也就没有换行。
3.printf(“%s\n”)是个特例,由于字符串%s最后都有\0,所以%s后的\n理应失效,但由于这个\n不是字符串%s中的\n,而是字符串%s外的\n,所以\n依然生效。
4.printf(“%s\0\n”),由于\0和\n都是字符串%s外面的字符,又因为\n前有\0,所以\n失效。
2.2、分割数组中存储的代表用户指令的字符串
2.2.1、若不使用库的接口,想自己实现分割的思路
由于输入指令时比如ls -a -l,指令和指令选项之间是有空格的,所以可以将空格替换成\0,此时指令变成了ls\0-a\0-l\0。然后用一个字符指针数组char* array[ ] 存储若干个指针,比如数组中第一个指针指向ls\0中的 l 字符的地址,第二个指针指向-a\0中的 - 字符的地址,像这样就可以将一个字符串分割成几个字串。
2.2.2、char*strtok(char* str,const char* delim)
1.在头文件<string.h>中。
2.参数str表示要被分解的字符串,delim表示作为分隔符的字符(可以是一个,也可以是集合)。
3.该函数返回被分解的第一个子字符串,若无可检索的字符串,则返回空指针。
注意事项:
1.strtok函数会修改原字符串,比如char c【】=“aaa-bbb-ccc”,char*b=strtok(c,“-”)后,数组c会变成“aaa\0bbb\0ccc\0”。
2.现在有char c【】=“aaa-bbb-ccc”,第一次使用strtok后,比如char*b=strtok(c,“-”),此时b指向字符串aaa,如果还想继续分割字符数组c,则要传NULL,比如char*d=strtok(NULL,“-”)。如果不传NULL,则会发生段错误segment fault,即越界。
2.2.3、代码和运行结果
2.3、内置指令(或者说内建指令)和外部指令
2.3.1、内置指令(或者说内建指令)和外部指令是什么?
shell内置命令,就是由 Bash 自身提供的命令,而不是文件系统中的某个可执行文件,什么意思呢?就是说这些内置命令是shell解释器的一部分,它们直接嵌入到Shell的代码中,这些命令本质上和shell是同一个文件,而不是作为独立的外部命令文件存在,因此使用这些命令时不需要额外从磁盘上加载,从这里也可以看出一个道理:内置命令的执行速度通常比外部命令(存储在磁盘上的可执行文件)更快,因为使用内置命令时无需shell【创建新的子进程后进行进程替换】,而是直接由shell解释器处理。
内置命令包括一些常见的shell操作,如cd(更改工作目录)、echo(打印文本)、pwd(显示当前工作目录)等。这些命令不需要磁盘上的外部可执行文件来执行,而是直接由shell解释器处理。
外部命令则是存储在磁盘上的可执行文件,需要通过shell解释器创建新的子进程来执行。当你在shell中使用外部命令时,shell会根据环境变量的值搜索系统的路径来找到并执行相关的可执行文件。外部命令包括系统工具和自定义程序,例如ls,grep,gcc等。
根据上几段的理论可得,因为cd不是磁盘上的某个可执行文件(外部指令),而只是一个shell内置命令,用于在shell中调用某个函数,所以在shell中使用cd命令时,shell进程不会创建出子进程,而是由shell进程本身执行。又因为进程只能修改自己的工作路径,不能修改其他进程的工作路径,所以cd命令才可以更改当前shell进程的工作目录。而如果调用的不是内置指令,而是调用第三方提供的磁盘上的某个可执行文件(外部指令),则本质是通过shell(父进程)创建子进程,并让子进程替换成这个可执行文件的进程,最后让替换后的子进程执行的。
2.3.2、cd失效的场景
如下图中,在我们自己编写的shell程序里输入cd .. 想以此返回上级路径,但发现再次pwd时,显示出来的路径没有变化。
代码如下:
运行结果如下:
2.3.3、cd失效的原因
因为进程只能修改自己的工作路径,上面失效的情景是:咱们自己编写的myshell用了fork创建子进程,然后是在子进程的代码块中执行cd命令,也就是子进程在cd,而进程是只能修改自己的工作路径的,所以当前情景下只能修改子进程的工作路径,而对于父进程,即我们编写的myshell进程的工作路径是不受fork出的子进程的影响的。所以cd实际上没有失效,只不过是子进程在cd,修改的是子进程的工作路径,父进程的工作路径不受影响。所以上图的运行结果中,在myshell进程,即父进程中pwd,即使cd .. 了,但显示的一直是/home/kiana。
有人可能会说:在系统shell中,shell是父进程,cd是shell创建出的进行过进程替换的子进程,那为什么系统shell这个父进程的工作路径能被子进程cd修改呢?答案:在上文的内置指令部分中说过了,cd不是shell创建出的子进程,cd是shell的内置命令,在shell中使用cd命令时,shell进程不会创建出子进程,而是由Shell进程本身执行,因此cd命令才可以更改当前Shell进程的工作目录。
2.3.4、cd失效的解决方案(chdir函数)
1.在创建子进程前进行判断,如果存储指令的字符指针数组的首元素,即第一个指针指向的字符串是cd,并且第二个指针指向的代表路径的字符串不为NULL,则让父进程,即myshell进程调用chdir函数修改父进程的当前工作路径,使当前工作路径变成指定路径。这样就能解决我们自己编写的shell程序cd失效的问题。
既然前面myshell中cd失效只是因为子进程cd了,但myshell没有cd改变工作路径,那么让myshell进行进程替换,父进程替换成cd不就可以了吗,为什么需要用chdir函数呢?
1.因为父进程,即myshell替换后,的确是修改了父进程的工作路径,但修改后父进程会直接退出,不能完成myshell的功能。
函数int chdir(const char *path)
1.在头文件<unistd.h>中。chdir表示change working directory,更改当前工作路径(目录)。
2.参数path是个指针,指向表示路径的字符串。
3.执行成功则返回0,失败返回-1。
代码如下:
下图中cd成功则立刻continue,不会创建子进程。
运行结果如下:
解决了在myshell中cd命令失效的问题后,此时我们通过myshell可以再举个例子证明【进程只能修改自己的工作路径,不能修改其他进程的工作路径】这个理论是正确的,本例用系统shll进程,即bash和我编写的myshell进程说明。
系统shell是父进程,运行myshell程序时,系统shell会创建子进程myshell,所以系统shell是myshell的父进程。下图运行结果中修改myshell的工作路径后,系统shell的工作路径仍不受影响,不会随之发生改变。
代码如下:
运行结果如下:
2.4、myshell的测试代码
测试代码如下。myshell的大逻辑除了分割指令外,然后就只剩下了:(结合下图红框处思考)fork创建子进程,然后让子进程进行进程替换,同时让父进程waitpid等待子进程。
测试结果如下图。
2.5、在myshell中指令输错了,按BACK键删除不了怎么办?
如上图,按BACK键会输出^H,删除不了指令。解决方法是摁住CTRL,然后按BACK键即可。
2.6、myshell的整体代码如下
#include<stdio.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<unistd.h>
#include<string.h>
#define NUM 100
char cmd_line[NUM];
char* argv[NUM];
int main()
{
while(1)
{
printf("[chen@localhost myshell]#");
fflush(stdout);
if(fgets(cmd_line,sizeof(cmd_line),stdin)==NULL)
continue;
cmd_line[strlen(cmd_line)-1]='\0';
printf("echo:%s\n",cmd_line);
argv[0]=strtok(cmd_line," ");
int i=1;
while(argv[i++]=strtok(NULL," "))
{
;
}
if(strcmp(argv[0],"cd")==0)
{
if(argv[1]!=NULL)chdir(argv[1]);
continue;
}
pid_t id= fork();
if(id==0)
{
//child
printf("下面的功能是子进程做的");
execvp(argv[0],argv);
exit(1);
}
//father,由于子进程如果进程替换失败,则直接exit(1)结束进程了,如果替换成功,下面的代码子进程也看不见,所以在注释下面的代码全是父进程执行的
int status;
pid_t code=waitpid(id,&status,0);
if(code==id)
printf("等待子进程成功,子进程退出码为%d\n",WEXITSTATUS(status));
}
}