【Linux 系统篇(十)】基础开发工具(五) —— 第一个系统程序 - 进度条

【Linux 系统篇(十)】基础开发工具(五) —— 第一个系统程序 - 进度条

大家好,欢迎来到 huangjin007_ 的博客
个人主页:huangjin007_
🔥文章收录专栏:Linux 内功修炼手册(系统篇)
总会有一些坚持
能从冰封的土地里
培育出十万朵怒放的蔷薇

Linux 系统篇(十) —— 第一个系统程序 - 进度条

文章目录

  • Linux 系统篇(十) —— 第一个系统程序 - 进度条
    • 一、回车与换行
      • 1.1 老式打字机
      • 1.2 计算机中的表示:`\r` 和 `\n`
    • 二、行缓冲区
      • 2.1 三个现象对比
      • 2.2 缓冲区到底是啥?
    • 三、练手程序:一个简单倒计时
    • 四、进度条项目:从朴素版本到工程化实现
      • 4.1 朴素进度条 V1(原地刷新版)
      • 4.2 更通用的进度条:引入下载任务模拟
      • 4.3 模拟下载和上传任务
      • 4.4 工程化管理:编写 Makefile
    • 五、完整代码汇总
      • process.h
      • process.c
      • main.c
      • Makefile
    • 结语:

一、回车与换行

在正式开始写进度条之前,有一个绕不开的基础概念需要先讲清楚——回车(Carriage Return)换行(Line Feed)

1.1 老式打字机

故事要从很久以前的机械打字机说起。在那个年代,打字机上有一个可以左右移动的“字车”,上面装着纸张。当你敲击按键时,字车会把纸张上的墨带打到纸上,留下字符。

  • 回车:打完一行字之后,你需要把字车推回到最左边,这就是“回车”。它对应一个动作:把打印头移动到当前行的行首
  • 换行:如果你想开始写下一行,就需要把纸张向上卷起一行,这个动作叫做“换行”。它对应:把纸张向上移动一行

在机械打字机上,这两个动作是分开操作的。先回车,再换行,才能从一行的末尾顺利移动到下一行的开头。

1.2 计算机中的表示:\r\n

后来,这些概念被带到了计算机和电传打字机中,并成为了字符编码的一部分。在 C 语言中,我们用转义字符来表示它们:

  • \r—— 代表回车,功能是把光标移动到当前行的行首
  • \n—— 代表换行,功能是把光标移动到下一行(列位置不变)

在 Linux 环境下编程时,如果你单独使用\r,光标就会回到当前行的开头,但不会向下换行。这恰恰是实现进度条动态刷新的关键——我们可以反复在同一行的开头打印新的内容,从而覆盖掉旧内容,给用户造成一种“原地更新”的视觉感受。


二、行缓冲区

看下面三段代码,猜猜它们的运行现象有什么不同。

2.1 三个现象对比

代码一:带\n的 printf

#include<stdio.h>#include<unistd.h>intmain(){printf("hello Linux!\n");sleep(3);return0;}

运行结果:程序一开始就打印出hello Linux!,然后等待 3 秒,程序结束。

代码二:没有\n的 printf

#include<stdio.h>#include<unistd.h>intmain(){printf("hello Linux!");sleep(3);return0;}

运行结果:程序运行后,屏幕上什么也没有,一片空白,等待 3 秒后,在程序即将退出时,才突然弹出hello Linux!

为什么会这样?难道printf没有执行吗?当然不是,printf已经执行了,但输出的内容并没有立刻显示到屏幕上,而是被放到了一个叫“缓冲区”的地方。

代码三:不带\n,但手动刷新缓冲区

#include<stdio.h>#include<unistd.h>intmain(){printf("hello Linux!");fflush(stdout);// 强制刷新标准输出缓冲区sleep(3);return0;}

运行结果:程序一开始就打印出hello Linux!,然后才等待 3 秒。

2.2 缓冲区到底是啥?

简单来说,缓冲区(Buffer)是 C 语言标准库在内存中开辟的一块临时空间,用来暂存待输出的数据

为什么要有缓冲区?
因为直接操作硬件(比如显示器、磁盘)是非常慢的。如果每次调用printf都立刻去写硬件,程序的性能会非常差。标准库的做法是:先把你要输出的数据收集起来,等攒够一批,或者遇到某些特定条件时,再一次性写出去。

对于标准输出stdout,它的缓冲模式通常是行缓冲。也就是说,遇到以下几种情况时,缓冲区才会被“刷新”(真正写到屏幕):

  1. 缓冲区满了
  2. 遇到换行符\n(行缓冲的核心特征)。
  3. 程序正常结束returnexit),系统会刷新所有缓冲区。
  4. 手动调用fflush(stdout)强制刷新
  5. 等待输入时(比如scanf有时也会触发刷新,但这取决于实现,不绝对)。

回到代码二的例子:printf("hello Linux!");执行后,字符串被写入了输出缓冲区,但因为没有\n,缓冲区没满,程序也没结束,所以它就一直待在内存里。紧接着sleep(3)让程序暂停了 3 秒,这期间屏幕当然是一片空白。直到main函数执行return 0准备退出时,系统才会自动清理并刷新缓冲区,于是字符串才被打印出来。

代码一因为带了\nprintf遇到换行符,立即触发刷新,所以字符串直接显示。代码三则是通过fflush(stdout)人工干预,强制把缓冲区的数据推送到终端。


三、练手程序:一个简单倒计时

有了\rfflush的知识,我们可以先写一个小例子巩固一下——一个在终端原地倒计时的程序。

#include<stdio.h>#include<unistd.h>intmain(){inti=10;while(i>=0){printf("%-2d\r",i);// 用 \r 让光标回到行首fflush(stdout);// 立刻刷新,否则看不到变化i--;sleep(1);// 每秒更新一次}printf("\n");// 倒计时结束后换行,让命令行提示符正常显示return0;}

解析:

  • "%-2d":格式化打印一个左对齐、宽度为 2 的整数。-表示左对齐,2表示至少占两位。这样当数字从10变成9时,9后面会多一个空格,正好把原来10中个位的0覆盖掉,不会留下残影。
  • "\r":打印完数字后,光标回到行首,而不是换到下一行。这样下一次printf就会从行首开始打印,覆盖掉上一次的数字。
  • fflush(stdout):由于printf的格式串中没有\n,行缓冲不会自动刷新,所以必须手动强制刷新,否则屏幕上始终不会有任何变化。
  • sleep(1):让程序暂停 1 秒,模拟倒计时的间隔。
  • 循环结束后单独打印一个"\n":倒计时完成时,把光标移到下一行,这样 shell 提示符会另起一行,而不是直接黏在0后面。

运行效果:你会看到屏幕上数字从10逐步递减到0,全程只在同一行原地变化,不会刷出一屏新行。


四、进度条项目:从朴素版本到工程化实现

掌握了倒计时的原理,进度条的实现思路就非常清晰了:我们也是用\r反复覆盖同一行,只是这次显示的内容不是数字,而是一个由字符拼接而成的进度条(比如[### ][50%]),并且还需要加一点小细节。

下面我们从最简单的版本开始,逐步演进到模块化、带回调函数、支持模拟下载和上传的完整工程。

4.1 朴素进度条 V1(原地刷新版)

我们先写一个最基本的进度条函数,它从 0% 跑到 100%,每次刷新一行。

#include<stdio.h>#include<unistd.h>#include<string.h>#defineNUM101// 进度条总格数 + 1 (用于字符串结尾)#defineSTYLE'='// 进度条填充字符voidprocess(){charbar[NUM];memset(bar,0,sizeof(bar));// 全部初始化为 '\0'constchar*label="|/-\\";// 旋转动画的字符序列intlen=strlen(label);intcount=0;while(count<=100){// 用 %-100s 保证进度条字符串宽度固定为 100,不足的部分用空格填充printf("[%-100s][%d%%][%c]\r",bar,count,label[count%len]);fflush(stdout);bar[count]=STYLE;// 每次往 bar 里多填一个 '='usleep(30000);// 休眠 30 毫秒,控制进度速度count++;}printf("\ndownload successfully!\n");}intmain(){process();return0;}

代码详解:

  • bar[NUM]是一个大小为 101 的字符数组,前 100 个位置用来放进度条字符=,最后一个位置始终为'\0'(由memset保证)。进度从 0% 开始时,bar里全是空字符,printf%-100s打印时会输出 100 个空格(因为空字符不会显示,但宽度占用了)。随着循环递增,bar[count]被赋值为=,进度条逐渐被填满。
  • "%-100s"-表示左对齐,100表示字段最小宽度为 100。如果字符串长度不足 100,剩余位置会用空格填充。
  • label是一个包含 4 个字符的字符串:'|''/''-''\'(注意反斜杠在字符串中需要转义,所以写成'\\')。每次循环取label[count % len],就能顺序循环播放这四个字符,产生一个旋转小棒的效果,用来提醒用户下载正在进行中。
  • usleep(30000):让程序暂停 30000 微秒,也就是 30 毫秒。因为sleep只能以秒为单位,这里用usleep实现更精细的时间控制(需要#include <unistd.h>)。

运行这个程序,你会看到一个从[ ]逐渐变为[====================================]的进度条,后面跟着百分比和一个不停旋转的字符。

4.2 更通用的进度条:引入下载任务模拟

上面的process()函数虽然能跑,但它是“自娱自乐”型——进度条自己控制自己的进度,没有和外部任何任务挂钩。实际应用中,进度条应该反映某个真实任务的完成情况,比如下载文件、处理数据等。所以我们把进度条的更新逻辑分离出来,做成一个可以接收“当前进度”参数的函数。

process.h(头文件声明)

#pragmaonce#include<stdio.h>voidprocess();voidFlushprocess(doubletotal,doublecurrent);

这里我们声明了两个函数:原有的process用于测试,新函数Flushprocess用于接收总任务量和当前进度,来动态刷新进度条。

process.c(实现)

#include"process.h"#include<string.h>#include<unistd.h>#defineNUM101#defineSTYLE'='voidFlushprocess(doubletotal,doublecurrent){charbar[NUM];memset(bar,0,sizeof(bar));constchar*label="|/-\\";intlen=strlen(label);staticintcount=0;// 这里把 count 声明为静态变量,用来控制旋转动画// 计算已经填充的格子数:current / total * 100 取整intnum=(int)(current*100/total);// 填充 barfor(inti=0;i<num;i++){bar[i]=STYLE;}// 百分比doublerate=current/total;// 旋转动画的字符索引count%=len;// 确保不越界printf("[%-100s][%.1f%%][%c]\r",bar,rate*100,label[count]);count++;fflush(stdout);}

几个关键改动:

  • 函数参数totalcurrent分别表示任务总量和当前完成量,单位可以根据实际场景定义,这里我们内部统一用double保持通用性。
  • static int count:在Flushprocess被多次调用时,count变量不会每次都被重新初始化为 0,而是保留上一次的值。这样旋转动画才能连贯地播放下去。如果不加static,每次调用count都会从 0 开始,动画就跳来跳去。
  • 进度计算:num = (int)(current * 100 / total);算出当前进度占 100 格中的几格;rate = current / total算出百分比小数。
  • 输出格式[%.1f%%]%.1f保留一位小数,%%printf中用来输出一个百分号本身。

4.3 模拟下载和上传任务

现在Flushprocess准备好了,我们需要写一个“任务函数”来模拟实际工作,并不断调用Flushprocess来更新进度条。

main.c(使用回调函数解耦任务与进度显示)

#include"process.h"#include<unistd.h>#include<stdio.h>// 定义一个回调函数类型:接收总量和当前量,无返回值typedefvoid(*callback_t)(doubletotal,doublecurrent);doubletotal=1024.0;// 模拟总数据量 1024MBdoublespeed=1.0;// 每次循环增加 1MBvoidDownload(callback_tcb){doublecurrent=0.0;while(current<=total){cb(total,current);// 调用回调更新进度条usleep(3000);// 模拟网络延迟current+=speed;// 数据量增加}printf("\ndownload %.2lfMB Done\n",current);}voidUpload(callback_tcb){doublecurrent=0.0;while(current<=total){cb(total,current);usleep(3000);current+=speed;}printf("\nupload %.2lfMB Done\n",current);}intmain(){Download(Flushprocess);// 把 Flushprocess 作为回调传入Upload(Flushprocess);return0;}

设计思路解析:

  • 回调函数callback_t是一个函数指针类型,指向一个形如void func(double, double)的函数。DownloadUpload函数接受这样一个函数指针cb,在循环体内调用cb(total, current)来通知“外界”当前进度是多少。至于这个“外界”怎么处理进度(比如打印进度条、记录日志、还是显示在图形界面),完全由调用者传入的具体函数决定。
  • 模拟任务total = 1024.0speed = 1.0,每次循环current += speed,所以从 0MB 跑到 1024MB 需要 1024 次循环。usleep(3000)让每次循环暂停 3 毫秒,整个模拟过程约 3 秒,肉眼能清晰看到进度条的变化。
  • main函数:直接调用Download(Flushprocess),就把进度条显示函数“注入”到了下载任务中。上传任务同理。如果想换一种进度显示方式(比如只打印百分比),只需换一个回调函数即可,完全不用修改DownloadUpload的代码。

4.4 工程化管理:编写 Makefile

当项目包含多个源文件时,每次手动用gcc编译会很麻烦。我们写一个简单的 Makefile 来自动化构建。

SRC = $(wildcard *.c) # 获取当前目录所有 .c 文件 OBJ = $(SRC:%.c=%.o) # 将 .c 后缀替换成 .o BIN = processbar # 最终生成的可执行文件名 $(BIN): $(OBJ) gcc -o $@ $^ # 链接所有 .o 文件生成可执行程序 %.o: %.c gcc -c $< # 编译每个 .c 为 .o .PHONY: clean clean: rm -f $(BIN) $(OBJ) # 清理编译生成的文件

Makefile 关键点:

  • wildcard函数:自动匹配通配符,获取文件列表。
  • $(SRC:%.c=%.o):模式替换,把main.cprocess.c变成main.oprocess.o
  • $@代表目标文件,$^代表所有依赖文件,$<代表第一个依赖文件。这些是 Makefile 的自动变量,可以简化书写。
  • .PHONY: clean:声明clean是一个伪目标,即使当前目录存在一个名为clean的文件,也依然执行 clean 命令。

使用方法:在终端进入项目目录,执行make即可编译生成processbar。执行./processbar运行程序。想清理的话,make clean


五、完整代码汇总

process.h

#pragmaonce#include<stdio.h>voidprocess();voidFlushprocess(doubletotal,doublecurrent);

process.c

#include"process.h"#include<string.h>#include<unistd.h>#defineNUM101#defineSTYLE'='voidFlushprocess(doubletotal,doublecurrent){charbar[NUM];memset(bar,0,sizeof(bar));constchar*label="|/-\\";intlen=strlen(label);staticintcount=0;intnum=(int)(current*100/total);for(inti=0;i<num;i++){bar[i]=STYLE;}doublerate=current/total;count%=len;printf("[%-100s][%.1f%%][%c]\r",bar,rate*100,label[count]);count++;fflush(stdout);}voidprocess(){charbar[NUM];memset(bar,0,sizeof(bar));constchar*label="|/-\\";intlen=strlen(label);intcount=0;while(count<=100){printf("[%-100s][%d%%][%c]\r",bar,count,label[count%len]);fflush(stdout);bar[count]=STYLE;usleep(30000);count++;}printf("\ndownload successfully!\n");}

main.c

#include"process.h"#include<unistd.h>#include<stdio.h>typedefvoid(*callback_t)(doubletotal,doublecurrent);doubletotal=1024.0;doublespeed=1.0;voidDownload(callback_tcb){doublecurrent=0.0;while(current<=total){cb(total,current);usleep(3000);current+=speed;}printf("\ndownload %.2lfMB Done\n",current);}voidUpload(callback_tcb){doublecurrent=0.0;while(current<=total){cb(total,current);usleep(3000);current+=speed;}printf("\nupload %.2lfMB Done\n",current);}intmain(){Download(Flushprocess);Upload(Flushprocess);return0;}

Makefile

SRC = $(wildcard *.c) OBJ = $(SRC:%.c=%.o) BIN = processbar $(BIN): $(OBJ) gcc -o $@ $^ %.o: %.c gcc -c $< .PHONY: clean clean: rm -f $(BIN) $(OBJ)

结语:

今天的内容到这里就结束了,希望你能有所收获~

干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _