【Linux】缓冲区理解

作者:@阿亮joy.
专栏:《学会Linux》
座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
在这里插入图片描述

目录

    • 缓冲区
      • 缓冲区引起的差异
      • 缓冲区的刷新策略
      • 缓冲区在哪里
      • 简单模拟实现缓冲区
    • 总结

缓冲区

缓冲区引起的差异

#include 
#include 
#include 
#include 
#include 
#include 

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;
}

【Linux】缓冲区理解_第1张图片

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

【Linux】缓冲区理解_第2张图片

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

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

【Linux】缓冲区理解_第4张图片

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

缓冲区的刷新策略

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

  • 立即刷新 — 无缓冲
  • 行刷新 — 行缓冲(显示器)
  • 缓冲区满 — 全缓冲(磁盘文件)

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

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

缓冲区在哪里

【Linux】缓冲区理解_第5张图片

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

【Linux】缓冲区理解_第6张图片

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

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

  • 如果没有进行输出重定向,看到了四条数据。stdout 默认使用的是行刷新。在 fork 创建子进程之前,三条 C 语言函数已经将数据打印输出到显示器(外设)上了,FILE 结构体内部和进程内部不存在对应的数据了。
  • 如果进行了输出重定向,写入文件不再是显示器,而是磁盘文件。其采取的缓冲区刷新策略是全缓冲,之前的三条 C 语言函数要打印的数据不足以将 stdout 的缓冲区写满,数据并没有被刷新到磁盘文件中。stdout 是属于父进程的。fork 创建子进程后,紧接着就是进程退出。谁先退出,一定要进行缓冲区刷新,其本质就是修改。一旦数据发生修改,那么就会有写时拷贝!最终 C 语言接口的数据会显示两份。
  • write 的数据为什么被没有显示两次呢?因为上面的过程都和 write 无关,write 没有使用 FILE*,而用的是文件描述符 放大,没有 C 语言提供的缓冲区。

简单模拟实现缓冲区

// myStdio.h
#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#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 

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;
}

【Linux】缓冲区理解_第7张图片

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

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

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

【Linux】缓冲区理解_第8张图片

【Linux】缓冲区理解_第9张图片

总结

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

你可能感兴趣的:(学会Linux,linux,服务器,缓冲区,运维)