探索数据交互的奥秘:深入剖析缓冲区的工作原理与应用场景

server/2025/3/17 11:46:25/

目录

1.理解“一切皆文件

2.缓冲区

2.1 什么是缓冲区

2.2 为什么要引⼊缓冲区机制

2.3 解释一个遗留问题

2.4 缓冲类型

2.5 FILE 

3.简单设计⼀下libc库


1.理解“一切皆文件

   ⾸先,在windows中是⽂件的东西,它们在linux中也是文件;其次⼀些在windows中不是⽂件的东西,⽐如进程、磁盘、显⽰器、键盘这样硬件设备也被抽象成了文件,你可以使⽤访问⽂件的⽅法访问它们获得信息;甚⾄管道,也是文件;将来我们要学习⽹络编程中的socket(套接字)这样的东西,使用的接口跟⽂件接口也是⼀致的。

   这样做最明显的好处是,开发者仅需要使⽤⼀套 API 和开发⼯具,即可调取 Linux 系统中绝⼤部分的资源。举个简单的例⼦,Linux 中⼏乎所有读(读⽂件,读系统状态,读PIPE)的操作都可以⽤read 函数来进⾏;⼏乎所有更改(更改⽂件,更改系统参数,写 PIPE)的操作都可以⽤ write 函数来进⾏。 

   之前我们讲过,当打开⼀个⽂件时,操作系统为了管理所打开的⽂件,都会为这个⽂件创建⼀个file结构体,该结构体定义在 /usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fs.h 下,以下展⽰了该结构部分我们关系的内容:

struct file {
...
struct inode
*f_inode;
/* cached value */
const struct file_operations
*f_op;
...
atomic_long_t
f_count;
// 表⽰打开⽂件的引⽤计数,如果有多个⽂件指针指向
它,就会增加f_count的值。
unsigned int
f_flags;
// 表⽰打开⽂件的权限
fmode_t
f_mode;
// 设置对⽂件的访问模式,例如:只读,只写等。所有
的标志在头⽂件<fcntl.h> 中定义
loff_t
f_pos;
// 表⽰当前读写⽂件的位置
...
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */

     值得关注的是 struct file 中的 f_op 指针指向了⼀个 file_operations 结构体,这个结构
体中的成员除了struct module* owner 其余都是函数指针。该结构和 struct file 都在fs.h下。 

struct file_operations {
struct module *owner;
//指向拥有该模块的指针;
loff_t (*llseek) (struct file *, loff_t, int);
//llseek ⽅法⽤作改变⽂件中的当前读/写位置, 并且新位置作为(正的)返回值.
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
//⽤来从设备中获取数据. 在这个位置的⼀个空指针导致 read 系统调⽤以 -
EINVAL("Invalid argument") 失败. ⼀个⾮负返回值代表了成功读取的字节数( 返回值是⼀个
"signed size" 类型, 常常是⽬标平台本地的整数类型).
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
//发送数据给设备. 如果 NULL, -EINVAL 返回给调⽤ write 系统调⽤的程序. 如果⾮负, 返
回值代表成功写的字节数.
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long,
loff_t);
//初始化⼀个异步读 -- 可能在函数返回前不结束的读操作.
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long,
loff_t);
//初始化设备上的⼀个异步写.
int (*readdir) (struct file *, void *, filldir_t);
//对于设备⽂件这个成员应当为 NULL; 它⽤来读取⽬录, 并且仅对**⽂件系统**有⽤.
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
//mmap ⽤来请求将设备内存映射到进程的地址空间. 如果这个⽅法是 NULL, mmap 系统调⽤返
回 -ENODEV.
int (*open) (struct inode *, struct file *);
//打开⼀个⽂件
int (*flush) (struct file *, fl_owner_t id);
//flush 操作在进程关闭它的设备⽂件描述符的拷⻉时调⽤;
int (*release) (struct inode *, struct file *);
//在⽂件结构被释放时引⽤这个操作. 如同 open, release 可以为 NULL.
int (*fsync) (struct file *, struct dentry *, int datasync);
//⽤⼾调⽤来刷新任何挂着的数据.
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
//lock ⽅法⽤来实现⽂件加锁; 加锁对常规⽂件是必不可少的特性, 但是设备驱动⼏乎从不实现
它.
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *,
int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned
long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *,
size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *,
size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
};

file_operation 就是把系统调⽤和驱动程序关联起来的关键数据结构,这个结构的每⼀个成员都
对应着⼀个系统调⽤。读取 file_operation 中相应的函数指针,接着把控制权转交给函数,从⽽
完成了Linux设备驱动程序的⼯作。

介绍完相关代码,⼀张图总结:

  上图中的外设,每个设备都可以有⾃⼰的read、write,但⼀定是对应着不同的操作⽅法!!但通过struct file 下 file_operation 中的各种函数回调,让我们开发者只用file便可调取 Linux 系
统中绝大部分的资源
!!这便是“linux下⼀切皆⽂件”的核心理解。 

2.缓冲区

2.1 什么是缓冲区

缓冲区是内存空间的⼀部分。也就是说,在内存空间中预留了⼀定的存储空间,这些存储空间⽤来缓冲输⼊或输出的数据,这部分预留的空间就叫做缓冲区缓冲区根据其对应的是输⼊设备还是输出设备,分为输⼊缓冲区和输出缓冲区

2.2 为什么要引⼊缓冲区机制

读写⽂件时,如果不会开辟对⽂件操作的缓冲区,直接通过系统调⽤对磁盘进⾏操作(读、写等),那么每次对⽂件进⾏⼀次读写操作时,都需要使⽤读写系统调⽤来处理此操作,即需要执⾏⼀次系统调⽤,执⾏⼀次系统调⽤将涉及到CPU状态的切换,即从⽤⼾空间切换到内核空间,实现进程上下⽂的切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执⾏效率造成很⼤的影响。
为了减少使⽤系统调⽤的次数,提⾼效率,我们就可以采⽤缓冲机制。⽐如我们从磁盘⾥取信息,可以在磁盘⽂件进⾏操作时,可以⼀次从⽂件中读出⼤量的数据到缓冲区中,以后对这部分的访问就不需要再使⽤系统调⽤了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作⼤⼤快于对磁盘的操作,故应⽤缓冲区可⼤⼤提⾼计算机的运⾏速度。⼜⽐如,我们使⽤打印机打印⽂档,由于打印机的打印速度相对较慢,我们先把⽂档输出到打印机相应的缓冲区,打印机再⾃⾏逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是⼀块内存区,它⽤在输⼊输出设备和CPU之间,⽤来缓存数据。它使得低速的输⼊输出设备和⾼速的CPU能够协调⼯作,避免低速的输⼊输出设备占⽤CPU,解放出CPU,使其能够⾼效率⼯作。

2.3 解释一个遗留问题

还记得这个问题吗:

  因为它是属于缓冲区的知识,缓冲区需要大量的文字去描述,所以将它遗留下来,现在我们们讲到了缓冲区就可以来将它解释清楚了。

为了更好地解释,我们再写一份代码:

这次依然是写入之后将文件关掉,但是不同的是,上一次我们只使用了库函数写入,这一次我们增加了系统调用写入,编译运行来看结果:

这次有内容了,我们看看是什么内容:

不出意外是系统调用写入的内容,那么这又是怎么一回事呢?为什么使用库函数写入文件,然后立即关闭内容不会写入文件,而为什么使用系统调用就能写入呢?

答:”语言层会提供一个语言层用户级的缓冲区,而文件自己也会有一个文件内核级缓冲区,当我们使用库函数写入内容时,会写入到用户级的缓冲区中,而用户级缓冲区要写内容到文件内核缓冲区中必须要刷新,而要刷新又要满足下列三个条件其中一个:(1)强制刷新,(2)刷新条件满足,(3)进程退出,恰好我们上述的例子没有没有满足这三个条件中的任何一个。

(1)强制刷新需要使用fflush函数。

(2)刷新条件一般是要满足用户级缓冲区内容达到一定程度才会刷新,比如有些平台要缓冲区写满才会刷新。

(3)一般函数返回值返回才算进程开始退出,而我们在返回值返回之前就将文件关闭了,也就不算退出,没有满足刷新条件。为什么要进程退出才刷新呢,因为缓冲区刷新是操作系统在负责,进程退出之后会返回fd,操作系统调用write系统调用拿着对应的fd将内容从语言层提供的缓冲区刷新到文件文件内核缓冲区中,这才算是将内容写入成功了。我们上述就是进程没有退出就将fd关闭了,操作系统拿不到对应的fd,所以无法完成刷新。

剩下一些内容我们后面再讲。

2.4 缓冲类型

标准I/O提供了3种类型的缓冲区

• 全缓冲区:这种缓冲⽅式要求填满整个缓冲区后才进⾏I/O系统调⽤操作。对于磁盘⽂件的操作通常使⽤全缓冲的⽅式访问。
• ⾏缓冲区:在⾏缓冲情况下,当在输⼊和输出中遇到换⾏符时,标准I/O库函数将会执⾏系统调⽤操作。当所操作的流涉及⼀个终端时(例如标准输⼊和标准输出),使⽤⾏缓冲⽅式。因为标准I/O库每⾏的缓冲区⻓度是固定的,所以只要填满了缓冲区,即使还没有遇到换⾏符,也会执⾏I/O系统调⽤操作,默认⾏缓冲区的⼤⼩为1024。
• ⽆缓冲区:⽆缓冲区是指标准I/O库不对字符进⾏缓存,直接调⽤系统调⽤。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显⽰出来。

除了上述列举的默认刷新⽅式,下列特殊情况也会引发缓冲区的刷新:

1. 缓冲区满时;
2. 执⾏flush语句;

⽰例如下:

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 0;
}
printf("hello world: %d\n", fd);
close(fd);
return 0;
}

我们本来想使⽤重定向思维,让本应该打印在显⽰器上的内容写到“log.txt”⽂件中,但我们发现,
程序运⾏结束后,⽂件中并没有被写⼊内容:

[hyb@VM-8-12-centos buffer]$ ./myfile
[hyb@VM-8-12-centos buffer]$ ls
log.txt makefile myfile myfile.c
[hyb@VM-8-12-centos buffer]$ cat log.txt
[hyb@VM-8-12-centos buffer]$

这是由于我们将1号描述符重定向到磁盘⽂件后,缓冲区的刷新⽅式成为了全缓冲。⽽我们写⼊的内容并没有填满整个缓冲区,导致并不会将缓冲区的内容刷新到磁盘⽂件中。怎么办呢?可以使⽤fflush强制刷新下缓冲区

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 0;
}
printf("hello world: %d\n", fd);
fflush(stdout);
close(fd);
return 0;
}

还有⼀种解决⽅法,刚好可以验证⼀下stderr是不带缓冲区的,代码如下:

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
close(2);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 0;
}
perror("hello world");
close(fd);
return 0;
}

这种⽅式便可以将2号⽂件描述符重定向⾄⽂件,由于stderr没有缓冲区,“hello world”不⽤fflash
就可以写⼊⽂件:

[hyb@VM-8-12-centos buffer]$ ./myfile
[hyb@VM-8-12-centos buffer]$ cat log.txt
hello world: Success

2.5 FILE 

• 因为IO相关函数与系统调⽤接⼝对应,并且库函数封装系统调⽤,所以本质上,访问⽂件都是通
过fd访问的。
• 所以C库当中的FILE结构体内部,必定封装了fd。

来段代码在研究⼀下:

#include <stdio.h>
#include <string.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}

运⾏出结果:

hello printf
hello fwrite
hello write

但如果对进程实现输出重定向呢? ./hello > file , 我们发现结果变成了:

hello write
hello printf
hello fwrite
hello printf
hello fwrite

我们发现 printf 和 fwrite (库函数)都输出了2次,⽽ write 只输出了⼀次(系统调⽤)。为
什么呢?肯定和fork有关!

• ⼀般C库函数写⼊⽂件时是全缓冲的,⽽写⼊显示器是⾏缓冲。
• printf fwrite 库函数+会⾃带缓冲区(进度条例⼦就可以说明),当发⽣重定向到普通⽂
件时,数据的缓冲⽅式由⾏缓冲变成了全缓冲。
• ⽽我们放在缓冲区中的数据,就不会被⽴即刷新,甚⾄fork之后
• 但是进程退出之后,会统⼀刷新,写⼊⽂件当中。
• 但是fork的时候,⽗⼦数据会发⽣写时拷⻉,所以当你⽗进程准备刷新的时候,⼦进程也就有了同样的⼀份数据,随即产⽣两份数据。
• write 没有变化,说明没有所谓的缓冲。 

综上: printf fwrite 库函数会⾃带缓冲区,而write 系统调⽤没有带缓冲区。另外,我们这
⾥所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。 

那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调⽤,库函数在系统调⽤的
“上层”, 是对系统调⽤的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,⾜以说
明,该缓冲区是⼆次加上的,⼜因为是C,所以由C标准库提供。

如果有兴趣,可以看看FILE结构体:

typedef struct _IO_FILE FILE; 在/usr/include/stdio.h

在/usr/include/libio.h
struct _IO_FILE {
int _flags;
/* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr;
/* Current read pointer */
char* _IO_read_end;
/* End of get area. */
char* _IO_read_base;
/* Start of putback+get area. */
char* _IO_write_base;
/* Start of put area. */
char* _IO_write_ptr;
/* Current put pointer. */
char* _IO_write_end;
/* End of put area. */
char* _IO_buf_base;
/* Start of reserve area. */
char* _IO_buf_end;
/* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //封装的⽂件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

3.简单设计⼀下libc库

my_stdio.h:

$ cat my_stdio.h
#pragma once
#define SIZE 1024
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2
struct IO_FILE
{
int flag; // 刷新⽅式
int fileno; // ⽂件描述符
char outbuffer[SIZE];
int cap;
int size;
// TODO
};
typedef struct IO_FILE mFILE;
mFILE *mfopen(const char *filename, const char *mode);
int mfwrite(const void *ptr, int num, mFILE *stream);
void mfflush(mFILE *stream);
void mfclose(mFILE *stream);

my_stdio.c:

$ cat my_stdio.c
#include "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
mFILE *mfopen(const char *filename, const char *mode)
{
int fd = -1;
if(strcmp(mode, "r") == 0)
{
fd = open(filename, O_RDONLY);
}
else if(strcmp(mode, "w")== 0)
{
fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);
}
else if(strcmp(mode, "a") == 0)
{
fd = open(filename, O_CREAT|O_WRONLY|O_APPEND, 0666);
}
if(fd < 0) return NULL;
mFILE *mf = (mFILE*)malloc(sizeof(mFILE));
if(!mf)
{
close(fd);
return NULL;
}
mf->fileno = fd;
mf->flag = FLUSH_LINE;
mf->size = 0;
mf->cap = SIZE;
return mf;
}
void mfflush(mFILE *stream)
{
if(stream->size > 0)
{
// 写到内核⽂件的⽂件缓冲区中!
write(stream->fileno, stream->outbuffer, stream->size);
// 刷新到外设
fsync(stream->fileno);
stream->size = 0;
}
}
int mfwrite(const void *ptr, int num, mFILE *stream)
{
// 1. 拷⻉
memcpy(stream->outbuffer+stream->size, ptr, num);
stream->size += num;
// 2. 检测是否要刷新
if(stream->flag == FLUSH_LINE && stream->size > 0 && stream-
>outbuffer[stream->size-1]== '\n')
{
mfflush(stream);
}
return num;
}
void mfclose(mFILE *stream)
{
if(stream->size > 0)
{
mfflush(stream);
}
close(stream->fileno);
}

 main.c:

$ cat main.c
#include "my_stdio.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
mFILE *fp = mfopen("./log.txt", "a");
if(fp == NULL)
{
return 1;
}
int cnt = 10;
while(cnt)
{
printf("write %d\n", cnt);
char buffer[64];
snprintf(buffer, sizeof(buffer),"hello message, number is : %d", cnt);
cnt--;
mfwrite(buffer, strlen(buffer), fp);
mfflush(fp);
sleep(1);
}
mfclose(fp);
}

本章完。 


http://www.ppmy.cn/server/175688.html

相关文章

Linux系统移植篇(二)Uboot 顶层 Makefile 解析

在之前的文章&#xff0c;我们经过编译&#xff0c;配置工具链&#xff0c;编译好了uboot的内容&#xff0c;呈现的工程结构如下所示&#xff1b; 具体的内容作用也就不做过多的解析了&#xff0c;读者可以根据内容单个去查。 在阅读 uboot 源码之前&#xff0c;肯定是要先看一…

Carto 无尽旅图 for Mac v1.0.7.6 (51528)冒险解谜游戏 支持M、Intel芯片

游戏介绍 《Carto》源于英文"Cartographer"&#xff08;制图师&#xff09;&#xff0c;卡朵不慎坠入未知世界。这里蜿蜒曲折&#xff0c;地形丰富。作为制图师卡朵&#xff0c;你将用你自己的神秘力量&#xff0c;操纵地图颠覆世界&#xff0c;将其翻转、拼合。当世…

JavaScript 中 call 和 apply 的用法与区别

文章目录 前言一、 call 方法1.1 基本用法1.2 传递多个参数 二、apply 方法2.1 基本用法2.2 传递数组参数 三、call 和 apply 的区别四、实际应用场景4.1 借用方法4.2 继承与构造函数 五、总结 前言 在 JavaScript 中&#xff0c;call 和 apply 是两个非常重要的函数方法&…

DeepSeek + Excel:数据处理专家 具体步骤

将DeepSeek与Excel结合使用&#xff0c;可显著提升数据处理效率&#xff0c;实现智能化的数据分析、清洗、计算及可视化。以下是具体操作步骤及核心技巧的综合指南&#xff1a; 一、接入DeepSeek的两种主要方法 1. 插件接入法&#xff08;推荐&#xff09; 步骤1&#xff1a;…

【Linux终端探幽】—基础指令集(中):迷宫寻宝

目录 rmdir与rm&#xff1a;清理迷宫的碎石与陷阱—— 扫除废弃路径的爆破术 man&#xff1a;迷宫探险的古老指南书—— 用咒语解锁指令的隐藏说明 cp&#xff1a;复制迷宫地图的影分身术—— 从一扇门到另一扇门的镜像魔法 mv&#xff1a;移动宝藏的隐形搬运工—— 路径重…

VSCode通过SSH免密远程登录Windows服务器

系列 1.1 VSCode通过SSH远程登录Windows服务器 1.2 VSCode通过SSH免密远程登录Windows服务器 文章目录 系列1 准备工作2 本地电脑配置2.1 生成密钥2.2 VS Code配置密钥 3. 服务端配置3.1 配置SSH服务器sshd_config3.2 复制公钥3.3 配置权限&#xff08;常见问题&#xff09;3.…

[oeasy]python074_ai辅助编程_水果程序_fruits_apple_banana_加法_python之禅

074_ai辅助编程_水果程序_fruits_加法 回忆上次内容 上次直接从模块中导入变量、函数 from my_file import pi 导入my_file.pi 并作为 pi 使用 from my_file import pi as my_pi 导入变量 并 重命名 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; …

JetBrains(全家桶: IDEA、WebStorm、GoLand、PyCharm) 2024.3+ 2025 版免费体验方案

JetBrains&#xff08;全家桶: IDEA、WebStorm、GoLand、PyCharm&#xff09; 2024.3 2025 版免费体验方案 前言 JetBrains IDE 是许多开发者的主力工具&#xff0c;但从 2024.02 版本起&#xff0c;JetBrains 调整了试用政策&#xff0c;新用户不再享有默认的 30 天免费试用…