【Linux】缓冲区

1、文件打开和读写的基本过程

磁盘上保存文件:文件=文件内容 + 文件属性

进程运行时,PCB内部维护文件描述符表(fd数组),每一个进程都有一个操作系统级别的文件缓冲区,每打开一个文件,就会创建一个struct file ,对应一份独立的内核缓冲区

  • read系统调用:
cpp 复制代码
ssize_t read(int fd, void *buf, size_t count);

buf是用户缓冲区,属于用户空间,用户代码可以直接访问,根据fd找到struct file以及它的内核缓冲区,如果内核缓冲区没有数据:进程阻塞,操作系统驱动把磁盘数据加载到内核缓冲区;加载完成唤醒进程,read 只做拷贝 :把数据从内核缓冲区拷贝到用户缓冲区 buf ,所以read函数本质是拷贝函数

普通磁盘文件磁盘加载速度快,阻塞感知不到

  • write系统调用:
cpp 复制代码
ssize_t write(int fd, const void *buf, size_t count);

buf是用户缓冲区,write把buf中写好的数据拷贝给内核缓冲区,write本质也是拷贝函数

  • 文件修改逻辑:

磁盘不能原地修改文件,修改文件的流程是:磁盘 → 加载到内核缓冲区 → 修改内核缓冲区数据 → 操作系统重新刷新回磁盘,在文件IO中,大部分情况下,要先加载,再操作,再刷新。read/write系统调用,只负责用户空间和内核缓冲区中数据的拷贝,磁盘和内核缓冲区之间的数据搬运是由操作系统驱动自动完成的

  • 为什么要有语言级缓冲区:

当使用malloc或new开辟空间时,需要系统调用,2倍扩容机制的目的是减少系统调用的次数,所以,调用系统调用是有成本的

使用read或write时,需要系统调用,但如果用户输入多次小批量数据,那么系统调用次数就会变多,如果把这些数据先缓存在用户层,攒一批,就可以减少系统调用次数

比如fputs:把参数s指向的"hello world"字符串拷贝到FILE对象内部的输出缓冲区,此时可以还输入多个字符串,这些字符串都会存到输出缓冲区中,当满足刷新条件时,调用系统调用,把数据拷贝到内核缓冲区,最后刷新把字符串输出到屏幕

输入缓冲区和输出缓冲区这两块就是C语言中的语言级缓冲区,有了语言级缓冲区可以提高C语言的IO函数的运行效率

2、引入缓冲区

以fopen为例:

cpp 复制代码
FILE *fopen(const char *path, const char *mode);

FILE的本质是结构体,FILE *fopen() 打开文件后,会在fopen内部创建结构体FILE对象

cpp 复制代码
struct FILE {
	int fd;               // 文件描述符,对应内核打开的文件
	char inbuffer[];      // 输入缓冲区
	char outbuffer[];     // 输出缓冲区
	// ...其他成员
};

stdin/stdout/stderr也是FILE*,分别对应 fd=0、1、2

fclose(fp)会释放这个FILE对象,关闭 fd,同时刷新缓冲区

  • 缓冲区的三种类型:

全缓冲:缓冲区满了才刷新,普通文件一般是全缓冲

行缓冲 :stdout终端,遇到\n才刷新,目标文件是显示器

无缓冲:直接刷新,没有语言级缓冲

  • 三种现象:
  1. 重定向
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main()
{
    close(1);
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    printf("fd is : %d\n", fd);  // stdout --> 1
    
    return 0;
}

此时实现输出重定向,如果加上close:

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

int main()
{
    close(1);
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    printf("fd is : %d\n", fd);  // stdout --> 1
    close(fd);
    return 0;
}

此时读取log.txt中的内容无输出

重定向后目标文件是log.txt,不是显示器,从行缓冲转换成全缓冲,缓冲区满了才能刷新,执行完printf后,数据还在用户缓冲区,然后执行close(fd),进程退出时刷新缓冲区,但fd已关闭,无数据,缓冲区中的字符串直接丢弃

从这里可以看出,刷新的本质是从语言缓冲区通过write(fd)系统调用把数据拷贝到文件的内核缓冲区里

  1. exit和_exit

exit:

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

int main()
{
    printf("hello world");
    sleep(3);
    exit(0);

    return 0;
}

_exit:

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

int main()
{
    printf("hello world");
    sleep(3);
    _exit(0);

    return 0;
}

缓冲区一定不在操作系统内,exit()会冲刷缓冲区,如果缓冲区在操作系统内,那么_exit也一定会冲刷缓冲区

  1. 向显示器打印字符串
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main()
{
    // C库函数
    // 向显示器打印字符串,有几种做法?
    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    const char *s = "hello fputs\n";
    fputs(s, stdout);

    // 系统调用
    const char *ss = "hello write\n";
    write(1, ss, strlen(ss));

    fork();
    return 0;
}

重定向到log.txt文件后打印log.txt中的内容:

除了write,其余的都输出了两遍

重定向后,输出本应向显示器上输出,为行刷新,重定向后变成向普通文件中写,变成全缓冲,缓冲区写满才会刷新,所以父进程打印的字符串会留在缓冲区,创建子进程后就return返回,父子都会调用fclose,父子进程要各自刷新一次,所以输出两次

内核的系统调用write已经把数据写给操作系统了,已经把数据从用户内存拷贝到内核缓冲区,交给操作系统管理,不再属于这个进程的用户内存,数据不再缓冲区里缓存,所以输出一次

内核文件缓冲区细节:

  1. 只要把数据从用户(语言)缓冲区 拷贝到了内核文件缓冲区,就相当于交给了硬件
  2. 客观上,就是写给了 file 对应的文件内核缓冲区啊 → OS → 自主刷新 → OS→ 磁盘,OS有自己的刷新策略,什么时候刷新,有OS自己定

如果想让内核缓冲区立即刷新,那么需要用到fsync函数

cpp 复制代码
#include <unistd.h>
int fsync(int fd);

3、标准错误stderr

程序变成进程是为了完成任务,即对数据做加工处理,那么就会有数据源和处理结果,为了方便debug,把标准输入标记为0,把标准输出标记为1

test.c:

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

int main()
{
    // 标准输出: 1
    printf("这是一个正常消息\n");
    fprintf(stdout,"这也是一个正常的日志消息\n");
    const char *s1 = "这是一个正常消息, write\n";
    write(1, s1, strlen(s1));

    // 标准错误: 2
    fprintf(stderr, "这是一个错误消息\n");
    const char *s2 = "这是一个错误消息, write\n";
    write(2, s2, strlen(s2));
    perror("perror, hello");

    return 0;
}

进行重定向:

这样可以是标准错误和标准输出分离

程序在运行时会产生大量的消息,此时可以把调试信息全部写到标准错误中,正常的输出写到标准输出中此时1、2分离可以方便调试

相关推荐
且从容.2 小时前
Ubuntu安装flume
linux·ubuntu·flume
承渊政道2 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
讯捷蓝达4 小时前
戴尔R720xd服务器卡死在 “Configuring Memory...Done“,换了内存还是卡?问题根本不在内存
运维·服务器
且从容.5 小时前
ubuntu安装flink
linux·ubuntu·flink
数字新视界6 小时前
动环监控信创适配在智能机房管理中的应用价值与未来展望
大数据·服务器·数据库·动环监控系统·动力与环境监控系统
爱和冰阔落6 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨6 小时前
WSL2的安装与使用
linux·wsl
微小的鱼7 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng7 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
Android系统攻城狮8 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频