【Linux】缓冲区理解

简介: 【Linux】缓冲区理解

👉缓冲区👈


缓冲区引起的差异


#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
    // C接口
    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    fputs("hello fputs\n", stdout);
    // system call
    const char* msg = "hello write\n";
    write(1, msg, strlen(msg));
    fork();
    return 0;
}

0e61d580d2ce41088eada25fbf3e33bb.png将 fork 函数注释掉,再重复以上的过程。

aafc294fe0cd49e1b5895c9b1bbf64e4.png


12a1e3315a85492aa6245a4c628d9ddd.png


对比这两份份代码,产生这种差异应该是与 fork 创建子进程有关,也应该与写实拷贝有关。想要理解这种想象,我们必须要理解缓冲区。


缓冲区本质就是一段内存,其意义是节省进程进行数据 IO 的时间。缓冲区就想现实生活中的快递行业能够节省发送者的时间。

af178c6b18d240328e235266e277bd11.png

进程要向文件中写入数据,首先进程会将数据拷贝到缓冲区,再将数据刷新写入到文件中。而我们之前用的 fwrite 等函数本质上就是拷贝函数。


缓冲区的刷新策略


如果有一堆数据,一次将这堆数据写入到外设的效率是要比将这堆数据分批次写入到外设的效率要高的。因为 IO 的大多数时间都要等外设就绪的,分批次写入需要等待更长的时间。那么,缓冲区一定会结合具体的设备定制自己的刷新策略:


立即刷新 — 无缓冲

行刷新 — 行缓冲(显示器)

缓冲区满 — 全缓冲(磁盘文件)

为什么显示器采取的是行缓冲呢?因为显示器是给用户看的,如果采取全缓冲的刷新策略,用户体验会比较差。为了用户体验不会太差且 IO 效率也不至于太低,所以显示器就采取了行缓冲的刷新策略。


两种特殊情况:一是用户强制刷新缓冲区;二是进程退出时,一般都要进行缓冲区刷新。


缓冲区在哪里

61ba280ccb2a4d80ad03b6dc81e09284.png


为了解释上图出现的现象,我们必须知道缓存区在哪里。因为这种现象一定和缓冲区有关!通过上图的现象,我们可以知道:该缓冲区一定不在内核中!!! 如果在内核中,那么 write 也应该被打印两次。其实我们之前谈论的所有的缓冲区都是用户级语言层面给我们提供的缓冲区,这个缓冲区就是在 FILE 结构体里,该结构体中包含了文件描述符 fd 和 用户级缓冲区。所以我们调用 fflush 函数强制刷新缓冲区,要传文件指针FILE*。


b1e52b2cd0a54f5f9692d69930db1250.png


因为 IO 相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上访问文件都是通过文件描述符 fd 访问的。fprintf 等函数向外设写入数据,首先会写入到 FILE 结构体内部的缓冲区中,然后在合适的时候刷新到外设中。


那么接下来,本人就给大家解释一下上面 fork 引起的差异。


如果没有进行输出重定向,看到了四条数据。stdout 默认使用的是行刷新。在 fork 创建子进程之前,三条 C 语言函数已经将数据打印输出到显示器(外设)上了,FILE 结构体内部和进程内部不存在对应的数据了。

如果进行了输出重定向,写入文件不再是显示器,而是磁盘文件。其采取的缓冲区刷新策略是全缓冲,之前的三条 C 语言函数要打印的数据不足以将 stdout 的缓冲区写满,数据并没有被刷新到磁盘文件中。stdout 是属于父进程的。fork 创建子进程后,紧接着就是进程退出。谁先退出,一定要进行缓冲区刷新,其本质就是修改。一旦数据发生修改,那么就会有写时拷贝!最终 C 语言接口的数据会显示两份。

write 的数据为什么被没有显示两次呢?因为上面的过程都和 write 无关,write 没有使用 FILE*,而用的是文件描述符 放大,没有 C 语言提供的缓冲区。


简单模拟实现缓冲区


// myStdio.h
#pragma once
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define SIZE 1024   // 缓冲区大小
#define SYNC_NOW   1    // 无缓冲
#define SYNC_LINE  2    // 行缓冲
#define SYNC_FULL  4    // 全缓冲
typedef struct FILE_
{
    int flags;  // 缓冲区刷新策略
    int fileno; // 文件描述符
    int size;   // buffer当前的使用量
    int capacity;   // buffer的总容量
    char buffer[SIZE];  //缓冲区
}FILE_;
FILE_* fopen_(const char* pathname, const char* mode);
void fwrite_(const void* ptr, int num, FILE_* fp);
void fflush_(FILE_* fp);
void fclose_(FILE_* fp);
// myStdio.c
#include "myStdio.h"
FILE_* fopen_(const char* pathname, 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:r+, w+...
    }
    int fd = 0;
    if(flags & O_RDONLY)  fd = open(pathname, flags);
    else  fd = open(pathname, 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 != NULL);
    fp->flags = SYNC_LINE; // 默认设置成行刷新
    fp->fileno = fd;
    fp->size = 0;
    fp->capacity = SIZE;
    memset(fp->buffer, 0, SIZE);
    return fp;  // 打开文件成功返回FILE*的原因
}
void fwrite_(const void* ptr, int num, FILE_* fp)
{
    // 数据写入到缓冲区
    memcpy(fp->buffer + fp->size, ptr, num); // 这里不考虑缓冲区溢出的问题
    fp->size += num;
    // 是否刷新缓冲区
    if(fp->flags & SYNC_NOW)
    {
        write(fp->fileno, fp->buffer, fp->size);
        fp->size = 0; // 清空缓冲区
    }
    else if(fp->flags & SYNC_LINE)
    {
        // 不考虑abcd\nef的情况
        if(fp->buffer[fp->size - 1] == '\n')
        {
            write(fp->fileno, fp->buffer, fp->size);
            fp->size = 0; // 清空缓冲区
        }
    }
    else if(fp->flags & SYNC_FULL)
    {
        if(fp->size == fp->capacity)
        {
            write(fp->fileno, fp->buffer, fp->size);
            fp->size = 0; // 清空缓冲区
        }
    }
    else
    {
        return;
    }
}
void fflush_(FILE_* fp)
{
    if(fp->size > 0)  write(fp->fileno, fp->buffer, fp->size);
    fsync(fp->fileno);  // 强制刷新内核缓冲区,将数据刷新到外设中
    fp->size = 0; // 清空缓冲区
}
void fclose_(FILE_* fp)
{
    fflush_(fp);
    close(fp->fileno);
    free(fp);
}
// 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\n";
    while(1)
    {
        --cnt;
        fwrite_(msg, strlen(msg), fp);
        sleep(1);
        printf("count:%d\n", cnt);
        if(cnt == 0)   break;
    }
    fclose_(fp);
    return 0;
}

0a8db74dc8284ddea1ba0888d2ee0487.png


监控脚本
while :; do cat log.txt ; sleep 1; echo "------------------"; done


因为默认的是行缓冲,所以一秒就会向文件写入一次数据。当然也可以调用fflush_函数强制刷新缓冲区。


其实调用 write 接口也不是直接将数据直接就写入外设中,而是内核缓冲区中,至于什么时候刷新内核缓冲区由操作系统自主决定!但是有些信息是非常重要的,需要马上刷新内核缓冲区写入到磁盘文件中。那么此时就需要借助fsync接口了,该接口可以直接刷新内核缓冲区并将数据写入外设中。

fe994c84362f4d4581508d32758c9c10.png

0f51a6e40bbd47f19a4c349639e4cdb5.png



👉总结👈


本篇博客主要讲解了什么是缓冲区、缓冲区的刷新策略、缓冲区在哪里以及简单模拟实现缓冲区。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️











相关文章
|
7月前
|
Linux C语言 C++
【Linux】14. 文件缓冲区
【Linux】14. 文件缓冲区
77 1
|
2月前
|
存储 缓存 固态存储
|
4月前
|
小程序 Linux 开发者
Linux之缓冲区与C库IO函数简单模拟
通过上述编程实例,可以对Linux系统中缓冲区和C库IO函数如何提高文件读写效率有了一个基本的了解。开发者需要根据应用程序的具体需求来选择合适的IO策略。
34 0
|
5月前
|
Linux 编译器 C语言
【Linux】基础IO----理解缓冲区
【Linux】基础IO----理解缓冲区
73 0
【Linux】基础IO----理解缓冲区
|
7月前
|
存储 缓存 监控
|
6月前
|
Linux
【linux】重定向|缓冲区
【linux】重定向|缓冲区
35 0
|
7月前
|
缓存 Linux Shell
[Linux打怪升级之路]-缓冲区
[Linux打怪升级之路]-缓冲区
|
7月前
|
存储 缓存 Linux
【linux基础I/O(二)】文件系统讲解以及文件缓冲区的概念
【linux基础I/O(二)】文件系统讲解以及文件缓冲区的概念
|
7月前
|
存储 缓存 Shell
【Shell 命令集合 磁盘维护 】⭐⭐⭐Linux 将文件系统的缓冲区数据立即写入磁盘 sync 命令使用教程
【Shell 命令集合 磁盘维护 】⭐⭐⭐Linux 将文件系统的缓冲区数据立即写入磁盘 sync 命令使用教程
106 1
|
7月前
|
存储 运维 Linux
【Shell 命令集合 系统设置 】⭐Linux 显示Linux内核环缓冲区的内容 dmesg命令 使用指南
【Shell 命令集合 系统设置 】⭐Linux 显示Linux内核环缓冲区的内容 dmesg命令 使用指南
147 0
【Shell 命令集合 系统设置 】⭐Linux 显示Linux内核环缓冲区的内容 dmesg命令 使用指南