你的第一个 Linux 系统程序:从进度条开始
编译器,编辑器,自动化构建的工具都掌握了,就可以实现 linux 的第一个系统程序——进度条了。
回车换行
平常都听到过“回车换行”,那么回车换行是一个动作吗?在学习 C 语言时,回车换行使用的都是 ’\n’,在 C++ 中,回车换行使用的是 std::endl。其实回车换行是两个动作,只是在 C 语言中,使用\n 表示了回车换行这两个动作。
换行是从当前行数,换到下一行的同一位置,如当前在第5行第6列,那么换行之后,就在第6行第6列;回车是将光标移动至当前行数的起始位置,如当前在第5行的第6列,那么回车之后,就在第5行的第1列。回车用 ’\r’ 表示;换行用 ’\n’ 表示。在C语言中会将 ’\n’ 自动解释成 ’\r’和’\n’。
开始前的准备:新建文件 code.c,打开 code.c 文件,写入以下代码:
#include <stdio.h>
#include <unistd.h>int main()
{printf("hello linux\r\n");return 0;
}
Makefile中的的内容:
code:code.cgcc -o $@ $^.PHONY:clean
clean:rm -rf code
linux 系统中有个 sleep 函数,函数信息如下图所示:

使用 sleep 函数需要包含头文件 <unistd.h>,这是 linux 系统特有的头文件,函数原型为: unsigned ine sleep(unsigned int seconds),功能:让程序休眠 seconds 秒。
可以将其运用到 code.c 文件中,代码如下所示:
#include <stdio.h>
#include <unistd.h>int main()
{printf("hello linux\r\n");sleep(2);return 0;
}
预测程序执行的现象,程序是由上到下依次执行的,先执行 printf 函数,再执行 sleep 函数,所以现象为先打印 hello linux,再休眠2s。
验证预测,输入make指令,结果如下所示:

程序果然先打印,后休眠,符合我们的预期。如果将 \r\n 换成 \n,与 \r\n 是一样的效果。
如果将回车换行符去掉,会出现怎样的现象?


可以观察到前2秒根本没有打印,程序结束后,才打印 hello linux,即先休眠,再打印。
分析:
那么该程序是先执行 printf 函数,还是先执行 sleep 函数?我们需要知道 C 语言程序的是按顺序结构运行的,除非有选择结构或循环结构,这里并没有选择结构和循环结构,所以程序先执行 printf 函数,再执行 sleep 函数。
既然如此,为什么我们看到的现象是先 sleep 两秒,再打印”hello linux”?正如我们所分析的那样,先执行 printf 函数,再执行 sleep 函数,也就是说,在 sleep 两秒期间,printf 函数已经执行完毕了,但是 "hello linux" 并没有显示在终端上,那么它存在哪?"hello linux" 字符串被缓存起来了,被存储到某个内存空间中,所以没有被打印到终端上。
但是在程序结束时,终端上却显示“hello linux”,这又是为什么?因为程序结束时缓存区内部的数据会自动刷新。为什么我们带上回车换行符却是直接显示出来了呢?因为显示器刷新数据的刷新方式是行刷新,遇到 ‘\r\n’ 或 ’\n’,以行为单位进行刷新。
当输出“hello linux”字符串时,这串字符串被保存到内存当中,printf 函数会判断 ”hello linux” 字符串是否携带了 ’\n’ 或者 ’\r\n’。如果带了,会被立即刷新到显示器上;如果没带,该字符串会一直存储到内存中,直到程序运行结束,才会刷新出来。
如果不带 ’\n’ 或者 ’\r\n‘ 就想立即看到结果字符串,应该怎么做?可以使用 C 语言中的 fflush 函数。函数信息如下所示:

该函数的作用是:强制刷新缓冲区。函数的参数是FILE* stream,这是什么?
默认任何程序运行时,基于需求都会打开三个流,标准输入,标准输出,标准错误,对应项分别为stdin,stdout,stderr。可以通过 man 手册查看:

stdin 对应的设备是键盘;stdout,stderr 对应的设备是显示器。linux 系统下一切皆文件,键盘和显示器都是文件。
缓冲区是有归属的,它属于显示器。这也是为什么刷新缓冲区,缓冲区的内容显示在显示器上,而不是显示在其它文件中。而显示器对应的文件是stdout。总之,如果想要强制刷新缓冲区,使用fflush 函数,参数填写 stdout。
去掉字符串后面的’\r\n‘,在 code.c 文件新增 fflush(stdout),代码如下所示:
#include <stdio.h>
#include <unistd.h>int main()
{printf("hello linux");fflush(stdout);sleep(2);return 0;
}
运行结果:

倒计时
在有了 ’\r\n‘ ,缓冲区和刷新的认识之后,可以利用这些知识实现一个10秒倒计时功能,倒计时是在同一个位置上不断刷新。
先实现9秒倒计时,既然要在同一个位置上不断刷新,那么就不需要换行。
int main()
{int count = 9; //倒计时while(count >= 0){printf("%d", count);sleep(1); //1s后再次打印count--;}return 0;}
结果:


问题:在没有换行的情况下,程序运行结束之后才显示结果,没和预期所想在同一位置上覆盖式的写。
解决方法:既然程序运行结束之后结果才显示出来,就需要强制将结果显示到显示器上,因此需要用到 fflush 函数;没和预期所想在同一位置上覆盖式的写,就需要每次写完之后,将光标回到当前行的起始位置。
改进后的代码:
int main()
{int count = 9; //倒计时while(count >= 0){printf("%d\r", count);fflush(stdout);sleep(1); //1s后再次打印count--;}return 0;}
结果:


问题:由结果可以知道,倒计时结束之后,最后的数字0被命令行给覆盖了。
解决方法:在倒计时结束之后,换行。
优化后的代码:
int main()
{int count = 9; //倒计时while(count >= 0){printf("%d\r", count);fflush(stdout);sleep(1); //1s后再次打印count--;}// 倒计时完毕后,换行printf("\n");return 0;
}
结果:


达到了预期的结果,可以尝试优化代码,倒计时前带上提示信息,优化后的代码如下所示:
int main()
{int count = 9; //倒计时while(count >= 0){printf("倒计时:%d\r", count);fflush(stdout);sleep(1); //1s后再次打印count--;}// 倒计时完毕后,换行printf("\n");return 0;
}
结果:

倒计时9完成了,接下来实现倒计时10。
将 count 更改为10,运行程序,观察程序的运行结果


问题:倒计时是“10,90,……,00”?
分析:在学习文件的权限,介绍文件类型有哪些时,曾提到一个问题,当向显示器写入数据12345时,输出的是整数12345,还是字符’1’’2’’3’’4’’5’?答案是:字符‘1‘’2‘’3‘’4‘’5‘。以显示器的视角,显示器输出的所有数据都是字符。printf 函数格式化输出,将所有非字符型的数据经过格式化转化成字符型。之所以这里倒计时是10,90,……,00,是因为开始打印数字10,显示器上打印的是’1’’0’两个字符,\r光标回到当前行的起始位置,接下来打印数据9,9覆盖住了1,0没有被覆盖,因此倒计时是10,90,……,00。
解决方法:让所有输出的数据的标准长度都为2。
代码:
int main()
{int count = 10; //倒计时while(count >= 0){printf("倒计时:%2d\r", count);fflush(stdout);sleep(1); //1s后再次打印count--;}// 倒计时完毕后,换行printf("\n");return 0;
}
结果:

被保留的是后面的位置(右对齐),如果想被保留的是前面的位置(左对齐),需要在2d的前面加上 - 。代码如下所示:
int main()
{int count = 10; //倒计时while(count >= 0){printf("倒计时:%-2d\r", count);fflush(stdout);sleep(1); //1s后再次打印count--;}// 倒计时完毕后,换行printf("\n");return 0;
}
结果:

进度条
要实现的进度条的结构:
![]()
先预留出100个字符的空间,进度条每走一步,[ ]就多一个’=’,在进度条向后走的过程中,进度会不断的增加,此外最后[ ]表示光标的状态,表示进度条是在往后走的。
实现前的准备:新建文件夹 probar,多文件 probar.h probar.c main.c。函数声明写在.h文件中,函数实现写在probar.c文件中,main.c 用于调用函数。
makefile 文件中的内容:
BIN=probar
CC=gcc
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)$(BIN):$(OBJ)@$(CC) -o $@ $^
%.o:%.c@$(CC) -c $<.PHONY:clean
clean:@rm -rf $(BIN) $(OBJ)
probar.h 中的代码:
#pragma once
#include <stdio.h>// 进度条函数
void Probar();
Probar 函数的初始实现为:
#define N 101
#define STYLE '='void Probar()
{ //提前开辟好空间 0~100共101个,所以需要101大小的空间 char probarbuff[N]; // 初始化probarbuff数组 memset(probarbuff, '\0', sizeof(probarbuff)/sizeof(char)); //计算循环的打印次数 int count = 0; // 循环打印进度条 while(count <= 100) { // 进度条 printf("[%s]\n", probarbuff); // 每循环一次,进度条就增加一个= probarbuff[count++] = STYLE; // 每打印一次休眠1秒 sleep(1);}
}
结果:

问题:进度条是一行一行的,但是要求是在同一行上覆盖式的写。
解决方法:不应该换行;每次打印进度条时,将光标移动到当前行的起始位置,因此需要回车 ’\r’。
优化后的代码:
#define N 101
#define STYLE '='void Probar()
{ //提前开辟好空间 0~100共101个,所以需要101大小的空间 char probarbuff[N]; // 初始化probarbuff数组 memset(probarbuff, '\0', sizeof(probarbuff)/sizeof(char)); //计算循环的打印次数 int count = 0; // 循环打印进度条 while(count <= 100) { // 进度条 printf("[%s]\r", probarbuff); // 每循环一次,进度条就增加一个= probarbuff[count++] = STYLE; // 每打印一次休眠1秒 sleep(1);}
}
结果:

问题:没有立即打印到显示器上。
解决方案:打印的字符串都在缓冲区内,需要使用 fflush 函数强制刷新显示在显示器上。
优化后的代码:
#define N 101
#define STYLE '='void Probar()
{ //提前开辟好空间 0~100共101个,所以需要101大小的空间 char probarbuff[N]; // 初始化probarbuff数组 memset(probarbuff, '\0', sizeof(probarbuff)/sizeof(char)); //计算循环的打印次数 int count = 0; // 循环打印进度条 while(count <= 100) { // 进度条 printf("[%s]\r", probarbuff); fflush(stdout); // 每循环一次,进度条就增加一个= probarbuff[count++] = STYLE; // 每打印一次休眠1秒 sleep(1);}
}
测试结果:

问题:实际测试可以发现,进度条移动的很慢。
解决方法:若想要移动得快一些,可以使用函数 usleep。usleep 函数的信息如下所示:

sleep 和 usleep 都是控制程序的输出时间,usleep 的时间的单位是微秒,1秒等于10的6次方微秒。
优化后的代码:
#define N 101
#define STYLE '='void Probar()
{ //提前开辟好空间 0~100共101个,所以需要101大小的空间 char probarbuff[N]; // 初始化probarbuff数组 memset(probarbuff, '\0', sizeof(probarbuff)/sizeof(char)); //计算循环的打印次数 int count = 0; // 循环打印进度条 while(count <= 100) { // 进度条 printf("[%s]\r", probarbuff); fflush(stdout); // 每循环一次,进度条就增加一个= probarbuff[count++] = STYLE; // 每打印一次休眠50000微秒 usleep(50000);}
}
结果:

![]()
进度条的移动速度果然变快了。
问题:对比两图可以发现,进度条的一部分被命令行覆盖了。
解决方法:进度条打印结束之后,需要换行。
结果:

问题:可以发现,进度条打印打印过程中,右方括号是移动的,这显然与实际情况不符,我们想要的情况是右侧不移动。
解决方法:字符串的长度定为100。
优化后的代码:
#define N 101
#define STYLE '='void Probar()
{ //提前开辟好空间 0~100共101个,所以需要101大小的空间 char probarbuff[N]; // 初始化probarbuff数组 memset(probarbuff, '\0', sizeof(probarbuff)/sizeof(char)); //计算循环的打印次数 int count = 0; // 循环打印进度条 while(count <= 100) { // 进度条 printf("[%100s]\r", probarbuff); fflush(stdout); // 每循环一次,进度条就增加一个= probarbuff[count++] = STYLE; // 每打印一次休眠50000微秒 usleep(50000);} printf("\n");
}
结果:
![]()
问题:进度条是反向的,因为默认是右对齐。
解决方法:若要正向打印,需要在100s的前面加上 - (左对齐)。
新增内容:显示进度条的进度,在这里进度可以认为就是计数器 count ,代码如下所示:
#define N 101
#define STYLE '='void Probar()
{ //提前开辟好空间 0~100共101个,所以需要101大小的空间 char probarbuff[N]; // 初始化probarbuff数组 memset(probarbuff, '\0', sizeof(probarbuff)/sizeof(char)); //计算循环的打印次数 int count = 0; // 循环打印进度条 while(count <= 100) { // 进度条 printf("[%-100s][%d]\r", probarbuff, count); fflush(stdout); // 每循环一次,进度条就增加一个= probarbuff[count++] = STYLE; // 每打印一次休眠50000微秒 usleep(50000);} printf("\n");
}
结果:
![]()
若想要进度显示%,应该怎么做?在%d的后面加上%%。
当我们下载数据时,会发现进度条不移动,进度也不动,那么此时有两种情况:1.进度条在移动,只是网速较慢,进度条移动不明显,进度变化不明显;2.卡死了。为了区分这两种情况,我们可以对当前实现的进度条新增功能——旋转光标,显示在进度的后面。光标有4种状态:| / — \,使用用数组实现。每次进度条打印,光标都显示不同的状态,这4种状态反复呈现。
#define N 101
#define STYLE '='void Probar()
{ //提前开辟好空间 0~100共101个,所以需要101大小的空间 char probarbuff[N]; // 初始化probarbuff数组 memset(probarbuff, '\0', sizeof(probarbuff)/sizeof(char)); // 旋转光标的四个状态 static char lable[4] = { '|', '/', '-', '\\' };//计算循环的打印次数 int count = 0; // 循环打印进度条 while(count <= 100) { // 进度条 printf("[%-100s][%d%%][%c]\r", probarbuff, count, lable[count%4]); fflush(stdout); // 每循环一次,进度条就增加一个= probarbuff[count++] = STYLE; // 每打印一次休眠50000微秒 usleep(50000);} printf("\n");
}
结果:
![]()
当前实现的进度条基本上能够满足我们的使用场景,但是如果要运用到实践中,还差一点。我们实现的只是进度条的第一版本,用它来理解进度条的基本结构和功能的实现。进度条是依赖于某个场景(下载,安装),接下来就将它运用到场景中。
进度条第二版本以第一版本为蓝本,场景为下载某个文件大小1024.0MB,网速1.0,每秒下载1MB。
main.c 文件种写入以下代码:
// 两个全局变量
double total = 1024.0; // 下载的文件的大小
double speed = 1.0; // 网速1.0// 下载场景
void DownLoad()
{// 当前下载的总量默认为0.0double cur = 0.0;// 下载---下载总量等于文件的大小时,也需要刷新进度条while(cur <= total){ // 进度条的刷新FlushProcess(total, cur);cur += speed; // 模拟下载usleep(50000); // 网络延迟}
}int main()
{Download();return 0;
}
接下来的问题是如何实现 FlushProcess 函数?FlushProcess函数的功能为刷新进度条,应该按照下载进度来更新进度条。所以 FlushProcess 函数的参数为目标文件的大小,当前的下载进度,即 total 和 cur。
probar.h 文件中新增函数声明:void FlushProcess(double total, double cur)。
FlushProcess函数的初始实现:
void FlushProcess(double total, double cur)
{// 避免进度条进度大于100%if(cur > total){cur = total;}// 进度条的进度double rate = cur / total * 100;// 进度条buff中=的个数int count = (int)rate; // 取整// 提前开好101大小的空间char probarbuff[N];// 初始化memset(probarbuff, '\0', sizeof(probarbuff)/sizeof(char));int i = 0;for(; i < count; i++){probarbuff[i] = STYLE; }// 刷新进度条printf("[%-100s][%f%%]\n", probarbuff, rate);
}
结果:

问题:进度条确实刷新了,但是进度条是一行一行的刷新,要求在同一行上刷新; 此外此外进度条进度的精度太高了,可以将进度保留一位小数
解决方法:将’\n’换成’\r’,此外可以进一步优化,添加 fflush 函数,不要等到 FlushProcess 函数执行完毕才打印进度条,当进度条刷新了,就立马打印;在%f的前面加上 .1。
优化后的代码:
void FlushProcess(double total, double cur)
{// 避免进度条进度大于100%if(cur > total){cur = total;}// 进度条的进度double rate = cur / total * 100;// 进度条buff中=的个数int count = (int)rate; // 取整// 提前开好101大小的空间char probarbuff[N];// 初始化memset(probarbuff, '\0', sizeof(probarbuff)/sizeof(char));int i = 0;for(; i < count; i++){probarbuff[i] = STYLE; }// 刷新进度条printf("[%-100s][%.1f%%]\r", probarbuff, rate);fflush(stdout);
}
结果:
![]()
![]()
问题:由上图可以发现,部分进度条被命令行覆盖了,因为没有换行。那么换行应该写在哪?
解决方法:写在FlushProcess函数中,当下载完毕,即 cur 大于等于 total 时,需要换行。
结果:

新增旋转光标功能,这里的旋转光标与下载文件的大小 total,进度条的进度 cur 没有任何关系,因为它是用于分析进度条的状态的,因此旋转光标应该与 FlushProcess 函数的调用次数有关,因为每次调用该函数,都会刷新进度条,进度条在刷新就能说明进度条在移动。
代码如下图所示:
void FlushProcess(double total, double cur)
{// 避免进度条进度大于100%if(cur > total){cur = total;}// 进度条的进度double rate = cur / total * 100;// 进度条buff中=的个数int count = (int)rate; // 取整// 提前开好101大小的空间char probarbuff[N];// 初始化memset(probarbuff, '\0', sizeof(probarbuff)/sizeof(char));int i = 0;for(; i < count; i++){probarbuff[i] = STYLE; }// 旋转光标static char lable[4] = { '|', '/', '-', '\\' };static int index = 0; // FlushProcess函数的调用次数// 刷新进度条printf("[%-100s][%.1f%%][%c]\r", probarbuff, rate, lable[index++]);index %= 4; //始终控制index在一定的范围内fflush(stdout);if(cur >= total){printf("\n");}
}
接下来测试当进度不再更新(即注释cur += speed),光标是否还会旋转?
![]()
![]()
为了使进度条更加的真实,可以设置网络浮动,每次下载的大小都不一样,就是生成随机数,实现一个函数来执行网络浮动的功能。
代码如下所示:
// 网络浮动 --- start为基准网速,range为浮动上限
double SpeedFloat(double start, double range) // range为浮动范围
{// 随机数为整数,去浮动范围的整数部分int int_range = range;// 生成浮动范围内的随机数 模int_range// range - int_range 是浮动范围的小数部分return start + rand()%int_range + (range - int_range);
}// 下载场景
void DownLoad()
{// 当前下载的总量默认为0.0double cur = 0.0;// 下载---下载总量等于文件的大小时,也需要刷新进度条while(cur <= total){ // 进度条的刷新FlushProcess(total, cur);cur += SpeedFloat(speed, 6.6); // 模拟下载usleep(50000); // 网络延迟}
}
结果:
![]()
问题:为什么进度条的进度没有达到100%?进度条是没有问题的,有问题的是下载函数的循环下载部分。当 cur 为1020多时,加上 SpeedFloat 函数的返回值,可能会大于 total,大于 total 不满足循环的条件,跳出循环,但是进度条还没有刷新呀!所以会看到进度条进度不为100%。
解决方法:在循环中再加上一层判断,当 cur 大于 total 时,让 cur 等于 total,再次刷新进度条,最后跳出循环。
代码:
// 下载---下载总量等于文件的大小时,也需要刷新进度条
while(cur <= total)
{ // 进度条的刷新FlushProcess(total, cur);cur += SpeedFloat(speed, 6.6); // 模拟下载if(cur > total){cur = total; // 模拟下载完成FlushProcess(total, cur);break;}usleep(50000); // 网络延迟
}
为了与实际相符合,下载的文件大小是不一样的,可以给 DownLoad 函数传参。
DownLoad 函数还有另外一种写法,当前 DownLoad 函数和 FlushProcess 函数高度耦合,一旦FlushProcess 函数的函数名被修改,DownLoad 函数中所有涉及 FlushProcess 函数的地方都需要做出调整,因此可以使用函数指针,DownLoad 函数使用回调函数。
逐一展示三个文件中的代码
probar.h
#pragma once
#include <stdio.h>// 进度条函数
void Probar();void FlushProcess(double total, double cur);
probar.c
#include "probar.h"
#include<unistd.h>
#include<string.h>#define N 101
#define STYLE '='void FlushProcess(double total, double cur)
{// 避免进度条进度大于100%if(cur > total){cur = total;}// 进度条的进度double rate = cur / total * 100;// 进度条buff中=的个数int count = (int)rate; // 取整// 提前开好101大小的空间char probarbuff[N];// 初始化memset(probarbuff, '\0', sizeof(probarbuff)/sizeof(char));int i = 0;for(; i < count; i++){probarbuff[i] = STYLE; }// 旋转光标static char lable[4] = { '|', '/', '-', '\\' };static int index = 0; // FlushProcess函数的调用次数// 刷新进度条printf("[%-100s][%.1f%%][%c]\r", probarbuff, rate, lable[index++]);index %= 4; //始终控制index在一定的范围内fflush(stdout);if(cur >= total){printf("\n");}
}
main.c
#include "probar.h"
#include <unistd.h>
#include <time.h>
#include <stdlib.h>double gtotal = 1024.0; // 下载的文件的大小
double speed = 1.0; // 网速1.0// 函数指针
typedef void (*call_back) (double total, double cur);// 网络浮动
double SpeedFloat(double start, double range) // range为浮动范围
{// 随机数为整数,去浮动范围的整数部分int int_range = range;// 生成浮动范围内的随机数 模int_range// range - int_range 是浮动范围的小数部分return start + rand()%int_range + (range - int_range);
}// 下载场景
void DownLoad(double total, call_back cb)
{// 随机数种子srand(time(NULL));// 当前下载的总量默认为0.0double cur = 0.0;// 下载---下载总量等于文件的大小时,也需要刷新进度条while(cur <= total){ // 进度条的刷新cb(total, cur);cur += SpeedFloat(speed, 6.6); // 模拟下载if(cur > total){cur = total; // 模拟下载完成cb(total, cur);break;}usleep(50000); // 网络延迟}
}int main()
{//Probar();DownLoad(490, FlushProcess);return 0;
}
在C语言中可以输出彩色文本,可以在网上查找,下面提供几个颜色代码的定义:
// 颜色代码定义
#define RED "\033[0;31m"
#define GREEN "\033[0;32m"
#define YELLOW "\033[0;33m"
#define BLUE "\033[0;34m"
#define PURPLE "\033[0;35m"
#define CYAN "\033[0;36m"
#define WHITE "\033[0;37m"
#define RESET "\033[0m"
将进度条设置为紫色:
代码:
printf(PURPLE"[%-100s][%.1f%%][%c]\r" RESET, probarbuff, rate, lable[index++]);
结果:
![]()
知识回顾
告别手动编译:用Makefile自动化你的Linux项目-CSDN博客
深入浅出GCC:Linux下的C/C++编译利器_linuxc++的编译器-CSDN博客
