前言:
本篇我们将开始尝试构建我们的第一个LINUX的小程序----进度条作为一个十分常见的程序,在我们之后的工程实践中也是需要多次运用,但是介于我们目前还没有去学习网络等方面的知识,没法独立的去利用程序去下载一个真正的程序,自然没法根据程序去进行一个真实的下载环境,但是我们依旧可以拿出一个模拟下载的进度条程序。
1.前期预备知识讲解:
1.自动化构建代码:
让我们先想想我们在使用VS的时候的一些细节:我们只需要点击重新生成解决方案就可以直接直接重新编译我们当前的代码,而且是多个文件一起编译,但是在vim中我们对于多个文件,必须一个一个去gcc/g++,这就非常麻烦和繁琐,所以我们要寻找一种方法让我们的vim编译器也可以做到只需要一次指令就可以同时将多个文件一起编译,在这里,我便要引入我们的make和makefile.
首先,make是指令,而makefile是我们需要创建的一个不能改变名字的一个文件,你可以这样理解makefile:它是一个在当前目录下存在的一个具有特定格式的文本文件。
我们首先要创建一个makefile文件,注意,我们创建的时候名字必须是Makefile/makefile这两个,大小写没影响,但是名字不能变,如下:
有了这个文件,我们就可以使用make指令来自动执行makefile文件里面的命令了。
依赖方法与依赖关系:
我们的makefile文件里面命令的本质实际上就是两个:依赖方法和依赖关系,我们代码之间主要是依靠一个一个文件来封存,所以本质上文件之间的关系就反映着代码之间的关系,如下:
在这里,我写好了main.c process.c process.h三个文件,其中两个源文件,一个头文件,我们在书写C语言的程序的时候就知道,我们都是直接将源文件进行编译成可执行程序的,又根据我们前面程序的翻译中知道,我们的头文件是以链接的形式直接拷贝或者地址链接到程序中的,所以我们不需要对其进行编译展开,由上述的逻辑,我们大致构建出了一个文件之间的依赖关系:
main.c process.c两个文件要被编译成一个可执行程序文件,而头文件是链接进行的,由此,我们的可执行程序文件与两个源文件就是依赖关系,而他们的依赖方法就是将这两个源文件编译成一个可执行程序,头文件自己链接,由此,我们可以这样去写我们的Makefile的指令:
第一行是依赖关系,指的是我们的关系是process可执行程序和我们的两个源文件之间的关系,第二行需要先TAB空格后开始写,主要写依赖方法:在这里我们的$^代表的是冒号之后的依赖的对象,而 $@代表的是冒号之前的依赖对象,当然你直接写名字也是可以的,主要是这样写更加简便,然后这条指令我不多说,就是上篇文章我们说过的编译指令,后面的@ echo(注意这里@和echo之间要留有空格,否则这条指令执行会全是乱码,不简洁),这条指令就是将双引号里面的内容呈现到屏幕上,这个指令我们之前也学到过,它同样可以将对应的字符输入到文件中。
由此,我们的编译指令便构建了出来,现在只需要我们输入make,就可以自动编译代码成为可执行程序,如下:
这样当我们去查看我们的目录,一个process可执行程序便出现在了列表中,这便是我们的可执行程序。
但是,一个程序光可以编译是不够的,我们还需要它进行程序的清理,也就是要自动删除我们的全部程序文件,跟gcc/g++一样,倘若一个一个删就会太麻烦,所以我们同样使用利用程序将其一次清理指令全部执行的命令,即我们需要使用.PHONY伪代码来进行,这是由于我们的删除文件是没有依赖对象的,删除不需要跟其他文件搭建关系,但是我们的依赖关系如果不建立,依赖方法又是没法执行的,故我们通过使用伪代码来解决这个问题,.PHONY的特点在于:
.PHONY没有依赖关系这么一说,它会直接执行对应的依赖方法,即无论目标对象是否存在都会重新生成,不会在于文件新旧的问题,这是因为目标对象与其同名文件之间没有一定必要的相互关系。
故在这里我们可以这样总结:
.PHONY修饰的文件本身是和普通文件没有明显去别的,它只是总是被执行它对应的依赖方法,并且这种执行是强制性的。
由此,我们可以这样接着编译指令构建我们的删除指令,如下:
在这里,我们的clean即我们的对象,我们执行的指令当输入这个对象的时候,即当我们输入make clean的时候,我们就会去执行清理可执行程序process的指令rm -f,这样,我们就可以自动去清理多个程序文件,而不需要一个一个去清理了,如下:
make/makefile的自主推导性:
对于下面的代码:
mybin:code.ogcc code.o -o mybin
code.o:code.sgcc -c code.s -o code.o
code.s:code.i gcc -S code.i -o code.s
code.i:code.cgcc -E code.c -o code.i
此代码是可以执行的,虽然扫描文件的时候从上到下执行,但是由于makefile的自主推导性,它是这样去分析这些代码的:
从尾部向上依次按照文件的编译过程去找是否存在对应的文件,知道形成可执行程序,所以,我们的文件是不怕顺序问题的,主要是该有的逻辑我们是不能有缺失的,否则会影响程序的编译。
2.ACM时间与通过时间对文件新旧的判断:
我们倘若已经生成了一个process可执行文件,我们要是再次对其编译可以么?
倘若我们这样去执行,就会跳出这样的回应:
这条指令的意思就是说,文件已经更新到最新版本时间,不支持再次编译了,也就是说,此时文件是旧的,LINUX是可以分辨出来并且拒绝重复编译的,那么,LINUX是如何去区分一个文件是否被修改从而确定其是否可以重新编译的呢?
这就不得不提到我们的ACM时间了。
何为ACM时间呢?让我们stat任意一个文件来查看文件的详细信息,如下:
你会发现,有三个时间:
Access:读写访问时间,当我们打开文件读取内容时,就会修改这个时间
Modify:文件内容修改时间,当我们进入文件并修改文件内容时,就会修改这个时间
Change:文件属性修改时间,文件的属性基本随着内容的修改也在不断的被修改着
根据他们三个之间的特点,他们相应的关系如下:
ACM分别对应着三个时间的首字母,当C时间被修改时(比如文件的三个读写权限)它只会影响自己,另外两个时间不被修改,当我们去修改M时间时,则A时间和C时间都会由此而被改变,而修改A时间时,对应的C文件的属性也会被修改。
那,文件访问的本质是什么呢?
首先文件是被存储在磁盘上的,也就是说,访问文件本质上就是访问磁盘,这个过程的效率是很低的,很耗费时间,会影响我们程序的执行。由此,在LINUX中,我们是不会重复去编译文件的,因为LINUX有着庞大的文件,倘若依次修改磁盘,会导致出现大量的磁盘的IO操作,这严重影响了系统的效率给,故LINUX会尽可能的减少文件的修改次数。
所以回过来,我们的文件的新旧是如何被系统分辨出来的呢?
它主要依靠的实际上就是文件的修改时间,但是时间并非本质,通过时间对比出来的新旧才为本质,源文件通过与可执行文件的修改时间进行对比:
首先第一次的时候,我们一定是先有源文件,然后通过编译得到可执行程序,此时我们会得出结论,源文件的修改时间一定是小于可执行程序文件的修改时间的,然后从第二次开始,到后面之后的很多次修改,我们修改文件的时候,先去修改源文件,此时当我们再去编译的时候,此时的源文件的修改时间反而大于可执行程序的修改时间了,故此时两者的大小关系发生了变化,LINUX系统识别到了这种变化,意识到文件做出了修改,故此时认为文件为新,执行重新编译的指令,但旧文件就不会执行这个指令,由此,便通过源文件和可执行文件的修改时间的大小关系的变化去分辨新旧文件。
但是,这条规律在大部分情况下都没问题,问题的产生不仅仅是修改新文件就能解决的,有些历史问题,需要重新清理项目才可以解决。
那如何修改文件的时间呢?
我们有时对代码已经足够完美了,没必要修改的情况下,我们还想让其重新编译,这时我们就需要去更新我们的3个时间去让文件变成新的,这里我们就需要我们的touch指令来修改文件的时间戳:
touch -a/-c/-m:分别对应更新a,c,m三个时间,倘若不加,就是对整体的文件的三个时间进行更新。
在这里要补充一下:对于.PHONY的伪目标,它是无视时间的,只要调用就必定执行,没有新旧判断这一说。
好了,现在我们已经掌握了vim编译器软件,gcc/g++编译代码,自动化构建代码,我们接下来就开始正式进行我们的进度条小程序的书写。
2.进度条程序:
说到进度条,我在这里以LOL的进度条举例子,在我们进入LOL游戏之前的10人英雄界面,我们就有一个进度条和一个不断旋转的小圆圈,入下:
由于实在找不到旋转的图标了,但是我们大致看到右下角的这个进度数字标识,再加上我们的进度条和一个360度旋转的小光标,这便是我们整个进度条程序的全部组成部分,如下:
好了,大致的铺垫都完事了,现在让我们开始正式书写程序。
1.缓冲区:
在LINUX中提供了一个接口头文件<unistd.h>,通过它可以让我们使用sleep函数,让程序休眠一段时间(秒数),首先我们要明白,在C/C++语言中,针对标准输出,给我们提供了默认的缓冲区,主要是输出缓冲区,在stdout开启的同时stdin,stdeer也会随着stdout一同开启。
所以,本质上我们打印的本质是先将要打印的内容放到stdout的输出缓冲区中,然后立刻刷新输出缓冲区打印出内容,倘若我们延时sleep前刷新stdout,我们就会立刻打印出内容,而不是等了几秒才打印,而我们平时使用的换行符\n实际上也是一种强制刷新的方式,所以我们回车每次都可以强制刷新缓冲区。
C/C++为我们提供了fflush()函数来强制刷新缓冲区,利用它我们就可以不断刷新缓冲区从而实现动态进行程序的效果
2.回车换行:
注意,回车和换行是两个完全不同的概念,你可以去看我们的键盘。我们所谓的回车键是这样的:
注意一个细节,这里的回车键先向下再向左,但我们实际上的回车就是直接回到最左边,所以我们键盘上实际的回车键是先向下再回车而不是单纯回车的意思,这个不要理解错了。所以,为什么我们在程序输入的时候Enter会直接跳到下一行的最开始而不是直接下一行,这正是因为这个键位是回车加换行的意思,两个命令同时触发了,**在C语言中,换行就是‘\n’,而回车就是’\r’.**故倘若我们向让我们的光标一直停在一个位置开头,就每一次都回车一次让光标一直在最左边的开头位置即可。
同时注意,我们其实每一个向屏幕输入的数字也好,字母也好,各种符号也好,他们的本质都是以字符形式被识别和存储的,而根据我们占位符的不同去识别和理解这个字符对应的数据到底是什么,所以这才有ASCII表的存在,就是为了转化字符的。
程序代码如下:
有了上面的知识的铺垫,我们的进度条自然而然就出来了,代码如下:
process.c文件(函数文件)
本来是想给源码的,但是太麻烦了,就直接上图片了,抱歉~~~~~!!!
这个就是我们的函数代码,我称之为进度条最终版本,在这个版本里,我们主要要实现一个进度条配合程序实时的跟进,而不是进度条只是自己向后运行而不考虑实际的下载情况,所以我在这里采取的是传入比率的方式带入程序进行运算,我在这里定义了一个静态数组和一个静态的整型,其中静态数组是保证进度条一直向前进行的,由于我们的进度条是函数形式,出了函数进度条的数组就会被销毁(当然,倘若你定义一个全局变量的数组也可以),同时,为了保证我们的进度条加载卡住的时候我们的小光标依旧在选旋转,我们的这个整型变量配合上一个字符数组来保证我们小光标的实时更新,以告诉用户我们还在加载而不是卡住了,打印的过程中的那个\033[31;44是我加上的输出的字符的颜色和背景色,这个上网就能查到,注意看我fflush的位置,我是先让打印结果进入缓存区后,再刷新,这样就起到了在我主函数的延时之前先刷新字符串打印出我们的进度条。
在最后加载完成的时候,补上加载完成的提示语句,此时rate为100%,即为进度条加载完毕。
process.h文件(头文件)
main.c文件(主函数文件)
我们在这里配合着我们的头文件一起看主函数文件,头文件不多说,其实主函数文件也没什么说的,基本的代码大家都能看懂,但是在这里我想说一说,我们的进度条一般是作为回调函数来调用使用的,而不是直接放入程序内部,因为这个进度条可能需要实时更新,所以我们定义了一个函数指针并将其类型重定义为callback_t ,传入我们的加载download函数中为cb,配合着cb,这样,我们只需要传入我们每次想要使用的任意版本的进度条即可,这种方法效率很高,同时也很节省时间。usleep保证了时间的进一步缩小为毫秒为单位,更加接近真实的时间而不是sleep的慢速,同时我们还模拟了一个进度条卡住的情况,当rate大于百分之50的时候,由于total被限制为target的一半,故我们的rate也被限制在百分之50,从而就模拟了进度条卡住的情况。
通过这样写,我们的最终效果如下:
虽然很简陋,但是你依旧可以通过一些方式进一步优化这个建议的进度条**,不过在我看来,最重要的是一些我们缓冲区,延时函数,如何操作LINUX熟练的写程序,进度条为何利用回调函数来进行等等这些重要的知识才最为关键**
总结:
无论如何,我们的进度条代码算是完成了,这是我们的第一个LINUX的小程序,但不会是最后一个,写下了这个小程序,让我们更加熟练的使用LINUX去写代码,这才是我们的关键所在,学习LINUX不仅仅是会写代码,同时要对整个生态,系统的一些很多知识有更多的理解和新的感受,这才是最关键的。