【Linux系统编程】——【从0构建第一个Linux系统-进度条】从0到1分阶段构建动态进度条


💯枫亭湖区: 个人主页

🥰个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》

🌠 有善始者实繁,能克终者盖寡

索引与导读

前言

Linux 系统编程领域,进度条的实现不仅仅是简单的字符串拼接,而是对标准输出的缓冲机制与终端控制序列的深度解构 本篇将从底层逻辑出发,带你拆解这一交互组件的实现原理:从利用行首回车(\r)实现原地覆盖,到通过 flush 手动强制刷新行缓冲,再至利用标准输入输出流的原子性控制实现平滑更新 我们将通过分阶段构建的方式,由浅入深,不仅实现稳定的更新动效,更会深入探讨其在多进程/多线程任务监控中的工程化应用,旨在助你构建出既符合 Linux 编程规范、又具备生产环境性能与高可维护性的终端交互工具

一、核心知识必备

1)行缓冲与缓冲区刷新运行机制

C 语言printf函数默认是行缓冲 ------这意味着 printf 产生的内容会先存放在内存缓冲区中

  • 规则:缓冲区只有在以下三种情况发生时,才会把内容"倒"进终端:

    1. 遇到换行符 \n:这意味着一行写完了,可以刷新输出

    2. 缓冲区满了:没地方存了,被迫刷新

    3. 程序结束:程序要退出了,必须把缓存里的内容清理干净


错误示例:

cpp 复制代码
#include <stdio.h>
int main() 
{
    printf("hello Lotso!");
    sleep(3);
    return 0;
}

修正:

cpp 复制代码
#include <stdio.h>
int main() 
{
    printf("hello bite!");
    fflush(stdout);
    sleep(3);
    return 0;
}

2)转义字符 \r (回车) 与 \n (换行) 的区别

  • 换行(\n :光标移动到下一行的行首,但不会回到当前行开头;我们日常使用它的时候其实是回车 + 换行的作用(\r\n

  • 回车(\r:光标回到当前行的行首,但不会移动到下一行

  • 进度条的核心是在同一行反复覆盖刷新 ,因此必须用 \r 让光标回到行首,再重新打印新的进度信息


1)实验一:对比 \n 和 \r 的效果

你可以复制这两段代码到 Linux 中运行,观察结果

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

int main() {
    for (int i = 0; i <= 5; i++) {
        printf("Current count: %d\n", i); // 注意这里用的是 \n
        sleep(1);
    }
    return 0;
}
  • 运行结果:你会看到数字一行接一行地往下打印,屏幕被占满了
plaintext 复制代码
Current count: 0
Current count: 1
Current count: 2
...

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

int main() {
    for (int i = 0; i <= 5; i++) {
        printf("\rCurrent count: %d", i); // 注意这里用的是 \r
        fflush(stdout);                   // 强制刷新缓冲区,否则看不到动效
        sleep(1);
    }
    printf("\n"); // 循环结束后补一个换行,避免后续的命令行提示符接在后面
    return 0;
}
  • 运行结果 :数字 05 会在同一个位置跳动,屏幕上永远只显示一行字 这就是"覆盖刷新"

2)实验二:结合进度条的逻辑
c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main() {
    int i = 0;
    char bar[102];
    memset(bar, 0, sizeof(bar)); // 初始化数组

    while (i <= 100) {
        // 打印:[进度条部分] 百分比%
        printf("\r[%-100s] %d%%", bar, i);
        fflush(stdout);

        bar[i] = '#'; // 每次循环给进度条加一个 '#'
        i++;
        usleep(50000); // 暂停 0.05 秒,制造动画感
    }

    printf("\n"); // 结束后换行,保持整洁
    return 0;
}

1. \r:将光标强行拉回每一行的最左侧

2. %-100s :这保证了进度条的"框"始终是 100 个字符宽 无论进度条是 1% 还是 100%,它的左括号 [ 和右括号 ] 始终站在屏幕的同一个位置,不会左右晃动

3. fflush(stdout):如果没有它,程序可能会一次性等到循环结束才给你刷出结果,你就看不见过程了


运行时的动态表现:

程序开始执行后,你会看到屏幕上出现了一个由方括号包围的进度区域:

  • 动画过程# 字符会从左向右以每 0.05 秒一个的速度填充,右侧的数字从 0% 实时增加到 100%
  • 原地覆盖 :因为使用了 \rfflush(stdout),屏幕上不会出现多行文字,进度条始终像在同一个位置"变长"一样

当循环结束后,程序执行 printf("\n"),光标会换行。此时终端屏幕的最后一行会定格为:

Plaintext 复制代码
[####################################################################################################] 100%
user@linux:~$

3)进度条的核心构成元素详解

一个完整的动态进度条通常包含 3 部分:

  • 进度条主体 :用 = 等字符填充,直观显示完成比例

  • 百分比 :显示完成进度(0%~100%

  • 动态光标 :用 | / - \ 循环切换,提示程序正在运行

  • 附加信息:如当前进度/总进度、传输速度,提升实用性


二、实践开发

我们先打造出一个基础框架,后面慢慢优化出`"彩色区分 + 速度显示 + 多场景适配" 的进度条

头文件、实现文件、主函数三部分来实现

1)基础框架(实际应用较少)

1.1)头文件 process.h

头文件主要用来进行函数声明,防止多重包含

c 复制代码
#ifndef _PROCESS_H_
#define _PROCESS_H_

// 回调函数指针:根据当前百分比,返回已下载的字节数
typedef double (*callback_t)(double progress);

// 核心通用进度条接口
void process_ui(double total_size, callback_t cb);

#endif

1.2)源文件 process.c
c 复制代码
#include "process.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>

#define NUM 51          // 进度条主体长度
#define STYLE '#'       // 填充字符

// ANSI 颜色控制宏
#define NONE          "\033[m"
#define BLUE          "\033[0;32;34m"
#define CYAN          "\033[0;36m"
#define LIGHT_GREEN   "\033[1;32m"
#define PURPLE        "\033[0;35m"

// 自动转换速度单位 (B/s, KB/s, MB/s)
void format_speed(double speed_bytes_per_sec, char* speed_buf) {
    if (speed_bytes_per_sec >= 1024 * 1024) {
        sprintf(speed_buf, "%.2f MB/s", speed_bytes_per_sec / (1024 * 1024));
    } else if (speed_bytes_per_sec >= 1024) {
        sprintf(speed_buf, "%.2f KB/s", speed_bytes_per_sec / 1024);
    } else {
        sprintf(speed_buf, "%.2f B/s", speed_bytes_per_sec);
    }
}

void process_ui(double total_size, callback_t cb) {
    char buffer[NUM];
    memset(buffer, '\0', sizeof(buffer));
    const char* label = "|/-\\";
    int len = strlen(label);
    
    struct timeval start_time, current_time;
    gettimeofday(&start_time, NULL); // 记录启动时间

    double rate = 0.0;
    int rotate_cnt = 0;

    while (rate <= 100.0) {
        // 计算流逝时间 (秒)
        gettimeofday(&current_time, NULL);
        double elapsed = (current_time.tv_sec - start_time.tv_sec) + 
                         (current_time.tv_usec - start_time.tv_usec) / 1000000.0;

        // 计算实时速度
        char speed_buf[32] = "0.00 B/s";
        if (elapsed > 0.001) { 
            double current_bytes = cb(rate); // 通过回调获取当前数据量
            format_speed(current_bytes / elapsed, speed_buf);
        }

        // 构造进度条字符串
        int fill_num = (int)(rate / 2); // 100% 对应 50 个 '#'
        for(int i = 0; i < fill_num; i++) buffer[i] = STYLE;

        // 彩色格式化打印,\r 实现原地刷新
        printf(BLUE"["NONE"%-50s"BLUE"]"CYAN"[%6.2f%%]"LIGHT_GREEN"[%s]"PURPLE"[%c]\r"NONE, 
               buffer, rate, speed_buf, label[rotate_cnt % len]);
        fflush(stdout); // 强刷标准输出缓冲区

        usleep(40000); // 模拟耗时 (40ms)
        rate += 1.0;
        rotate_cnt++;
    }
    printf("\n");
}

1.3)测试文件 main.c
c 复制代码
#include "process.h"
#include <stdio.h>

static double g_total_size = 0.0;

// 场景回调函数:计算当前百分比对应的已完成字节数
double scene_cb(double progress) {
    return g_total_size * (progress / 100.0);
}

int main() {
    // 场景 1:5G 大文件下载 (MB/s 级别)
    g_total_size = 512 * 1024 * 1024; // 512 MB
    printf("\n==== [场景 1] 正在下载大文件 ====\n");
    process_ui(g_total_size, scene_cb);

    // 场景 2:IoT 慢速同步 (KB/s 级别)
    g_total_size = 2.5 * 1024 * 1024; // 2.5 MB
    printf("\n==== [场景 2] 正在同步弱网数据 ====\n");
    process_ui(g_total_size, scene_cb);

    return 0;
}

1.4)💡 编译与运行方法
bash 复制代码
gcc main.c process.c -o progress_bar
./progress_bar

2)关于Makefile自动化构建流程的编写

我们在之前讲过 Makefile 的编写策略,不懂的可以看下面这篇文章,文章结尾有工程化的Makefile

🔗Lucy的空间骇客裂缝:make/Makefile自动化流程构建


三、操作实践与效果展示


💻结尾--- 核心连接协议

警告: 🌠🌠正在接入底层技术矩阵。如果你已成功破解学习中的逻辑断层,请执行以下指令序列以同步数据:🌠🌠


【📡】 建立深度链接: 关注本终端。在赛博丛林中深耕底层架构,从原始代码到进阶协议,同步见证每一次系统升级。

【⚡】 能量过载分发: 执行点赞操作。通过高带宽分发,让优质模组在信息流中高亮显示,赋予知识跨维度的传播力。

【💾】 离线缓存核心: 将本页加入收藏。把这些高频实战逻辑存入你的离线存储器,在遭遇系统崩溃或需要离线检索时,实现瞬时读取。

【💬】 协议加密解密:评论区留下你的散列码。分享你曾遭遇的代码冲突或系统漏洞(那些年踩过的坑),通过交互式编译共同绕过技术陷阱。

【🛰️】 信号频率投票: 通过投票发射你的选择。你的每一次点击都在重新定义矩阵的进化方向,决定下一个被全量拆解的技术节点。



相关推荐
.千余2 小时前
【Linux】网络基础2---Socket编程预备
linux·网络·php
曦月合一2 小时前
在CentOS 6.5系统中OpenJDK 1.7升级更新 OpenJDK 1.8,并部署
linux·centos·jdk1.8
小小ken2 小时前
virtualbox中的ubuntu虚拟机登录到桌面后出现屏幕闪烁现象解决办法
linux·运维·ubuntu
UCloud_TShare2 小时前
告警至处置的自动化鸿沟:AI Agent 的破局思路探索
运维·人工智能·自动化
tianyuanwo3 小时前
Linux社区ISO制作底层探秘:从mkisofs到xorriso的全面解析
linux·mkisofs·xorriso
xiaoye-duck3 小时前
《Linux系统编程》Linux基础开发工具 (三):从零实现动态进度条(附回车、换行与缓冲区详解)
linux
cui_ruicheng3 小时前
Linux网络编程(四):UDP Socket基础编程
linux·服务器·网络·udp
用户2367829801683 小时前
Linux more 命令详解:从基础分页到高级文本查看技巧
linux
sunlifenger3 小时前
构筑绿色能源数字底座,风光一体化智慧电站整体解决方案
服务器·网络·能源