【Linux】缓冲区
1、文件打开和读写的基本过程
磁盘上保存文件:文件=文件内容 + 文件属性
进程运行时,PCB内部维护文件描述符表(fd数组),每一个进程都有一个操作系统级别的文件缓冲区,每打开一个文件,就会创建一个struct file ,对应一份独立的内核缓冲区
- read系统调用:
ssize_t read(int fd, void *buf, size_t count);
buf是用户缓冲区,属于用户空间,用户代码可以直接访问,根据fd找到struct file以及它的内核缓冲区,如果内核缓冲区没有数据:进程阻塞,操作系统驱动把磁盘数据加载到内核缓冲区;加载完成唤醒进程,read 只做拷贝:把数据从内核缓冲区拷贝到用户缓冲区 buf,所以read函数本质是拷贝函数
普通磁盘文件磁盘加载速度快,阻塞感知不到
- write系统调用:
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为例:
FILE *fopen(const char *path, const char *mode);
FILE的本质是结构体,FILE *fopen() 打开文件后,会在fopen内部创建结构体FILE对象
struct FILE {
int fd; // 文件描述符,对应内核打开的文件
char inbuffer[]; // 输入缓冲区
char outbuffer[]; // 输出缓冲区
// ...其他成员
};
stdin/stdout/stderr也是FILE*,分别对应 fd=0、1、2
fclose(fp)会释放这个FILE对象,关闭 fd,同时刷新缓冲区
- 缓冲区的三种类型:
全缓冲:缓冲区满了才刷新,普通文件一般是全缓冲
行缓冲:stdout终端,遇到\n才刷新,目标文件是显示器
无缓冲:直接刷新,没有语言级缓冲
- 三种现象:
- 重定向
#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:
#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)系统调用把数据拷贝到文件的内核缓冲区里
- exit和_exit
exit:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
printf("hello world");
sleep(3);
exit(0);
return 0;
}

_exit:
#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也一定会冲刷缓冲区

- 向显示器打印字符串
#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已经把数据写给操作系统了,已经把数据从用户内存拷贝到内核缓冲区,交给操作系统管理,不再属于这个进程的用户内存,数据不再缓冲区里缓存,所以输出一次
内核文件缓冲区细节:
- 只要把数据从用户(语言)缓冲区拷贝到了内核文件缓冲区,就相当于交给了硬件
- 客观上,就是写给了 file 对应的文件内核缓冲区啊 → OS → 自主刷新 → OS→ 磁盘,OS有自己的刷新策略,什么时候刷新,有OS自己定
如果想让内核缓冲区立即刷新,那么需要用到fsync函数
#include <unistd.h>
int fsync(int fd);
3、标准错误stderr
程序变成进程是为了完成任务,即对数据做加工处理,那么就会有数据源和处理结果,为了方便debug,把标准输入标记为0,把标准输出标记为1
test.c:
#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分离可以方便调试
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)