Linux 缓冲区

概念

  • 用于临时存储数据内存区域,目的是优化设备 I/O 操作,以提高传输效率

刷新方式

  1. 无缓冲(立即刷新):write
  2. 行缓冲(行刷新):显示器文件
  3. 全缓冲(缓冲区满刷新):磁盘文件

刷新策略

  1. 强制刷新(fflush)
  2. 进程退出的时候,刷新缓冲区

样例实验

cpp 复制代码
  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <string.h>
  4 
  5 const char* str1 = "C library: fputs\n";
  6 const char* str2 = "system call: write\n";
  7 int main()
  8 {
  9     printf("C library: printf\n");
 10     fprintf(stdout, "C library: fprintf\n");
 11     fputs(str1, stdout);
 12     write(1, str2, strlen(str2));
 13     //fork();                                                                                                                                 
 14     return 0;            
 15 } 

无fork

向显示器文件打印

  • 缓冲区的刷新方式 是:行刷新
  • 前三个C库函数,不停留在C语言的缓冲区,直接出来
  • 所以系统调用接口实在最后呈现

向磁盘文件打印

  • 缓冲区的刷新方式 是:全刷新
  • 前三个C库函数所打印的内容没有填满C语言缓冲区,所以实在程序结束的时候才刷新缓冲区
  • 系统调用接口不经过C语言缓冲区,导致他先打印在前面

有fork

为什么重定向到磁盘文件的时候会多打印?

磁盘文件里的系统调用那句没有多打印?

是因为fork起作用了?那为什么显示器文件就没有呢?

解决这些问题的关键是打印二字

  • 打印可以理解成一种刷新的方式
  • 打印过程并不是直接程序到文件
  • self buffer:被写入的数据
  • 刷新:C buffer拷贝到文件缓冲区的过程,即:用户到操作系统
  • 调用C库函数的时候,先是拷贝到C buffer里;对应系统调用接口是不经过C buffer,直接拷贝到文件缓冲区里

原理

  • 最根本的还是对fork创建子进程的补充和理解
  • 可能对C buffer不是特别了解;这就是一个内存区域,这个区域的地址通过页表,与虚拟地址空间映射
  • 其实就是子进程也有,于内存上是同一份;但是注意:每个进程都有自己独立的虚拟地址空间和页表,这里他们的内容一样,所以指向同一个缓冲区
  • 那么问题就解决了,子进程结束的时候会刷新缓冲区,所以会有两份;系统调用打印的内容不在C语言缓冲区里,所以子进程没有

问题与补充

1.对printf的理解

  • 边拷贝,边做字符串分析
  • printf封装了write
  • 刷新本质都是通过write实现的
  • printf每次都会往C buffer里写,但并不是每次都刷新,是按照刷新方式来决定是这次printf否调用write

2.对write的理解

  • 不是说write不会将数据放入C语言缓冲区吗?是的,不矛盾
  • C buffer 和self buffer一样都只是一个用于存放数据的内存区域,只不过C buffer 大点,就是为了一次性刷新多点数据,来提高程序的效率
  • 所以说write的本质就是将C buffer 或者 self buffer里的数据直接(不停留的)刷新到文件缓冲区
  • 换句话说write不依赖C buffer,但是write可以刷新C buffer

3.对 "清空" 的理解

  • 缓冲区刷新之后会被清空,清空并不是对物理内存数据的清空
  • 用字段就可以表示当前缓冲区的有效数据长度或当前位置
  • 所以上面的代码,在子进程刷新的时候是发生了写实拷贝的

4.对 "文件缓冲区" 的理解

  • 属于操作系统,不属于进程

5.程序并不知道是否重定向,那么是怎么决定缓冲区的刷新方式的?

  • 当 stdout 被重定向到文件时,C 标准库 会自动检测到输出流的目标发生了变化 ,并根据不同的流类型调整缓冲区的刷新策略

  • 终端检测:标准库通过 isatty() 系统调用来判断标准输出是否连接到终端;如果标准输出是终端设备,则使用行缓冲;如果标准输出被重定向到文件或其他非终端设备,则使用全缓冲

  • 总结:C 标准库在第一次打开或使用流时,通过检测文件描述符 1(即 stdout)对应的文件属性来决定缓冲方式

FILE

  • 可以通过FILE对象来找到缓冲区
  • C 语言提供的缓冲区(C buffer)和 FILE 结构体中的缓冲区是一样的,它们指的是同一个概念

用系统调用封装C库函数

main.c

cpp 复制代码
#include "mystdio.h"
#include <stdio.h>
int main()
{
    myFILE* fp = my_fopen("./log.txt", "w");
    if(fp == NULL)
    {
        perror("my_fopen");
        return -1;
    }

    int cnt = 5;
    const char* msg = "abcde\n12345"; 
    while(cnt--)
    {
        my_fwrite(msg, strlen(msg), fp);
        //my_fwrite(msg, sizeof(msg), fp);
        sleep(1);
    } 
    my_fclose(fp);
    return 0;
}

mystdio.h

cpp 复制代码
#pragma once
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>

#define DFL_MODE 0666
#define FFLUSH_LINE 1
#define SIZE_BUFFER 1024

typedef struct _myFILE
{
    int fileno;
    int flag;
    char buffer[SIZE_BUFFER];
    int end;
}myFILE;

myFILE* my_fopen(const char* path, const char* mode);
int my_fwrite(const char* str, int num, myFILE* stream);
int my_fflush(myFILE* stream);
int my_fclose(myFILE* stream);

mystdio.c

cpp 复制代码
#include "mystdio.h"

myFILE* my_fopen(const char* path, const char* mode)
{
    int fd = 0;
    int flags = 0;
    if(strcmp(mode, "r") == 0)
        flags |= O_RDONLY;
    else if(strcmp(mode, "w") == 0)
        flags |= (O_WRONLY | O_TRUNC | O_CREAT);
    else if(strcmp(mode, "a") == 0)
        flags |= (O_APPEND | O_WRONLY | O_CREAT);
    
    if(flags | O_TRUNC)
        fd = open(path, flags, DFL_MODE);
    else fd = open(path, flags);
    
    if(fd < 0)
    {
        errno = 2;
        return NULL;
    }

    myFILE* fp = (myFILE*)malloc(sizeof(myFILE));//自定义类型要加()
    if(fp == NULL)
    {
        errno = 3;
        return NULL;
    }

    fp->fileno = fd;
    fp->flag = FFLUSH_LINE;
    fp->end = 0;
    return fp;
}

int my_fwrite(const char* str, int num, myFILE* stream)
{
    memcpy(stream->buffer + stream->end, str, num);
    //stream->end += num;

    // 判断是否需要刷新, "abcd\nefgh"
    //if((stream->flag & FFLUSH_LINE) && stream->end > 0 && stream->buffer[stream->end-1] == '\n')
    //{
    //    my_fflush(stream);
    //}
    int last_end = 0;//geshu
    if((stream->flag & FFLUSH_LINE) && (num > 0))//问题在这,个数用num
        for(size_t i = stream->end; i < stream->end + num; i++)
           // if(stream->buffer[i] == '\\' && stream->buffer[i + 1] == 'n')
            if(stream->buffer[i] == '\n')
                last_end = i + 1;
    printf("%d\n", last_end);
    stream->end = stream->end + num - last_end;
    printf("%s\n\n", stream->buffer);
    if(last_end)
    {
        write(stream->fileno, stream->buffer, last_end);
        memmove(stream->buffer, stream->buffer + last_end, stream->end);
    }
    return 0;
}

int my_fflush(myFILE* stream)
{
    if(stream->end > 0)
    {
        write(stream->fileno, stream->buffer, stream->end);
        stream->end = 0;
    }
    return 0;
}

int my_fclose(myFILE* stream)
{
    my_fflush(stream);
    return close(stream->fileno);
}

实验目的

  • 模拟缓冲区是怎么刷新的
  • 模拟实现行刷新,解决代码问题和逻辑问题
  • 更好理解缓冲区的实现

函数解释

my_fclose

  • 就是一点:关闭之前,要刷新缓冲区

my_fflush

  • 调用这个函数的时候,如果stream->end不为0,就是缓冲区里有有效值,刷新
  • 刷新之后记得清0 stream->end

my_fwrite

逻辑

  • 将self buffer 拷贝到 C buffer里
  • 检查新拷贝的内容是否有\n
  • 更新stream->end
  • 刷新缓冲区
  • 拷贝没有被刷新的内容

遇到的问题

  1. 遍历:我写的其实不需要num>0这个条件了,因为下面的for循环检查越界了;在此之前我写的是stream->end>0,这就导致第一次的abcde不会输出,因为stream->end这时候为0;bug就在这,我写的这个逻辑中第一次的stream->end并不代表个数
  2. stream->end更新的位置:开始是放在if(last_end)里,这样肯定不对!因为不刷新的时候,stream->end也要更新啊

总结

  • 刷新方式比较重要
  • 弄清楚样例
  • 创建子进程时候,缓冲区是同一个,进程退出时候,会发生写实拷贝
  • FILE结构体有维护缓冲区的字段
相关推荐
大树8814 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠14 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush414 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行52015 小时前
Linux 11 动态监控指令top
linux
小宇宙Zz15 小时前
Maven依赖冲突
java·服务器·maven
不会C语言的男孩16 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
古城小栈16 小时前
Unix 与 Linux 异同小叙
linux·服务器·unix
程序猿阿伟17 小时前
《Chrome离线扩展安装的底层逻辑与场景落地指南》
服务器·网络·chrome
半条-咸鱼17 小时前
【STM32】I2C协议原理、HAL读写与OLED显示操作
嵌入式硬件·c·信息与通信
凡人叶枫17 小时前
Effective C++ 条款42:了解 typename 的双重意义
java·linux·服务器·c++