【Linux】缓冲区

发布于:2024-05-16 ⋅ 阅读:(65) ⋅ 点赞:(0)

目录

一、初识缓冲区

二、用户级缓冲区

三、内核级缓冲区

四、内核级缓冲区 VS 用户级缓冲区

 五、用户级缓冲区在哪里?


一、初识缓冲区

缓冲区是什么?可以简单理解成一部分内存。例如用户缓冲区(char arr[])、C标准库提供的缓冲区、操作系统提供的缓冲区。

为什么要有缓冲区?要通过减少对磁盘的直接访问来提高文件操作的效率。例如给朋友寄快递,不是直接给朋友送过去,而是通过快递站,快递站也不是来一个包裹就送一个包裹,而是积累一部分包裹再统一发送,朋友只用到他对应的快递站取走包裹即可。提高了使用者的效率和发送的效率。

缓冲区能暂存数据,因此也要有一定的刷新方式。(一般策略)

  1. 无缓冲(立即刷新)
  2. 行缓冲(行刷新)
  3. 全缓冲(缓冲区满了,再刷新)
  4. 一般对于显示器文件,行刷新(行缓冲) ;对于磁盘上的文件,全缓冲(缓冲写满,在刷新)

特殊情况:

  1. 强制刷新 
  2. 进程退出的时候,一般要自动刷新缓冲区,即便数据没有满足刷新条件。

强制刷新缓冲区,把当前缓冲区的内容全部刷新。类似于寄快递时对工作人员说:赶快给我发出去,不然就投诉你。这时工作人员就会把驿站当前积累的包裹都发送出去。行刷新,就是把驿站快递柜的一行快递发送。全刷新,就是驿站快递柜满了,把积累的全部包裹发送出去。

看下面一段代码思考结果如何,输出重定向到文件中,结果一样吗?

#include <stdio.h>    
#include <string.h>    
#include <unistd.h>    
    
int main()    
{    
    printf("C: hello printf\n");    
    fprintf(stdout, "C: hello fprintf\n");    
    fputs("C: hello fputs\n",stdout);                                                                            
    const char* str = "system call: hello write\n";    
    write(1,str,strlen(str));    
    
    fork();    
    return 0;    
}  

现象:直接执行程序时向显示器打印,C标准库和系统调用的函数都打印一次,但输出重定向到 log.txt 文件时,C标准库提供的函数打印两次,系统调用的函数只打印一次。

原因:

  1. 当我们直接向显示器打印的时候,显示器文件的刷新方式是行刷新!而且代码输出的所有字符串,都有'\n'。fork之前,数据全部已经被刷新,包括systemcall。
  2. 重定向到 log.txt ,本质是向磁盘文件中写入(不是显示器),系统对于数据的刷新方式已经由行缓冲,变成了全缓冲!
  3. 变成全缓冲意味着缓冲区变大,实际写入的简单数据,不足以把缓冲区写满,fork执行的时候数据依旧在缓冲区中!
  4. 我们目前所谈的"缓冲区",和操作系统是没有关系的,只能和C语言有关。(因为系统调用的结果打印正常符合预期,且C 库中的printf、fprintf、fputs实际上还是封装的wirte,如果是因为write引起的,write函数也会打印两次)

二、用户级缓冲区

我们日常用的最多的其实是C/C++标准库提供的语言级别的缓冲区,也叫用户级缓冲区。而操作系统提供的是内核级缓冲区。
在上面的例子中,我们这边的快递站就是C语言提供的用户级缓冲区,朋友就是操作系统,朋友那边的快递站就是内核级缓冲区。

上面代码原理的补充:

  • C/C++标准库提供的缓冲区,里面一定保存的是用户的数据,属于当前进程在运行时的数据。
  • 如果把用户的数据交给操作系统,那么这个数据不属于用户,属于操作系统。
  • 进程退出刷新缓冲区时,要进行刷新缓冲区,这也属于 "清空" 或 "写入" 操作。
  • C/C++缓冲区的数据属于进程的数据、fork后父子进程共享缓冲区数据,当要写入或者任一进程退出时,要刷新缓冲区,此时会发生 "写时拷贝"。
  • 可以证明write系统调用没有使用C的缓冲区,直接写入到操作系统。这个数据不属于进程。

什么叫做刷新?

一个进程有对应的文件描述符表,例如有一个打开的log.txt文件,它加载到内存中,对应一个struct file,文件描述符为3,对应一个文件缓冲区。磁盘上也有一个对应的log.txt。

用户要把"hello"字符串打印到log.txt文件,该字符串是经过fprintf等函数写入到C语言提供的缓冲区(例如名叫buffer),经过fflush或进程退出时,调用write(3,buffer)写入到文件缓冲区中。

把用户的缓冲区(例如字符数组)拷贝到C语言的缓冲区,再把C语言提供的缓冲区的内容拷贝到文件缓冲区,这个过程叫做刷新。

补充:

  • fprintf等函数不仅将打印内容拷贝到缓冲区中,还做了格式化输出操作。这个格式化输出操作是在拷贝时完成的。我们之前用的所有缓冲区都是C语言提供的用户级缓冲区。
  • 直接把数据从用户的缓冲区拷贝到文件缓冲区,成本很高,刷新操作可以提高效率
  • C语言中几乎所有的I/O接口都有缓冲区。
  • 文件缓冲区中的数据需要写入文件系统时,数据再被写入到文件系统的磁盘块中。这也是为了提高效率(减少磁盘访问)。(该操作由OS执行)

经过上面的分析,就可以更好的理解用户级缓冲区的特点:

  • 用户级缓冲区通常位于用户空间,由进程自己管理。
  • 用户级缓冲区的主要目的是提高文件操作的效率,减少系统调用的次数。
    例如,当写入文件时,数据首先被写入用户级缓冲区,然后写入内核级缓冲区,最后写入文件系统。
  • 用户级缓冲区可以被刷新,使用 'fflush' 函数可以手动刷新缓冲区,确保所有数据都被写入文件。
  • C标准库(如stdio.h)提供了文件操作的函数,如 'fopen'、'fread'、'fwrite' 等。这些函数可以在打开文件时指定缓冲区类型,如 '"w+b"'。

以下是C语言提供的几种用户级缓冲区类型:

  • 无缓冲区(无缓冲):使用 '"wb"' 或 '"rb"' 模式打开文件时,文件没有缓冲区。每次调用写入或读取函数时,数据都会立即写入或从文件中读取。
  • 行缓冲区(行缓冲):使用 '"w+b"' 或 '"r+b"' 模式打开文件时,文件有行缓冲区。每写入或读取一行数据时,数据会被缓冲,直到缓冲区满或遇到换行符。
  • 全缓冲区(全缓冲):使用 '"w+"' 或 '"r+"' 模式打开文件时,文件有全缓冲区。写入或读取的数据会被缓冲,直到缓冲区满或调用 'fflush' 函数。 

三、内核级缓冲区

上面讲到的文件缓冲区就是操作系统的内核级缓冲区
内核缓冲区通常位于内核空间,由操作系统直接管理。
标准输出(stdout)和标准错误(stderr)通常使用内核级缓冲区。
内核级缓冲区通常在进程结束时自动刷新,但在某些情况下,可以通过调用 'fflush' 函数来手动刷新这些缓冲区。
内核级缓冲区的主要目的是减少对物理设备的直接访问,提高系统性能。

用户级缓冲区和内核级缓冲区可以同时使用,以提高程序的性能。例如,当读取文件时,数据首先被读取到用户级缓冲区中,然后从用户级缓冲区读取到程序的内存中。当写入文件时,数据首先被写入程序的内存中,然后写入用户级缓冲区,最后写入内核级缓冲区,最终写入文件系统。

四、内核级缓冲区 VS 用户级缓冲区

  • 作用:内核级缓冲区主要用于减少对物理设备的直接访问,提高系统性能;而用户级缓冲区主要用于提高文件操作的效率,减少系统调用的次数。
  • 位置:内核级缓冲区位于内核空间,由操作系统管理;用户级缓冲区位于用户空间,由程序自己管理。
  • 管理:内核级缓冲区由内核直接管理,对用户不可见;用户级缓冲区由程序员控制,可以通过编程方式进行管理。
  • 性能:内核级缓冲区可以显著减少磁盘I/O操作,提高系统性能;用户级缓冲区可以减少系统调用的次数,提高程序的执行效率。

缓冲区在Linux中的主要作用:

  1. 减少磁盘访问:缓冲区允许数据在内存中暂存,而不是每次操作都直接从磁盘读取或写入。这减少了磁盘I/O操作的次数,从而提高了系统的整体性能。
  2. 优化内存使用:缓冲区可以存储大量数据,减少了对物理内存的需求。数据在内存中缓存,可以在需要时快速访问,而不是每次都从磁盘加载。
  3. 减少系统调用次数:缓冲区允许在多个操作之间缓存数据,减少了与文件系统交互的系统调用次数。这减少了内核和用户空间之间的上下文切换,提高了程序的执行效率。
  4. 提高文件操作的效率:缓冲区可以减少对文件的频繁读写,从而提高了文件操作的效率。例如,当编辑一个大型文件时,文本编辑器可以在缓冲区中修改数据,而不是每次修改都写入文件。
  5. 减少延迟:缓冲区可以减少数据传输的延迟,因为数据可以快速地在内存中传递。这使得程序可以更快速地处理数据,尤其是在处理大量数据时。
  6. 支持并行操作:缓冲区可以支持并行操作,例如,多个进程可以同时访问同一个缓冲区。这使得系统可以更有效地利用多核处理器和多进程架构。

 五、用户级缓冲区在哪里?

任何情况下,我们输出输入的时候,都要有一个FILE,FILE是一个结构体,FILE里面包含了fd,也提供了一段缓冲区!因此任何一个文件都要在C标准库里通过FILE创建一个用户级缓冲区。
例如进程打开了5个文件,就有5个FILE对象和struct file,对应5个文件缓冲区,读写时互相不影响。

可以看看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
};

向显示器打印123的时候,打印的其实是1字符2字符3字符,我们用键盘输入123时,也是输入1字符2字符3字符,这3个字符被放入到stdin对应FILE结构体中的缓冲区。scanf将缓冲区的数据合并,根据用户定义的格式赋给指定的值。键盘显示器也叫字符设备。

C语言把打开的文件叫做流,输入流输出流,流也相当于C语言提供的缓冲区,一方面C语言从缓冲区的头部拿数据,一方面用户在缓冲区尾部追加数据,该操作像流水一样,所以称作‘流’。C++也是同理,也有自己的缓冲区。原理和C相同,但操作上有很大区别。