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


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


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

文章目录


一、回车与换行

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

1.1 老式打字机

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

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

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

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

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

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

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


二、行缓冲区

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

2.1 三个现象对比

代码一:带 \n 的 printf

c 复制代码
#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("hello Linux!\n");
    sleep(3);
    return 0;
}

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

代码二:没有 \n 的 printf

c 复制代码
#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("hello Linux!");
    sleep(3);
    return 0;
}

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

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

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

c 复制代码
#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("hello Linux!");
    fflush(stdout);   // 强制刷新标准输出缓冲区
    sleep(3);
    return 0;
}

  运行结果:程序一开始就打印出 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 的知识,我们可以先写一个小例子巩固一下------一个在终端原地倒计时的程序。

c 复制代码
#include <stdio.h>
#include <unistd.h>

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

解析:

  • "%-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%,每次刷新一行。

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>

#define NUM 101        // 进度条总格数 + 1 (用于字符串结尾)
#define STYLE '='      // 进度条填充字符

void process()
{
    char bar[NUM];
    memset(bar, 0, sizeof(bar));  // 全部初始化为 '\0'
    
    const char* label = "|/-\\";  // 旋转动画的字符序列
    int len = strlen(label);
    
    int count = 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");
}

int main()
{
    process();
    return 0;
}

代码详解:

  • 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(头文件声明)

c 复制代码
#pragma once
#include <stdio.h>

void process();
void Flushprocess(double total, double current);

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

process.c(实现)

c 复制代码
#include "process.h"
#include <string.h>
#include <unistd.h>

#define NUM 101
#define STYLE '='

void Flushprocess(double total, double current)
{
    char bar[NUM];
    memset(bar, 0, sizeof(bar));
    
    const char* label = "|/-\\";
    int len = strlen(label);
    
    static int count = 0;  // 这里把 count 声明为静态变量,用来控制旋转动画
    
    // 计算已经填充的格子数:current / total * 100 取整
    int num = (int)(current * 100 / total);
    
    // 填充 bar
    for(int i = 0; i < num; i++)
    {
        bar[i] = STYLE;
    }
    
    // 百分比
    double rate = 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(使用回调函数解耦任务与进度显示)

c 复制代码
#include "process.h"
#include <unistd.h>
#include <stdio.h>

// 定义一个回调函数类型:接收总量和当前量,无返回值
typedef void (*callback_t)(double total, double current);

double total = 1024.0;   // 模拟总数据量 1024MB
double speed = 1.0;      // 每次循环增加 1MB

void Download(callback_t cb)
{
    double current = 0.0;
    while(current <= total)
    {
        cb(total, current);         // 调用回调更新进度条
        usleep(3000);               // 模拟网络延迟
        current += speed;           // 数据量增加
    }
    printf("\ndownload %.2lfMB Done\n", current);
}

void Upload(callback_t cb)
{
    double current = 0.0;
    while(current <= total)
    {
        cb(total, current);
        usleep(3000);
        current += speed;
    }
    printf("\nupload %.2lfMB Done\n", current);
}

int main()
{
    Download(Flushprocess);   // 把 Flushprocess 作为回调传入
    Upload(Flushprocess);
    return 0;
}

设计思路解析:

  • 回调函数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 来自动化构建。

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

c 复制代码
#pragma once
#include <stdio.h>

void process();
void Flushprocess(double total, double current);

process.c

c 复制代码
#include "process.h"
#include <string.h>
#include <unistd.h>

#define NUM 101
#define STYLE '='

void Flushprocess(double total, double current)
{
    char bar[NUM];
    memset(bar, 0, sizeof(bar));
    
    const char* label = "|/-\\";
    int len = strlen(label);
    
    static int count = 0;
    int num = (int)(current * 100 / total);
    
    for(int i = 0; i < num; i++)
    {
        bar[i] = STYLE;
    }
    
    double rate = current / total;
    count %= len;
    printf("[%-100s][%.1f%%][%c]\r", bar, rate * 100, label[count]);
    count++;
    
    fflush(stdout);
}

void process()
{
    char bar[NUM];
    memset(bar, 0, sizeof(bar));
    
    const char* label = "|/-\\";
    int len = strlen(label);
    
    int count = 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

c 复制代码
#include "process.h"
#include <unistd.h>
#include <stdio.h>

typedef void (*callback_t)(double total, double current);

double total = 1024.0;
double speed = 1.0;

void Download(callback_t cb)
{
    double current = 0.0;
    while(current <= total)
    {
        cb(total, current);
        usleep(3000);
        current += speed;
    }
    printf("\ndownload %.2lfMB Done\n", current);
}

void Upload(callback_t cb)
{
    double current = 0.0;
    while(current <= total)
    {
        cb(total, current);
        usleep(3000);
        current += speed;
    }
    printf("\nupload %.2lfMB Done\n", current);
}

int main()
{
    Download(Flushprocess);
    Upload(Flushprocess);
    return 0;
}

Makefile

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)

结语:

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

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

相关推荐
叮咚侠8 小时前
Ubuntu 24.04 安装zabbix-agent 5.0 服务
linux·ubuntu·zabbix·zabbix-agent
故乡dee云8 小时前
华为云国际服务器连不上怎么办?安全组、端口、IP 和网络排查方法
服务器·网络·华为云
佳児素花痴╮8 小时前
服务器模型、网络调试工具、网络协议头分析
服务器·网络·网络协议
左手厨刀右手茼蒿10 小时前
数据仓库的架构演进:从MySQL到ClickHouse到数据湖的工程化实践
linux·嵌入式·系统内核
左手厨刀右手茼蒿10 小时前
创业团队技术选型:别用大厂架构解决小团队问题
linux·嵌入式·系统内核
明志数科11 小时前
宇树科技IPO背后的产业逻辑:人形机器人从“讲故事“到“交数据“
运维·服务器·数据库
外滩运维专家11 小时前
验证码接口上线三天就被刷了,我后来加的几道防线
运维
Data_Journal11 小时前
什么是 CAPTCHA,它是如何工作的?
java·大数据·服务器·前端·数据库
跨境小彭11 小时前
店群运营实操复盘:批量活动申报自动化优化方案
大数据·运维·人工智能·自动化·跨境电商·temu·temu电商运营
Jlzn888811 小时前
2026年锂电CCS组装线:车规储能双场景兼容自动化方案的技术架构与实践
运维·架构·自动化