Unix/Linux编程实践教程阅读笔记-more指令的实现-来自第一章的笔记-P1-P24

个人目前的阅读感受是:

这本书比较偏重于实践,看它之前可以先看一下CSAPP的第八章,对系统编程有一定了解后,读这本书更有助于触类旁通,收获更大。

 

第一章讲的东西不多,前面的一些基本Linux指令就不记录了,随便一查就知道,需要记录的是它对more指令的实现,里面有不少细节值得品味,包括它的思路,编程习惯,等等。

 

首先,还记得吗?我们在“CSAPP阅读笔记-异常,进程,进程控制-来自第八章8.1-8.4的笔记”中记录过:

命令行窗口本质就是一个shell程序,我们在其中输入指令(比如mkdir,ls,pwd等等)来调用一系列功能,指令分内建指令和外部指令,外部指令,比如ls,其实就是一个个可执行文件,存放在/bin目录下,当我们在命令行窗口输入ls时,可以显示当前目录下的文件,其实就是在运行/bin目录下的名为ls的可执行文件,那个可执行文件里实现了“显示当前目录下的文件”的功能,当然,这也说明,这个shell程序默认的外部指令的执行目录就是/bin,一旦发现收到的指令不是内建指令,就会自动到/bin目录下去寻找对应名字的可执行文件。

那shell程序是怎么实现输入ls就去运行名为ls的可执行文件的呢?

答案是shell程序里调用了fork+execve两个系统函数,创建子进程并加载对应的可执行程序,这两个函数的底层,反映到汇编代码上,就是汇编里的syscall指令。

那内建指令是怎么回事?

内建指令就是在shell程序里自带的一些功能的实现,它是在shell内部实现的,当一个指令被调用时,先检查是不是内建指令,若是,则直接在shell内部完成对应操作,若不是则去/bin下寻找其可执行文件并加载执行。比如常见的就是cd指令,我们在/bin目录下是找不到它的可执行文件的。

具体的实现可以参考之前的笔记,贴上传送门:

https://www.cnblogs.com/czw52460183/p/10812261.html

 

那么当用指令调用这些程序时,参数怎么传呢?比如ls -l,这个参数是怎么传递到可执行文件的?

答案是通过main函数的参数,比如int main(int argc,char *argv[]),其中argc代表参数的个数,argv是字符串数组,代表每个参数,需要注意的是,指令代表第一个参数,比如指令more,more czw.txt调用时参数有2个,argv[0]是more,argv[1]是czw.txt字符串。

 

下面介绍正题:

先简单介绍下more指令的作用:more指令可以查看文件内容并将它分屏显示,比如more czw.txt,会把当前目录下czw.txt的内容分屏显示,当czw.txt内容较多时,会先显示第一屏的内容后暂停,此时若按下空格,会继续显示下一屏的内容,若按下回车,会显示下一行内容,若按下"q",会退出。

配合上重定向和管道符号,more可以有特殊的用法:

比如 ls | more,此时会将当前目录下的文件分屏显示。

为什么会有这种效果?

因为Linux命令中的“|”是管道命令,作用是“把前一个命令原本要输出到屏幕的数据当作是后一个命令的标准输入”,因此ls的输出被当作more的输入,实现了分屏显示的效果。

 

那么现在我们要实现这个more指令,也就是写一个类似于/bin目录下的某个命令的程序,先来思考一下流程应该是怎样的?

很明显,more既然有查看文件内容的作用,那我们的程序里必然需要实现文件的打开和读取,按下回车后可以实现单行显示,那至少要有把字符串输出到屏幕的功能,此外,当内容不止一个屏幕时,还需要读取用户的输入,实现交互。

注意:这里补充两点,我们设计的more指令,当more后面没有参数时,则以标准输入作为分屏显示的内容。此外,more指令后面可以跟好几个文件名字,此时效果表现为分屏显示所有文件的内容(后面的文件内容紧接着前面的文件)。

设计流程如下:

1.打开传入参数对应的文件,注意,按照之前说的,这里的参数存放在main函数传入的argv中,此外,打开之前要先区分是否有参数,没有参数时,直接以标准输入作为源来读取文件内容,有参数时,则对每个参数打开文件并读取文件内容,打开文件时要注意,必须考虑到打开文件失败的情况,同时,对打开的文件读取后,要及时关闭此文件

2.读取文件时,要注意,因为有满屏后单行展示的可能,所以读取时不要一次全部读入,而是一次读一行,读取后要将它输出到屏幕上,注意,读取也要考虑到读取失败的情况,只有读取成功才会输出到屏幕。

3.输出到屏幕前要看下是否满屏,这个可以用行数来作判定,不满屏就直接输出,注意,输出到屏幕上也要考虑到输出失败的情况

满屏意味着需要显示提示信息到屏幕上,并且等待用户的进一步指示,可以用一个新的函数来完成,注意,要获取用户的进一步指示代表又一次调用读取文件函数,但此时不是读取一行,只是读取一个字符。读取完成后要作判断,根据不同的情况作相应的操作。

 

有了上述流程,我们来看下用哪些函数。

首先是要打开文件,我们这里使用fopen完成,它可以在当前目录下打开一个文件,它是对Linux系统函数open的封装,是C语言库函数,它和open的区别在于前者返回一个文件描述符,后者返回一个文件指针,大多数情况下用fopen而不是open(因为前者可移植)。需要注意的是fopen是带缓冲区的,这样的好处是在做进一步的读写操作时可以减少用户态和内核态的切换,于是在顺序访问文件时,就会更快,当然,如果是随机访问文件,由于缓冲区存放的之前访问的内容不是当前读取所需要的,仍需要切换到内核态进行读取,因此有缓冲区不会有访问速度的改善,反而由于fopen是对open的进一步封装,导致fopen速度不及open。

两者的详细介绍与用法可以参考:

https://www.cnblogs.com/hnrainll/archive/2011/09/16/2178706.html

https://blog.csdn.net/hairetz/article/details/4150193

https://www.runoob.com/cprogramming/c-function-fopen.html

 

fopen的函数原型是:

FILE *fopen(const char *filename, const char *mode)

这里的FILE是什么呢?它是C语言预定义的一个结构体,位于stdio.h中,是管理文件流的一种结构,这个结构体里面的组成项还是挺多的,含有文件名、文件状态和文件当前位置等信息,可以自己去定义的地方看看,但我们实际使用时其实不需要完全清楚结构体中每一项的作用,因为它就像open调用时的文件描述符一样,类似于一个句柄,我们获取它,是因为在以后的读写中需要这个FILE类型的文件指针参数,所以直接使用就行,它失败时会返回NULL。

 

其次是读取文件的函数,要支持一行一行地读取。我们使用fgets函数来实现,函数原型是:

char *fgets(char *str, int n, FILE *stream)

它也是C库函数,这个函数支持对指定字符数量的读取,因此要实现一行一行读取,我们要设定一个固定的值,代表每一行的字符数。

函数的用法参考:

https://www.runoob.com/cprogramming/c-function-fgets.html

注意fgets读取发生错误时返回的是NULL而不是EOF,这个在我们作读取失败的判定时有用。

 

接着是输出到屏幕的函数,其实需要的就是一个写入函数,只不过写入对象是标准输出设备,我们使用fputs函数来实现,函数原型是:

int fputs(const char *str, FILE *stream)

用法参考:

https://www.runoob.com/cprogramming/c-function-fputs.html

注意,fputs失败时返回EOF。

 

最后是读取单个字符用的函数,我们使用getchar函数实现,函数原型是:

int getchar(void)

用法参考:

https://www.runoob.com/cprogramming/c-function-getchar.html

注意,getchar失败时返回EOF。

 

好了,长长的前戏完成了,现在给出代码:

 

#include <stdio.h>
#include <stdlib.h>

//设置一页有10行,一行有20个字符
#define LINELEN 20
#define PAGELEN 10

void do_more(FILE *file);
int see_more();

int main(int argc,char *argv[])
{
    FILE *p;
    //若more指令后面没有参数,则把标准输入的内容作为输入文件
    if(argc==1)
    {
        do_more(stdin);
    }
    else
    {
        //依次打开每个参数对应的文件,输出到屏幕
        while(--argc)
        {
            if((p=fopen(*(++argv), "r")) != NULL)
            {
                do_more(p);
                fclose(p);
            }
            //若某个文件打开失败,则直接报异常退出
            else
            {
                exit(1);
            }
        }
    }
    return 0;
}

//对打开的文件,读取文件内容并输出到屏幕
void do_more(FILE *file)
{
    //行计数器
    int numberOfLines=0;
    int reply;
    //缓冲区,缓存读取的一行内容
    char temp[LINELEN];
    //循环读取一行文件内容
    while( fgets(temp, LINELEN, file)!= NULL)
    {
        //输出到屏幕前判断下是否满屏
        if(numberOfLines == PAGELEN)
        {
            reply = see_more();
            //当用户指示不在预设范围内,或指示为退出(q)时,要退出对此文件的操作
            //注意这里不能用exit(0),因为main函数里可能还有下一个文件要读取并输出
            if(reply==0)
            {
                break;
            }
            numberOfLines -= reply;
        }
        //不满屏时输出到屏幕且行计数+1
        //输出时要考虑到输出失败的情况,这里的应对是一旦某一行输出失败则程序异常退出
        if((fputs(temp, stdout)) == EOF)
        {
            exit(1);
        }
        numberOfLines++;
    }
}

//满屏后等待用户指示
int see_more()
{
    //提示用户满屏了
    printf("more?");
    int c;
    //获取用户指示
    while( (c=getchar()) != EOF )
    {
        if(c == 'q')
        {
            return 0;
        }
        if(c == ' ')
        {
            return PAGELEN;
        }
        if(c == '\n')
        {
            return 1;
        }
    }
    return 0;
}

基本上看着之前讲解的流程,配合注释就能看明白了,这里main函数里完成了对文件的打开,do_more函数用来读取文件并显示,see_more用来获取满屏时的用户指示,可以自己用gcc编译并测试一下。

 

但现在有两个问题,一是上面的程序当碰到如 ls | more这样的指令时,会出现无法分屏显示的情况,会全部显示出来。这是因为"|"把more的输入重定向到了ls的输出,这样当运行到getchar时,getchar会从ls的输出读取用户指示,导致无法得到正常的用户指示。

注意,之前我有一个疑惑:我认为重定向后,当进入see_more时,getchar既然会把ls的输出通过getchar读取,那很有可能读的就既不是"q",也不是空格或回车,那see_more返回0的话,do_more里会break,那这个文件就不再读取了,那应该只显示一屏的内容,为什么会输出所有的内容呢?

这是因为我的理解有误,这里ls | more,“|”虽然实现了重定向,但more指令后面是没有参数的,不能认为ls的输出是more指令的参数,因此在main函数里其实进入的是argc==1这个判断,此时ls的输出的内容其实经重定向,已经取代了stdin,成为了输入源,而不是文件名参数,而getchar是从标准输入获取指令的,这才造成了错误。 

 

那怎么解决这个由重定向导致的用户指示无法读取呢?

  我们需要先普及一些知识:

  /dev文件夹下有个tty文件,这是键盘和显示器的设备描述文件,向这个文件写相当于显示在用户屏幕上,读相当于从键盘获取用户的输入。

因此思路如下:遇到ls | more这种情况时,前面不用改变,也就是说,仍然由于argv==1,导致do_more从stdin(被重定向到ls的输出)中读取文件,但遇到满屏时,在see_more中不再从标准输入读取用户指示,而是从/dev/tty中读取即可。

  由于getchar是固定从标准输入读取的,所以我们需要换一个函数,支持从某个文件读取单个字符。

  我们选用getc函数,函数原型为:

int getc(FILE *stream)

  它读取失败时,返回的也是EOF。

  

  因此我们只需要改一下do_more和see_more即可,如下:

void do_more(FILE *file)
{
    int numberOfLines=0;
    int reply;
    char temp[LINELEN];
    //指定see_more的读取源
    FILE *fp;
    fp = fopen("/dev/tty","r");
    //打开失败则程序异常退出
    if(fp==NULL)
    {
        exit(1);
    }
    while( fgets(temp, LINELEN, file)!= NULL)
    {
        if(numberOfLines == PAGELEN)
        {
            reply = see_more(fp);
            if(reply==0)
            {
                break;
            }
            numberOfLines -= reply;
        }
        if((fputs(temp, stdout)) == EOF)
        {
            exit(1);
        }
        numberOfLines++;
    }
}

//see_more修改为带参数的函数,参数为读取源
int see_more(FILE *cmd)
{
    printf("more?");
    int c;
    //用getc从指定源获取用户指示
    while( (c=getc(cmd)) != EOF )
    {
        if(c == 'q')
        {
            return 0;
        }
        if(c == ' ')
        {
            return PAGELEN;
        }
        if(c == '\n')
        {
            return 1;
        }
    }
    return 0;
}

需要修改的地方已经用注释标出了,无需解释。

 

当然,这个more程序还有个问题,就是当获取用户指示时,需要敲入回车才能生效,如何改为只要接受指示就立刻生效呢?

可以用getch这个指令,它包含于<conio.h>这个头文件中,不是C库函数。但这个头文件是windows包含的函数库,mac下无法使用,因此需要找别的办法。

答案是使用system("stty -icanon"),这样读取字符时就不会缓存起来,而是立刻读取,当然,调用system ("stty -icanon")即可关闭上面的设置。

具体可以参考:

https://blog.csdn.net/dshf_1/article/details/86773054

https://zhidao.baidu.com/question/446573579.html

我们只要在main函数最开始加一句system("stty -icanon");   即可达到目的。

 

 

完结撒花🎉

posted on 2019-06-08 20:19  暴躁法师  阅读(326)  评论(0编辑  收藏  举报