欢迎光临
我们一直在努力

《Linux 内核文件缓冲区:IO 性能 “悄无声息” 的优化大师》

前引:你调用write写入文件时,数据真的 “立刻” 写到磁盘了吗?其实不然 ——Linux 内核引入了文件缓冲区,让数据先暂存于内存,再 “批量写入” 磁盘(延迟写策略)。这种设计是平衡 “IO 性能” 与 “系统开销” 的关键,但也带来了 “数据何时落盘” 的疑问。本文将解析内核文件缓冲区的工作机制,带你理解 “缓冲 IO” 为何能成为 Linux 性能优化的幕后功臣!

注意:IO(输入 / 输出)是计算机系统与外部进行数据交互的核心机制

目录

【一】C语言文件接口

(1)fopen()

(2)fwrite()

(3)输出流

【二】系统调用open()

实现C接口创建文件

【三】系统调用接口write()

实现C接口文件写入

【四】文件描述符

(1)文件的管理

(2)描述符0、1、2

【五】重定向

(1)输出重定向

(2)追加重定向

(3)输入重定向

【六】Linux之下一切皆文件

【七】缓冲区

(1)引入

(2)语言缓冲区

(3)子进程与缓冲区


为什么对文件没有添加路径,会默认创建在当前工作目录?

文件的操作由当前进程完成的,而每个进程都有自己的“当前工作目录”,进程启动时会继承父进程的“当前工作目录”,如果我们在创建文件时没有指定路径,会采用当前进程的工作目录为默认路径

如何查看当前进程的工作目录:

由当前进程的PID->指令查询(ls -l /proc/PID/cwd)cwd代表当前工作目录

【一】C语言文件接口

(1)fopen()

在C语言中,我们一般打开文件是由 fopen()函数完成,函数原型:

#include <stdio.h>

FILE *fopen(const char *filename, const char *mode);

第一个参数:打开的文件名,可以带路径

第二个参数:打开文件的方式,常见的选项如下:

"r" read(只读) 文件必须存在,否则打开失败
"w" write(只写) 清空原内容;若文件不存在则创建(每次写入会清除目标文件内容)
"a" append(追加) 在文件末尾追加内容;若文件不存在则创建(末尾继续写入目标文件)

例如:对文件的打开/写入和关闭(对文件的写入可以不加“\\n”或者“\\0”,因为这是文件操作)

(2)fwrite()

函数原型:

#include <stdio.h>

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

参数解释:

第一个参数:写入内容 第二个参数:写入大小

第三个参数:写入次数

第四个参数:文件指针

例如:

(3)输出流

C程序执行时,会默认打开三个输⼊输出流,分别是stdin,stdout,stderr,返回类型都是FILE*类型

stdin:类似键盘文件

stdout:类似显示器文件

stderr:类似显示器文件

既然类似都是 FILE* 我们也可以直接向对应的流里面写,例如:

【二】系统调用open()

open() 是 Unix/Linux 系统调用的相关文件操作接口,不是 C 标准库函数,用来打开或者创建一个文件,需要设置文件的权限等信息,因此各种语言中的创建文件可能就是封装此接口!

函数原型:

#include <fcntl.h> // 包含 open() 的声明
#include <sys/stat.h> // 包含文件权限宏(S_IRUSR, S_IWUSR 等)
#include <unistd.h> // 包含 read(), write(), close() 等

int open(const char *pathname, int flags, mode_t mode);

参数解释:

第一个参数:打开或者创建的文件名(可以带路径)

第二个参数:打开文件的标志,必须包含以下三个之一,仅可选一个:

标志含义
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 可读可写

                       还可以 按位或(|)添加其他选项,常用的有:

标志含义
O_CREAT 如果文件不存在,就创建它(需要提供 mode 参数)
O_EXCL 与 O_CREAT 一起使用,如果文件已存在则报错(防止覆盖)
O_TRUNC 如果文件已存在且以写方式打开,则清空原有内容
O_APPEND 每次写操作都追加到文件末尾
O_NONBLOCK 非阻塞模式(对管道、设备文件很有用)

                       例如:O_WRONLY | O_CREAT | O_TRUNC

第三个参数:当第二个参数选择创建文件时,必须填此参数(文件权限掩码)

                       当 flags 包含 O_CREAT 时,必须提供此参数,用来设置新文件的权限

                       它是一个八进制数,常用的宏定义在 <sys/stat.h> 中:

宏八进制含义
S_IRUSR 0400 所有者可读
S_IWUSR 0200 所有者可写
S_IXUSR 0100 所有者可执行
S_IRGRP 0040 组可读
S_IWGRP 0020 组可写
S_IXGRP 0010 组可执行
S_IROTH 0004 其他用户可读
S_IWOTH 0002 其他用户可写
S_IXOTH 0001 其他用户可执行

返回值:

  • 成功:返回一个非负整数(文件描述符 fd,通常是 3,4,5…,因为 0、1、2 被标准输入、输出、错误占用)
  • 失败:返回 -1,并且设置全局变量 errno 表示错误原因(可用 perror() 打印)
实现C接口创建文件

可以看到,文件的创建或者打开,“w”和"a"这些选项,我们可以通过系统调用的选项来完成!

【三】系统调用接口write()

write 是 Linux/Unix 系统的系统调用(直接与内核交互),用于将内存缓冲区的数据写入文件、设备或网络,是程序向外部输出数据的基础接口!

函数原型:

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

注意:write 会在写入后把偏移量更新到写入的最后一个字节的下一个位置

           如果要读取对应文件把文件偏移量使用 lseek() 移动到文件开头移动到文件开头,例如:

lseek(fd, 0, SEEK_SET); // 移动到文件开头

参数解释:

第一个参数:文件描述符(通过open()获取)

第二个参数:指向待写入数据的缓冲区的指针(const 表示函数不会修改缓冲区内容)

第三个参数:要写入的字节数(即从 buf 中取多少字节写入目标)

返回值:

成功返回写入的字节数;失败返回-1

实现C接口文件写入

【四】文件描述符

(1)文件的管理

我们知道文件的创建或者打开都是通过进程来完成的,而每个进程都会创建一个task_struct的结构对象来进行管理,在这个结构对象里面有一个 FILE* 的结构体指针,来指向一个文件描述表,这个文件描述表是一个指针数组,存放的是文件指针,来指向打开的文件,fd 即是数组的下标,例如:

在各种语言(比如C语言中),fopen()函数返回 FILE 类型的指针,和操作系统的FILE有何区别:

对比项C 语言的 FILELinux 内核的 struct file
位置 C 标准库 内核空间
可见性 对应用程序员可见 对应用程序员不可见
作用 封装内核接口间接操控文件 操作系统内部管理文件的结构
平台 任何支持 C 标准库的系统都有 仅 Linux 内核有
(2)描述符0、1、2

当我们开始用语言编写程序的时候,会默认占用文件描述表的三个位置,即stdin,stdout,stderr

stdin:类似键盘文件

stdout:类似显示器文件(用于普通输出,通常用来将不同类型的输出写到不同的文件

stderr:类似显示器文件(用于错误、异常信息输出将不同类型的输出写到不同的文件

验证:

【五】重定向

在上面我们已经知道了每个进程中文件描述表的存在,来管理打开的文件:

因为文件指针指向着文件,下标对应该文件指针的位置,所以我们如果直接移动文件指针在文件描述表的位置,就改变了该文件在描述表中的位置,操作系统根据文件描述表的下标来锁定文件的位置,当打开一个新的文件,会从上到下寻找空缺的数组位置来存放该文件指针,即该文件的位置

在系统调用接口中有这样一个接口:dup2()可以更改文件指针的下标位置

函数原型:

#include <unistd.h>

int dup2(int oldfd, int newfd);

第一、二个参数都是文件下标,将 oldfd对应的下标文件指针 覆盖到 newfd对应下标(左移到右)

返回值:成功返回 newfd,失败返回-1

(1)输出重定向

操作符号:>

作用:将输出写入文件,且覆盖之前内容(理解:改变内容输出位置为文件“—>”)

实现:搭配open()文件操作选项每次清零文件内容+dup2()接口来改变文件描述符下标

例如:

(2)追加重定向

操作符号:>>

作用:将输出写入文件,且在末尾追加(理解:改变内容输出位置为文件“—>”)

实现:搭配open()文件操作选项调整为每次追加内容+dup2()接口来改变文件描述符下标

例如:

(3)输入重定向

操作符号:<

作用:从文件读取内容输出,且在末尾追加(理解:改变内容输入的位置“<—”)

实现:搭配open()文件操作选项调整为每次追加内容+dup2()接口来改变文件描述符下标

例如:(注意使用 write() 之后用 lseek() 移动到文件开头移动到文件开头否则读取不了

【六】Linux之下一切皆文件

既然Linux需要管理底层的各种软硬件资源,比如:文件、网卡、显示器这些,是否是给每个资源都要生成一个独特的管理方式?NO,它把所有资源都套上 “文件的壳”,用同一套逻辑操作

管理实现:

Linux 内核有一层 虚拟文件系统(VFS),为不同类型的 “文件”(磁盘文件、设备、套接字等)提供统一接口。先描述再组织,以结构体多态继承方式实现统一接口,对各种硬件 / 文件系统完成管理,只要适配 VFS,就能被系统以 “文件” 的方式管理,无需修改上层逻辑!

【七】缓冲区

在之前我们有听过缓冲区,类似用来临时存储数据,下面我们看一个现象:

(1)引入

可以看到,只有系统调用可以正常打印,这是因为缓冲区的概念,我画图来解释:

小编个人理解:

向文件的写入、访问等都需要通过进程完成,频繁的访问不如一次性解决,因此缓冲区的存在可以提高性能,在语言层面也是如此,先将数据加载到自己对应的缓冲区,待得到某个信号再统一的刷新缓冲区的内容——缓冲区的优势

语言的标准库里面封装了系统调用,如图各种库函数需要先将内容写到语言的缓冲区,而系统调用函数直接访问系统的缓冲区,close会直接在系统层面关闭文件描述符对应的资源,也就无法再通过对应文件应描述符向系统缓冲区写入,自然的语言中的缓冲区更无法向显示器打印,包括在关闭了1号描述符资源之后,系统调用也无法向内核缓冲区写入,例如:

(2)语言缓冲区

缓冲区的刷新有三种形式:

(1)无缓冲、直接刷新——写一次刷新一次

(2)行刷新——比如‘\\n’,行语句刷新

(3)全缓冲刷新——缓冲区满了才刷新(例如:向文件写入)

(4)进程退出自动刷新缓冲

缓冲区的优势:

(1)解决效率问题(上面已经讲过)

(2)配合格式化(例如printf("Hello %d",2),它在缓冲区之前会完成数据替换)

缓冲区的存在位置:

库函数的调用完成一般会返回 FILE* 的指针,而 FILE 是一个结构体,数据应该是先存储在这个结构体成员里面,里面封装了文件描述符、缓冲区(字符串存储形式)………

(3)子进程与缓冲区

我们先来看下面的代码和现象:如果在最后fork()子进程,然后指向结果写入文件会发生什么

关键:代码段没有用特殊手段是可读的(真正共享),刚 fork() 时父子进程也是共享数据的,但是退出的时候父子进程一方退出导致另一方数据被修改,数据发生写时拷贝,因此打印两份缓冲区的内容;而 write() 是在 fork() 之前直接写道内核缓冲区,因此只打印一份

赞(0)
未经允许不得转载:171主机测评 » 《Linux 内核文件缓冲区:IO 性能 “悄无声息” 的优化大师》
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址