【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)

结语:

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

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

相关推荐
MetrixAeroCore1 小时前
出海餐厅手持POS机物联网卡:多币种计费稳定传输与跨境税务合规对接方案
运维
路由侠内网穿透.1 小时前
本地部署开源日志收集系统 Log Bull 并实现外部访问
运维·服务器·网络·数据库·开源
时空无限1 小时前
linux mellanox 网卡队列 irq rps 相关
linux·运维
AlbertS2 小时前
Let‘s Encrypt 证书自动续期并自动应用到Nginx
运维·nginx·ssl·cerbot·renew·续期
kdxiaojie2 小时前
Linux 驱动研究 —— V4L2 (14)
linux·运维·笔记·学习
IMPYLH2 小时前
HTML 的 <dl> 元素
linux·windows·html
如若1232 小时前
Ubuntu 无 sudo 安装花生壳并实现 SSH 内网穿透:Conda 环境部署、冲突排查与自动启动
linux·运维·ubuntu·ssh·内网穿透
承渊政道2 小时前
Linux系统学习【掌握Ext系列⽂件系统的相关内容】
linux·运维·学习·文件系统·存储结构·inode
未知违规用户2 小时前
大模型项目: 学习FastAPI 服务器开发
服务器·人工智能·python·学习·fastapi