Linux:第一个系统程序--进度条

1. 回车与换行的概念

在计算机和 Linux 系统里,回车换行是两个控制字符,用来控制文本里光标怎么移动:

  • 换行(LF,对应字符 \n 意思是:光标换到下一行,但不一定回到行首。Linux 系统里,文本文件的换行就只用这一个字符表示。

  • 回车(CR,对应字符 \r 意思是:光标回到当前行的最左边,但不往下走。

我们在C语言的代码里就见过 \n 表示换行的语句,但实际上,C语言里面的 \n 表示的其实是两个动作:回车 和 换行。所以此 \n 非彼 \n ,一个是C语言中的 \n ,一个是计算机系统中的换行 \n 。

2. 行缓冲区

大家来看这两段代码:

cpp 复制代码
#include <stdio.h>

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

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

这两段代码的唯一的区别就是,有没有 \n 这个符号,而在C语言中, \n 这个符号代表回车换行,是由回车和换行两个操作组成的。那么这两段代码运行时会有什么效果呢?

第一段代码会直接在显示屏上打印出 hello world 的内容。但是第二段代码会先停顿三秒钟,再将 hello world 的内容打印到屏幕上,这是为什么?

首先,因为C语言是严格按照顺序来执行语句的,所以第二段代码中,在 sleep(3) 这个语句执行之前,printf语句肯定已经执行完成了,那为什么显示器上没有直接呈现hello world的内容呢?其实这就和缓冲区以及 \n 换行符有关系。

首先,缓冲区是内存里一块临时存放数据的区域,作用是减少频繁的 I/O 操作,提高效率。程序不会每产生一个字节就立刻输出到屏幕或文件,而是先攒到缓冲区,等满了或满足条件时再一次性刷新出去。

行缓冲区 是缓冲区的一种,它的刷新规则是以换行符 \n 为触发条件:遇到换行时,缓冲区里的内容就会立即输出;没遇到换行时,数据会一直留在缓冲区里,直到缓冲区满、程序结束或手动刷新才显示。在 Linux 中,标准输出(终端打印)默认就是行缓冲模式

那有没有什么方法,让我可以不写 \n ,也同时让我printf的语句立即显示到显示器上呢?当然是有的,我们可以这样写:

首先要介绍一下 stdout, stdout 是程序与显示器之间的数据通道 ,它被抽象为文件,但不是可执行文件。在 Linux 下,stdout(标准输出)默认是行缓冲 :只有遇到换行符 \n、缓冲区满、或者程序正常结束时,才会把缓冲区里的内容输出到屏幕。往 stdout 写数据就是往这个通道的缓冲区里存数据,缓冲区满足条件后,数据才会被送到显示器显示。

一个语句从执行到被显示到屏幕上,是这样一个步骤:

  • 应用层(你的代码)

    • 你调用 printf("hello world"),数据先被写入 C 标准库(libc)stdout 维护的用户态缓冲区
    • 这一步只是在内存里攒数据,还没碰硬件。
  • 标准库层(触发刷新)

    • 当遇到 \n、缓冲区满、程序结束或手动调用 fflush(stdout) 时,标准库会调用 write(1, buf, len) 系统调用。
    • 这一步把用户态缓冲区的数据,交给操作系统内核。
  • 内核层(真正的输出执行者)

    • 内核收到 write 系统调用后,会把数据写入 终端设备(如 /dev/pts/0 的内核缓冲区。
    • 内核再通过设备驱动程序,把数据发送到显示器硬件,最终在屏幕上显示。

所以会出现上面那种现象,是因为 hello world 这个语句被执行后,没有遇到换行符 \n ,就先存到了 stdout 的缓冲区中,等到程序执行结束了,再把 hello world 这个内容输出到屏幕。而像write系统调用等内容,后续我们会一一讲解。

3. 验证缓冲区的一个样例

大家肯定都见过倒计时,9、8、7、6......,像这样的在显示屏上同一位置有规律的闪烁不同的数字。那么之所以能在同一位置闪烁,就是回车符号与缓冲区的作用:

比如这样一段代码,我们就可以实现一个在相同位置变化成不同数字的简易倒计时器。并且如果修改成这样,还可以变成一个有提示信息的倒计时器:

这段代码通过 \r(回车符)将光标移回当前行首,使后续输出从同一行开头覆盖之前的内容,实现视觉上的动态更新;同时因 stdout 默认是行缓冲,仅遇 \n 才自动刷新,而此处使用 \r 不会触发自动刷新,需调用 fflush(stdout) 强制将缓冲区数据立即输出到屏幕,从而保证每次循环的数字都能实时显示,避免内容堆积到程序结束才展示。

此时我们倒计时器的起始数字是 9 ,那如果我想要起始数字变成 10 呢?

这边竟然变成了 10 、90、80、70.....这样去倒计时,到底怎么回事:

其实是因为,从10开始的时候这个字符串里面有10个字符,但是从第二个语句 count is :9 开始,后面的语句都只有9个字符,这就导致第一句写下来的第十个位置的字符无法被覆盖,所以才会一直显示 0 ,在视觉上让我们觉得是 10 、90、80、70.....这样去倒计时的。

所以我们可以这样去修改:

%2d 是 C 语言 printf 家族函数中的格式化输出控制符,具体含义如下:

%d:表示以十进制整数形式输出。

2 表示最小输出宽度为 2 个字符

如果数字本身不足 2 位 (比如 0~9),会在数字左边补空格,让输出占满 2 个字符宽度。

如果数字等于或超过 2 位 (比如 10123),则按数字实际长度输出,不会被截断。

但是%2d是在数字的左边补空格,这就相当于,到时候我们的倒计时计数器会是这样的:

count is : ++10++ count is : ++9++ count is : ++8++

而我们想要的效果是这样的:

count is : ++10++ count is : ++9++ count is : ++8++

这个时候就可以这样修改:

这表示左对齐。

至此我们的程序就能正常运行了:

经过这个小样例,大家就能清楚的感知到:换行符、回车符的作用、缓冲区的真实存在、缓冲区确实存在刷新的操作、显示器是一个字符设备等等知识。

4. 进度条代码的编写

4.1 目标现象

我们想要实现如上图所示的动态变化的一个进度条,反应内容的进度,这就需要缓冲区、换行、回车等等知识点的结合。

4.2 代码实现

4.2.1 第一版进度条

我们先单独创建一个目录 procbar,procbar 就是 progress bar 的缩写,意思是:进度条。然后创建三个文件,分别对应函数的声明定义和编译。

先不着急实现我们的目标,先来测试一下我们创建的文件的关系是否合理以让我们正常编写代码。在process.h头文件中编写了一个Process的函数,这就是我们进度条函数的主体,然后在 process.c 文件中定义,最后在 main.c 文件中调用。

接着我们来写这个procbar目录的makefile文件:

这里的Makefile用的其实还是我们在上一篇文章中讲到的模板,只是把BIN这个目标文件给变换了一下。详情请看这篇文章:https://blog.csdn.net/2502_91842264/article/details/159547300?fromshare=blogdetail&sharetype=blogdetail&sharerId=159547300&sharerefer=PC&sharesource=2502_91842264&sharefrom=from_link

测试后发现我们的文件之间的逻辑关系没问题,Makefile文件的逻辑也没有问题,就可以直接开始编写代码了:

因为我们的进度条实际上是在字符数组中存储内容,结合行缓冲区以及回车符号的作用,以达到视觉上的动态进度的效果。所以我们要先定义一个字符数组,因为我们的进度最多是 100 ,而这个字符数组最后还要存储一位 \0 ,所以我们一共需要101个位置,为了提高这份代码的可修改性,我们直接使用宏定义。然后再用 memset 函数去初始化定义processbuff这个数组,让其内部充满 \0 这个数据。

接着我们写入一个while循环,这个目的是让processbuff这个数组按设定时间间隔去不断添加一个我们想要的字符 = ,这里也用宏定义以方便后续的修改,然后将其打印出来。并且要控制在原位置上依次添加,所以用了 \r 回车符,但因为没有 \n 就不能立即刷新缓冲区,就使用 fflush 函数,这里我们还学习了一个新的函数 usleep ,因为我们知道,sleep这个函数的时间单位是秒,并且只能填写整数秒,我现在在编写代码期间想要快速观察到我程序的结果,还要等待一百秒,时间太长了。而usleep函数是微秒级睡眠,也就是它的单位是微妙,比如我想要控制每 0.5 秒就打印一个 = ,那我就写成 usleep(500000),因为微秒和秒的转换进制是:

1 秒 = 1000 毫秒 1 毫秒 = 1000 微秒

1 秒 = 1,000,000 微秒(100 万)

至此我们的进度条初步的打印出来了。但是现在依然有一个问题就是:我们的进度条两边有中括号,最右边的中括号会随着括号内=的变多而不断改变位置,可是我们的目的是想让右边的中括号一直保持在最右边的位置不动,只增加括号内=的数量。

所以我们可以先预留出100个位置的空间,让 ] 这个符号直接在末尾位置,并且让我们打印 = 的时候执行左对齐:

这样就没问题了。为了让我们的进度条的进度更加直观一些,可以做出以下调整:

这里我们之所以输入两个% ,是因为在C语言中,单个 % 的含义编译器会把 % 当成格式符的开始,期待后面跟着 d/s/c 等类型字符。如果你只写一个 %,编译器会认为你格式符不完整 ,直接报错或行为异常。而 %% 的含义 %%printf 里专门用来表示字面量 % 的转义写法,最终输出时会变成一个 %

但是有的时候,可能进度条会卡住,也可能是进度太慢了,那么我该如何去判断呢?所以就需要一个可以参考的对象,那么我们就可以在进度值后面加一个符号变化,来代替参考:

至此,这个进度条的基础工作就做完了。

4.2.2 第二版进度条

我们上面写的这个进度条还完全不能投入到正常的工程当中去进行使用,因为这仅仅只是一个我们所写的程序,按部就班的按照我们设定的时间间隔机械的打印出来了一串视觉效果上像进度条的东西而已。而真正的进度条,一定是附属于某一程序,按照实际情况进行的东西。

所以现在我们来写第二版进度条:

这是在 main.c 编译文件中的代码:

这是在 process.c 文件中实现函数定义的代码:

在这个过程当中:单次循环就是:模拟下载 1 单位数据 → 计算当前进度百分比 → 生成对应长度的 = 进度条 → 在终端同一行刷新显示进度和百分比 → 暂停模拟网络延迟 ,重复直到下载完成**。** 视觉上终端里会显示一个从空到满逐渐变长的 = 进度条,同时百分比从 0.0% 增长到 100.0%

现在还差一个旋转光标的问题。因为大家可以看到函数里面的cnt实际上是取了rate的整数部分,并且只有当cnt的数值从一个整数增长 1 变为另一个整数的时候,我们的进度条才会多打出一个 = ,但实际上cnt有时可能就增长零点几,但是视觉上我们会觉得这个进度条没有动静。所以为了验证进度条到底是完全卡死了,还是虽然进度有在增长,但是由于增长幅度太小导致我们从视觉上没有观察到进度条在增长,我们需要旋转光标。

并且从上面的话我们可以得知,旋转光标的输出次数只和FlushProcess这个函数的调用次数有关系。

所以我们添加上光标的输出,现在进度条就初具雏形了。

不过至此,我们还是基于一个特定条件下的进度条,因为我们一开始设置的下载总量还有下载速度都是固定死的,可真实情况下,网络是会有浮动的,也就意味着下载速度肯定不是一直不变的。

在这里我们使用 time.h 这个头文件中的函数,rand和srand搭配使用,生成随机数字,来模拟我们真实情况中的网络波动造成的网速变化。这个rand()%int_range就是要生成从0到int_range-1这个范围内的任意一个数字。

不过随即下载速度的问题是解决了,可是我们执行程序的时候却发现,我们的进度条到不了 100%。这其实是我们 main.c 这个文件中的while语句的判断条件写的有问题,因为之前我们的speed是一直按照 1 的速度去执行的,所以必然能累加到一百。但是现在因为speed是随机的了,所以有可能当前是93.5,下一次SpeedFloat(speed,100.3)这个语句执行出来的结果是 50 ,那原来的 cur 是93.5,加了 50 之后变成143.5就超过100了,就不满足curr<=total的语句了。

所以我们在DownLoad函数内加一个判断语句即可。

至此我们就可以实现不同下载容量的大小,去展示出不同的进度条:

但是这段代码实际上还有一个缺陷,从解耦的角度来讲,这份代码并不够好。因为DownLoad函数里面还有FlushProcess函数内置,这就导致耦合度会偏高,因为我们要使用回调函数的方法:

至此,我们的进度条就成功实现了。下面展示一下全部代码:

cpp 复制代码
 'main.c'
  1#include"process.h"                                                                                                                                                                              
  2 #include <time.h>
  3 #include <stdlib.h>
  4 
  5 //场景是:要下载一个文件
  6 double total = 1024.0;//要下载的内容总量
  7 double speed = 1.0; //下载的网速
  8 
  9 //函数指针类型
 10 typedef void (*callback_t)(double,double);
 11 
 12 double SpeedFloat(double start,double range)
 13 {
 14   int int_range = (int)range;
 15   return start + rand()%int_range + (range - int_range);
 16 }
 17 
 18 void DownLoad(int total,callback_t cb)
 19 {
 20   srand((unsigned int)time(NULL));
 21   double curr = 0.0;  //已经下载的数量
 22   while(curr <= total)
 23   {
 24     cb(total,curr); //更新进度
 25 
 26     curr += SpeedFloat(speed,100.3);   //模拟下载的行为
 27 
 28     if(curr > total)
 29     {
 30       curr = total;
 31       cb(total,curr); //更新进度
 32       break;
 33     }
 34     usleep(30000);  //模拟下载的延迟
 35   }
 36 }
 37 
 38 int main()
 39 {
 40   printf("download is: 20 MB\n");
 41   DownLoad(20,FlushProcess);
 42   printf("download is: 2000 MB\n");
 43   DownLoad(2000,FlushProcess);
 44   printf("download is: 1024 MB\n");
 45   DownLoad(1024,FlushProcess);
 46   printf("download is: 89 MB\n");
 47   DownLoad(89,FlushProcess);
 48 
 49   return 0;
 50 } 
cpp 复制代码
'process.c'
 1 #include"process.h"
  2 #include<string.h>
  3 #include<unistd.h>
  4 
  5 #define SIZE 101
  6 #define STYLE '='
  7 
  8 
  9 void FlushProcess(double total,double curr)
 10 {
 11   if(curr > total)
 12     curr = total;
 13   double rate = curr / total * 100;//乘以100是为了转化成百分制
 14   int cnt = (int)rate;//取rate的整数部分
 15   char processbuff[SIZE];
 16   memset(processbuff,'\0',sizeof(processbuff));
 17   int i = 0;
 18   for(; i< cnt; i++)
 19   {
 20     processbuff[i] = STYLE;
 21   }
 22 
 23   static const char* lable = "|/-\\";
 24   static int index = 0;
 25 
 26   //进行刷新操作
 27   printf("[%-100s] [%.1lf%%] [%c]\r",processbuff,rate,lable[index++]);
 28   index %= strlen(lable);
 29   fflush(stdout);
 30   if(curr >= total)
 31   {
 32     printf("\n");
 33   }
 34 }
 35 
 36 //void Process()                                                                                                                                                                                 
 37 //{
 38 //  char processbuff[SIZE];
 39 //  memset(processbuff,'\0',sizeof(processbuff));
 40 //  int cnt = 0;
 41 //  const char* lable = "|/-\\";
 42 //  int len = strlen(lable);
 43 //
 44 //  while(cnt <= 100)
 45 //  {
 46 //    printf("[%-100s] [%d%%] [%c]\r",processbuff,cnt,lable[cnt%len]);
 47 //    fflush(stdout);
 48 //    processbuff[cnt++] = STYLE;
 49 //    usleep(30000);
 50 //  }
 51 //  printf("\n");

本文到此结束,感谢各位读者的阅读。如果有讲解的不到位或者错误的地方,欢迎各位读者进行批评或指正。

相关推荐
克莱因3582 小时前
Linux 进程(2)服务管理指令
java·linux·服务器
不怕犯错,就怕不做2 小时前
Linux中的IS_ENABLED 函数实战使用demo
linux·驱动开发·嵌入式硬件
源远流长jerry2 小时前
软件定义网络 SDN 核心技术深度解析:从概念到实践
linux·网络·架构
橙露2 小时前
Linux 服务器性能排查:CPU / 内存 / 磁盘 / 网络一键定位
linux·服务器·网络
李子焱2 小时前
第一节:初识n8n与下一代工作流自动化
运维·自动化
暴力求解2 小时前
Linux---命名管道与共享内存(一)
linux·运维·服务器
小鸡食米2 小时前
Linux 防火墙
linux·运维·服务器
bingHHB2 小时前
聚水潭 × 金蝶云星空:日均万单电商如何实现销售出库自动记账
运维·自动化·集成学习
ICT系统集成阿祥2 小时前
BGP邻居状态机详解
运维·服务器