【Linux初阶】基础IO - FILE结构体中的缓冲区

news2024/11/18 4:51:24

🌟hello,各位读者大大们你们好呀🌟
🍭🍭系列专栏:【Linux初阶】
✒️✒️本篇内容:库函数和系统函数的刷新差异,缓冲区的意义、刷新策略、存在位置、文件写入的逻辑,缓冲区的代码实现,缓冲区和OS的关系
🚢🚢作者简介:计算机海洋的新进船长一枚,请多多指教( •̀֊•́ ) ̖́-


文章目录

  • 一、为什么我的库函数输出两次,系统函数只输出一次?
  • 二、理解缓冲区问题
    • 1.缓冲区存在的意义
    • 2.缓冲区的刷新策略
    • 3.缓冲区在哪里?
    • 4.文件写入的底层逻辑(1)
    • 5.回答库函数输出两次,系统函数输出一次的问题
  • 二、缓冲区的封装实现
    • 1.myStdio.h
    • 2.myStdio.c
    • 3.main.c
    • 4.Makefile
  • 三、理解缓冲区和OS的关系
    • 1.文件写入的底层逻辑(2)
    • 2.OS宕机对缓冲区的影响
  • 结语


一、为什么我的库函数输出两次,系统函数只输出一次?

在正式学习缓冲区知识前,我们用一个代码问题作为引入

#include <stdio.h>
#include <string.h>
int main()
{
	const char* msg0 = "hello printf\n";
	const char* msg1 = "hello fwrite\n";
	const char* msg2 = "hello write\n";
	printf("%s", msg0);
	fwrite(msg1, strlen(msg0), 1, stdout);
	write(1, msg2, strlen(msg2));
	fork();
	return 0;
}

运行出结果:

hello printf
hello fwrite
hello write

但如果对进程实现输出重定向呢?./hello > file, 我们发现结果变成了:

hello write
hello printf
hello fwrite
hello printf
hello fwrite

我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。

假如我们将 fork()代码注释掉,再运行上面的重定向指令,我们可以发现它会正常将 write、printf、fwrite三个信息重定向到 file文件中,因此我们可以推断出问题肯定和fork有关!

要理解上面问题出现的原因,就要涉及我们接下来要学习的缓冲区的知识了。

———— 我是一条知识分割线 ————

二、理解缓冲区问题

1.缓冲区存在的意义

在这里我先给出一条结论:缓冲区实际上就是一段内存

那么缓冲区是什么?谁提供的?为什么要存在呢?

内存的进程要将数据写入到磁盘的文件,这个动作的实现,首先需要在内存中开辟一块缓冲区,进程将数据拷贝到我们的缓冲区中,然后进程对应的拷贝函数就直接返回了,返回之后,进程就可以继续向后执行自己的代码了。

在进程执行代码期间,缓冲区中的数据会定期的发送到磁盘的文件中。
在这里插入图片描述

为什么进程不直接自己将数据写入到文件中呢?这是因为对于进程来说太消耗时间了,我们知道,进程写入磁盘文件的这个动作属于IO,涉及到外设,因此相对比较慢,如果进程自己执行这个动作,效率会很低。

至此,我们就上面的三个问题先做一个回答:

  • 缓冲区实际上就是一段内存
  • 缓冲区是用户级语言层面(语言库)给我们提供的(后面会将讲解);
  • 缓冲区的意义是什么呢?节省进程进行数据 IO的时间

那么问题又来了:你说我将信息拷贝给了缓冲区,我哪里有拷贝呢?

以 fwrite函数为例,与其我们将 fwrite理解为写入文件的函数,倒不如将 fwrite理解为拷贝函数!将数据从进程,拷贝到缓冲区或外设中。

———— 我是一条知识分割线 ————

2.缓冲区的刷新策略

假设我们有一块数据,我们是应该一次性写入到外设 还是 少量多次的写入到外设呢?

实际上,一次性写入到外设是效率最高的。在我们 IO的过程中,最耗费时间的并不是数据的拷贝时间,以 1秒为例,有可能 990毫秒都在等待外设准备就绪,10毫秒完成拷贝。因此,对于计算机来说,一次性写入效率是最高的。

缓冲区一定会根据具体的设备,定制自己的刷新策略。一般而言,我们有三种策略两种特殊情况

三种策略

  1. 立即刷新 - 无缓冲(不常见);
  2. 行刷新 - 行缓冲(显示器就是使用行缓存);
  3. 缓冲区满 - 全缓冲(磁盘文件就是全缓冲);

以显示器为例,显示器采用行缓冲,因为显示器需要满足人的阅读需求(及时高效反馈),使用行缓存可以在满足人的需求的同时,保证缓冲效率不会太低。

以磁盘文件为例,IO时采用全缓冲,等待磁盘响应的次数最少,可以保障最大的效率。

两种特殊情况

  1. 用户强制刷新;
  2. 进程退出 - 一般都要进行缓冲区刷新;

为什么有些程序在没有强制刷新要求的情况下,数据迟迟不刷新到显示器上?很大可能就是这个程序的进程还没有退出,缓冲区的数据还没有刷新。

———— 我是一条知识分割线 ————

3.缓冲区在哪里?

再次回到我们一开始的那个代码问题的运行结果:

hello write
hello printf
hello fwrite
hello printf
hello fwrite

告知:上面这个结果的出现,一定和缓冲区有关。

那么这个缓冲区是谁提供的呢?

通过对我们一开始的那个代码运行结果的分析,我们可以得出,缓冲区不在内核中,因为如果缓冲区在内核中,write也应该要被打印两次。

结论:我们之前谈论的缓冲区,都是用户语言层面(语言库)给我们提供的缓冲区。C语言的缓冲区是C标准库提供的。

缓冲区在哪里?

在我们进行文件读写过程中,涉及到的调用接口(stdout、stdin、stderr),都是 *FILE 类型的,*FILE会被 FILE结构体管理,在这个结构体中,就包含了 fd(文件描述符) & 一个缓冲区

总结:这个缓冲区,在 stdout、stdin、stderr -> *FILE -> FILE结构体 -> fd & 一个缓冲区

因此,我们强制刷新时,需要使用 fflush(文件指针),关闭文件时,需要 fclose(文件指针)

———— 我是一条知识分割线 ————

4.文件写入的底层逻辑(1)

如果有兴趣,可以看看FILE结构体:

typedef struct _IO_FILE FILE;/usr/include/stdio.h
/ usr / include / libio.h
struct _IO_FILE {
	int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

 //缓冲区相关
 /* The following pointers correspond to the C++ streambuf protocol. */
 /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
	char* _IO_read_ptr; /* Current read pointer */
	char* _IO_read_end; /* End of get area. */
	char* _IO_read_base; /* Start of putback+get area. */
	char* _IO_write_base; /* Start of put area. */
	char* _IO_write_ptr; /* Current put pointer. */
	char* _IO_write_end; /* End of put area. */
	char* _IO_buf_base; /* Start of reserve area. */
	char* _IO_buf_end; /* End of reserve area. */
	/* The following fields are used to support backing up and undo. */
	char* _IO_save_base; /* Pointer to start of non-current get area. */
	char* _IO_backup_base; /* Pointer to first valid character of backup area */
	char* _IO_save_end; /* Pointer to end of non-current get area. */

	struct _IO_marker* _markers;
	struct _IO_FILE* _chain;

	int _fileno; //封装的文件描述符

#if 0
	int _blksize;

#else
	int _flags2;

#endif
	_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
 /* 1+column number of pbase(); 0 is unknown. */
	unsigned short _cur_column;
	signed char _vtable_offset;
	char _shortbuf[1];

	/* char* _save_gptr; char* _save_egptr; */
	_IO_lock_t* _lock;

#ifdef _IO_USE_OLD_IO_FILE
};

通过观察,我们知道了 FILE* 指向地结构体内部包含缓冲区相关的读写接口 和 封装的文件描述符。

就此我们可以得出文件写入的底层逻辑:fprintf、fwrite等接口的数据,都会被写入我们自己写的 FILE* 类型指针所指向结构体内部的缓冲区中,由于结构体内部封装了 fd(文件描述符),所以我们的C语言(其他语言以此类推)会自动地帮我们在合适的时间将我们地数据刷新到外设里。

其中,文件的写入真的那么简单吗?实际上文件写入中间还有一些细节,我们会在 三、理解缓冲区和OS的关系 中进行进一步的挖掘。

———— 我是一条知识分割线 ————

5.回答库函数输出两次,系统函数输出一次的问题

通过对上面知识的理解,现在我们终于可以理解为什么会出现第一章中的代码问题了。

  1. 回到我们一开始的代码问题,stdout默认使用的是行刷新,在我们的代码 fork之前,由于 \n的存在,3条函数已经将数据都打印到显示器上(外设)了,此时我们的 FILE结构体内部,已经不存在对应的数据了。

  2. 此时如果我们再进行 >,写入的文件就不是显示器,而是普通文件,采用的是全缓冲策略,之前的3条函数虽然都带有 \n,但是不足以写满缓冲区,最终的数据并没有被我们刷新!

  3. 执行 fork的时候,stdout属于父进程,创建子进程后,紧接着就是退出。符合第二种进程缓冲的特殊情况:进程退出。最后,无论父子进程谁先退出,都需要刷新缓冲区,而缓冲的本质就是修改。因此会发生写时拷贝!!数据会显示两份。

  4. write为什么没有显示呢?因为上面的过程与 write无关,write为系统调用,没有 FILE结构体,而是直接使用 fd(文件描述符),没有C提供的缓冲区

#include <stdio.h>
#include <string.h>
int main()
{
	const char* msg0 = "hello printf\n";
	const char* msg1 = "hello fwrite\n";
	const char* msg2 = "hello write\n";
	
	//C接口
	printf("%s", msg0);
	fwrite(msg1, strlen(msg0), 1, stdout);
	
	//系统接口
	write(1, msg2, strlen(msg2));
	
	fork();
	return 0;
}
hello write
hello printf
hello fwrite
hello printf
hello fwrite

二、缓冲区的封装实现

创建 myStdio.h、myStdio.c、main.c、Makefile四个文件

1.myStdio.h

#include <assert.h>
#include <stdlib.h> //malloc申请堆空间
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define SIZE 1024 //缓冲区默认大小。OS实际上是动态申请的
#define SYNC_NOW    1 //立即同步刷新
#define SYNC_LINE   2 //行刷新
#define SYNC_FULL   4 //全刷新

typedef struct _FILE {
    int flags; //刷新方式
    int fileno; //文件描述符
    int cap; //buffer的总容量
    int size; //buffer当前的使用量
    char buffer[SIZE];
}FILE_;


FILE_* fopen_(const char* path_name, const char* mode);
void fwrite_(const void* ptr, int num, FILE_* fp); //写入源文件不可被修改,加const
void fclose_(FILE_* fp);
void fflush_(FILE_* fp);

2.myStdio.c

#include "myStdio.h"

FILE_* fopen_(const char* path_name, const char* mode)
{
    int flags = 0; //默认标记位
    int defaultMode = 0666; //默认起始权限

    if (strcmp(mode, "r") == 0) //字符串比较,根据不同的情况设置标志位
    {
        flags |= O_RDONLY;
    }
    else if (strcmp(mode, "w") == 0)
    {
        flags |= (O_WRONLY | O_CREAT | O_TRUNC);
    }
    else if (strcmp(mode, "a") == 0)
    {
        flags |= (O_WRONLY | O_CREAT | O_APPEND);
    }
    else
    {
        //TODO
    }
    int fd = 0;

    if (flags & O_RDONLY) fd = open(path_name, flags); //打开文件
    else fd = open(path_name, flags, defaultMode);

    if (fd < 0) //打开失败,向屏幕打印原因
    {
        const char* err = strerror(errno); //获取错误信息
        write(2, err, strlen(err));
        return NULL; // 打开文件失败会返回NULL
    }
    FILE_* fp = (FILE_*)malloc(sizeof(FILE_)); //申请堆空间
    assert(fp); //检查

    fp->flags = SYNC_LINE; //默认设置成为行刷新,这里的flags和上面open的flags不同
    fp->fileno = fd;
    fp->cap = SIZE;
    fp->size = 0;
    memset(fp->buffer, 0, SIZE); //将缓冲区清0

    return fp; // 这就是为什么你们打开一个文件,就会返回一个FILE *指针
}

void fwrite_(const void* ptr, int num, FILE_* fp)
{
    // 1. 写入到缓冲区中 - 这里我们不考虑缓冲区溢出的问题
    memcpy(fp->buffer + fp->size, ptr, num); //拷贝,拷贝目标、拷贝源、拷贝的字节数
    fp->size += num;

    // 2. 判断是否刷新
    if (fp->flags & SYNC_NOW) //判断刷新策略
    {
        write(fp->fileno, fp->buffer, fp->size);
        fp->size = 0; //清空缓冲区
    }
    else if (fp->flags & SYNC_FULL)
    {
        if (fp->size == fp->cap) //判断缓冲区满没满
        {
            write(fp->fileno, fp->buffer, fp->size);
            fp->size = 0;
        }
    }
    else if (fp->flags & SYNC_LINE)
    {
        if (fp->buffer[fp->size - 1] == '\n') // 有\n,则行刷新,abcd\nefg , 不考虑
        {
            write(fp->fileno, fp->buffer, fp->size);
            fp->size = 0;
        }
    }
    else {

    }
}

void fflush_(FILE_* fp)
{
    if (fp->size > 0) write(fp->fileno, fp->buffer, fp->size); //文件,文件内容,文件大小
    fsync(fp->fileno); //将数据,强制要求OS进行外设刷新!
    fp->size = 0;
}

void fclose_(FILE_* fp)
{
    fflush_(fp);
    close(fp->fileno);
}

3.main.c

#include "myStdio.h"
#include <stdio.h>

int main()
{
    FILE_* fp = fopen_("./log.txt", "w");
    if (fp == NULL)
    {
        return 1;
    }
    int cnt = 10;
    const char* msg = "hello world "; //1.没有带\n; 2.有带\n;
    while (1)
    {
        fwrite_(msg, strlen(msg), fp);
        //fflush_(fp); //写一条就刷新一条
        
        sleep(1);
        printf("count: %d\n", cnt); //count会在屏幕中打印,文件写入我们是看不到的
        //if(cnt == 5) fflush_(fp); //强制刷新实验
        
        cnt--;
        if (cnt == 0) break; //10秒过后,屏幕中才会打印,这是因为数据在缓冲区中,进程退出才刷新
    }                        // 如果写入的内容带有 \n,会进行行刷新,每秒刷新一次
    fclose_(fp);

    return 0;
}

4.Makefile

main:main.c myStdio.c
    gcc - o $@ $ ^ -std = c99
.PHONY:clean
clean :
    rm - f main

三、理解缓冲区和OS的关系

1.文件写入的底层逻辑(2)

  1. 缓冲区的内容不会直接写入到磁盘,它会将数据先写入到 struct file的内核缓冲区中,再由内核缓冲区刷新到磁盘(外设);
  2. 缓冲区分为语言层面的缓冲区操作系统层面的内核缓冲区
  3. 这个真正将数据从内存刷新到磁盘的动作,由 OS自主决定;
  4. 在操作系统看来,并不是像行、全这样简单的缓冲刷新。总之,操作系统会权衡自己的内存使用情况,来对数据做刷新

在这里插入图片描述

文件是如何刷新到外设的呢?我们以C语言为例,先用C语言的接口,将数据拷贝到C语言的缓冲区里,C语言根据对应的缓冲策略,通过 struct file的接口,将数据拷贝到内核缓冲区中,然后再由操作系统定期刷到外设中

如果将操作系统定期刷到外设也理解为一次拷贝,那么文件刷新到外设的这个动作需要经历三次拷贝!它们分别为:内存->语言层缓冲区->内核缓冲区->外设

2.OS宕机对缓冲区的影响

OS宕机会对缓冲区刷新造成影响,会导致数据丢失。

那假如是一个对数据问题 0容忍的一个机构(银行),遇到这种问题该怎么办呢?我们不是无法决定内核缓冲区对磁盘(外设)的刷新策略吗?

不用担心,操作系统早就为我们想好啦!操作系统给用户层提供了将内核缓冲区的数据强制刷新到磁盘的接口,它就是 fsync。

fsync(fp->fileno); //将数据,强制要求OS进行外设刷新!

相信大家在上面二、缓冲区的封装实现的 fflush代码部分已经看过,这里就不对其应用再做赘述了。


结语

🌹🌹 基础IO - FILE结构体中的缓冲区 的知识大概就讲到这里啦,博主后续会继续更新更多C++ 和 Linux的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/726869.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Unity与iOS交互(1)——需要了解的IOS相关知识

【前言】 以下只是简要介绍&#xff0c;详细的内容需要自己去看链接 【Objective-C基础知识】 .h .m .mm .cpp文件区别 .h是头文件扩展名。头文件包含类&#xff0c;类型&#xff0c;函数和常数的声明&#xff0c;这里的定义一般是Public的 .m是实现文件扩展名。其包含源代…

云环境利用工具-----cf

简介 CF 是一个云环境利用框架&#xff0c;适用于在红队场景中对云上内网进行横向、SRC 场景中对 Access Key 即访问凭证的影响程度进行判定、企业场景中对自己的云上资产进行自检等等。 项目地址&#xff1a;https://github.com/teamssix/cf 使用手册&#xff1a;https://wi…

Java POI excel单元格背景色(填充)、字体颜色(对齐)、边框(颜色)、行高、列宽设置

文章目录 1、Excel Cell单元格背景色颜色名称对照关系2、Excel Cell单元格背景填充样式颜色填充对照关系3、Excel Cell字体样式设置对照图4、Excel 行高、列宽设置5、Excel单元格边框设置边框类型图片对比附一&#xff1a;一些问题1、关于列宽使用磅*20的计算方式2、关于行高使…

常用数据预处理与特征选择方法总结记录

在很多机器学习或者是深度学习建模之前&#xff0c;对于数据的处理尤为重要&#xff0c;选择合适的数据预处理方法和特征构建算法对于后续模型的结果有着很大的影响作用&#xff0c;这里的主要目的就是想记录总结汇总常用到的处理方法&#xff0c;学习备忘&#xff01; 数据清洗…

Docker集群部署-MySQL主从复制

实验目的 利用Docker实现MySQL主从复制架构的部署&#xff0c;实现1主1从集群配置。 实验准备 要求实验主机能够连接外网&#xff0c;已经正确安装Docker&#xff0c;并关闭防火墙和selinux。 【实验步骤】 新建主服务器容器实例3307 # docker run -p 3307:3306 --name my…

mysql —案例复杂查询+索引使用+DBeaver中创建索引

前言 接上章 我们 对一个简单的选课功能进行 设计分析 实际上在工作中 拿到一个需求&#xff0c;也是这样的一个分析过程 一个份 需求文档原型 出来&#xff0c;只要是你负责这个模块&#xff0c;就需要你自己建表建库&#xff0c;设计接口文档&#xff0c;也许现在有的公司…

AIGC|FineTune工程之LoRa高效参数微调

徐辉 | 后端开发工程师 一、引言 随着深度学习和自然语言处理技术的快速发展&#xff0c;大型预训练语言模型&#xff08;如GPT、Vicuna、Alpaca、Llama、ChatGLM等&#xff09;在各种应用场景中取得了显著的成果。然而&#xff0c;从零开始训练这些模型需要大量的计算资源和…

Cristiano Linux学习小结

一、linux基础 1.1 基本概述 1、Linux组成&#xff1a;内核 Shell 文件系统 应用程序 2、内核&#xff08;Kernel&#xff09;&#xff0c;即核心程序。实现操作系统的基本功能(进程管理、内存管理、进程间通信、虚拟文件系统和网络接口)&#xff0c;决定着系统的性能和稳定性。…

递归典型例题:汉诺塔问题

文章目录 1. 什么是汉诺塔2. 汉诺塔的解题步骤3. 代码实现汉诺塔 1. 什么是汉诺塔 1. 汉诺塔的来源 一位法国数学家曾编写过一个印度的古老传说&#xff1a;在世界中心拿勒斯的圣庙里边&#xff0c;有一块黄铜板&#xff0c;上边插着三根宝柱。印度教的主神梵天在创造世界的时…

div绑定键盘点击事件

为箭头绑定绑定键盘方向键 <div class"toggle-nav"><spanv-if"leftToggleSt"click"toggleGoods(1)"keyup.left"toggleGoods(1)"class"toggle-left"><a-icon type"left" class"icon" /&…

Linux系统设置

Linux的系统设置 01 选择“Install CentOS7” 02 选择安装界面的语言 03 选择时区&#xff0c;这里选择上海 04 选择安装类型&#xff0c;选择最小安装即可&#xff0c;不需要图形界面与其他的组件 05 选择安装位置&#xff0c;自定义分区 06 我要配置分区&#xff0c;进行…

ARS408毫米波雷达使用记录

参考&#xff1a;ARS_408毫米波雷达数据解析学习记录 感谢博主的分享&#xff08;https://blog.csdn.net/weixin_49401384&#xff09; 雷达can消息解析&#xff08;通用can解析思路&#xff09; socketcan学习can总线 毫米波雷达can协议解读RadarCfgRadarState 代码基本是关于…

Python接口自动化核心模块 - 数据库操作和日志

进行接口测试时&#xff0c;我们需要连接到数据库中&#xff0c;对数据源进行备份、还原、验证等操作。 Python连接数据库常见模块 MysqlDB python2时代最火的驱动库。基于C开发&#xff0c;对windows平台不友好。现在已经进入python3时代&#xff0c;基本不再使用 MysqlCl…

干农活太累了,尤其是对一位22女孩来说

干农活太累了&#xff0c;尤其是对一位22女孩来说。要是能够通过电商来销售农产品可以大大减轻干农活的负担。在重庆荣昌的一个小村庄里&#xff0c;一个名叫杨儿杨的00后女孩&#xff0c;正通过抖音电商&#xff0c;书写着她自己的故事。杨儿杨的生活并不容易。5岁前&#xff…

数据库 --- mysql(01)

MYSQL 1、数据库简介 数据&#xff1a;描述事物的符号记录&#xff0c; 可以是数字、 文字、图形、图像、声音、语言等&#xff0c;数据有多种形式&#xff0c;它们都可以经过数字化后存入计算机。 数据库&#xff1a;存储数据的仓库&#xff0c;是长期存放在计算机内、有组…

武大+上交提出 BatGPT:创新性采用双向自回归架构,可预测前后token

进NLP群—>加入NLP交流群 本论文介绍了一种名为BATGPT的大规模语言模型&#xff0c;由武汉大学和上海交通大学联合开发和训练。 该模型采用双向自回归架构&#xff0c;通过创新的参数扩展方法和强化学习方法来提高模型的对齐性能&#xff0c;从而更有效地捕捉自然语言的复杂…

Seata 分布式事务的中间件Seata设计和实现方案

文章目录 分布式事务的中间件SeataSeata AT 模式-默认模式前提整体机制写隔离读隔离 Seata XA 模式前提整体机制工作机制 Seata TCC 模式Seata Saga 模式概述缺点&#xff1a; Saga的实现外传 分布式事务的中间件Seata Seata 是一款开源的分布式事务解决方案&#xff0c;致力于…

element ui组件的自定义类名样式不生效

element ui中&#xff0c;类似描述列表这种组件 会提供自定义类名属性 需要注意&#xff0c;样式不能写在<style scoped>标签中&#xff0c;会被vue自动加上data-v属性&#xff0c;导致样式失效。 必须写在<style>标签里

C++进阶—红黑树详解及map、set封装(3)

目录 1. 红黑树的概念 2. 红黑树的性质 3. 红黑树节点的定义 4. 红黑树性质分析 5. 红黑树插入节点简要分析&#xff08;新插入节点的parent为红色&#xff09; 5.1 简要分析1-变色&#xff1a; 5.2 简要分析2-变色旋转 5.3 简要分析总结 6. 红黑树插入节点详细分析 …

Linux下载安装Redis(Ubuntu系统)

相比于 Windows 系统而言&#xff0c;Redis 更适合于在 Linux 系统上使用&#xff0c;这是由 Redis 的底层机制决定的。下面介绍一下如何在 Linux 发行版 Ubuntu 系统上安装 Redis 数据库。 了解Redis版本 Redis 版本号采用国际标准惯例&#xff0c;即“主版本号.副版本号.补…