【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分离可以方便调试

相关推荐
祈禾1 小时前
Redis三大缓存问题与分布式锁
运维·数据库·redis·笔记·分布式·缓存
ltl3 小时前
向量库与图 RAG:HNSW、DiskANN 到 GraphRAG
linux
ltl3 小时前
可观测性存储与成本:采样、下采样、冷热分层
运维
学代码的CJY4 小时前
Linux 库制作与原理:静态库、动态库、ELF 与动态链接完全指南
linux·运维·服务器
沸速存储4 小时前
CPU 和 GPU 核心差别在哪?为什么 AI 训练离不开 GPU
服务器·人工智能·科技·嵌入式硬件·电脑
SatanII4 小时前
403 禁地:Nginx 拒绝所有生者进入
运维·nginx
呱呱巨基5 小时前
Linux 查找命令
linux·笔记·学习·查找命令
路由侠内网穿透5 小时前
本地部署企业级快速开发平台芋道管理后台并实现外部访问
运维·服务器·网络·网络协议
basketball6165 小时前
软考中级网络工程师 第4章 无线通信网 备考总结
linux·服务器·网络·网络工程师·软考