王海鱼 发表于 2024-7-20 12:42:56

【Linux】基础I/O——FILE,用户缓冲区

1.FILE里的fd

        FILE是C语言定义的文件布局体,里面包罗了各种文件信息。可以肯定的一点是,FILE布局体内一定封装了 fd 。为什么?来看接下来的思路分析:
   1.使用系统接口的必然性
         文件存储在磁盘上,属于外设。谁有权限访问外设呢?只有操作系统。因为操作系统对上要提供稳定的服务,对下要管理好各种软硬件资源。
         假如文件操作能绕开操作系统,那么操作系统怎么知道某个文件到底有没有被创建,有没有被烧毁呢,还怎么给你提供稳定的服务呢?基于上述简单的熟悉,我们不难理解,要想访问硬件资源,就必须通过操作系统。
         而操作系统出于安全性和减少使用成本的角度考虑,是不相信托何人的。就像银行一样,不会将金库直接向大众开放,而是只会有几个业务窗口为大家提供服务。操作系统也是如许,操作系统提供的窗口就是系统接口。
         至此通过我们的逻辑推演,我们已经可以得出以下的结论:要想访问外设就必须使用操作系统提供的系统接口。所以C语言的各种文件操作函数本质就是对系统接口的封装

2.FILE布局体封装fd的必然性
 C语言的文件操作都是系统统接口的封装,而系统接口的使用只认fd,因此FILE布局体中必然会封装fd

验证的方法也很简单直接:
https://img-blog.csdnimg.cn/direct/04af0feb1f9845f38c7db6dc6d886bef.png
FILE究竟是个什么东西呢?是一个c语言提供的布局体类型
我们在/usr/include/stdio.h头文件中可以看到下面这句代码,也就是说FILE现实上就是struct _IO_FILE布局体的一个别名。
typedef struct _IO_FILE FILE;
 而我们在/usr/include/libio.h头文件中可以找到struct _IO_FILE布局体的定义,在该布局体的浩繁成员当中,我们可以看到一个名为_fileno的成员,这个成员现实上就是封装的文件描述符。
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;

        /*char* _save_gptr;char* _save_egptr; */

        _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};


2.用户缓冲区

我们看一段代码
例1

#include <stdio.h>
#include<string.h>
#include<unistd.h>

int main()
{
        const char* fstr = "hello fwrite\n";
        //都是往屏幕打印
        printf("hello printf\n");
        fprintf(stdout,"hello fprintf\n");
        fwrite(fstr, strlen(fstr), 1, stdout);//第二个是块大小,第三个是块个数

        const char* str = "hello write\n";
        write(1, str, strlen(str));
} https://i-blog.csdnimg.cn/direct/8c87b9f94d3b43e5827db53a91e60029.png
符合我们的预期
我们将结果重定向到log.txt里面去
https://i-blog.csdnimg.cn/direct/3fb7255753564f75a27b887b6b8cdb38.png
嗯?write怎么在前面了?
 我们接着看一下代码
例2

#include <stdio.h>
#include<string.h>
#include<unistd.h>

int main()
{
        const char* fstr = "hello fwrite\n";
        //都是往屏幕打印
        printf("hello printf\n");
        fprintf(stdout,"hello fprintf\n");
        fwrite(fstr, strlen(fstr), 1, stdout);//第二个是块大小,第三个是块个数

        close(1);
} https://i-blog.csdnimg.cn/direct/88eea6d35d0e4baf974eff2802bea947.png
还能打印
我们再看看代码
例3

#include <stdio.h>
#include<string.h>
#include<unistd.h>

int main()
{
        const char* fstr = "hello fwrite\n";
       
        printf("hello printf\n");
        fprintf(stdout,"hello fprintf\n");
        fwrite(fstr, strlen(fstr), 1, stdout);//第二个是块大小,第三个是块个数

        const char* str = "hello write\n";
        write(1, str, strlen(str));

    fork();
} https://i-blog.csdnimg.cn/direct/c69448f9ee2348c4ba4847ebdf9d76f9.png
打在屏幕也挺正常的
但是,当我们将步伐的结果重定向到log.txt文件当中后,我们发现文件当中的内容与我们直接打印输出到表现器的内容是不一样的。
https://i-blog.csdnimg.cn/direct/d4c54e35c5e545e591f0928702368f82.png

   同样一个步伐,为什么C库函数printf和fwrite,fprintf打印的内容重定向到文件后就变成了两份,而系统接口write打印的内容照旧原来的一份呢?
如今讲不明白,我们先看一些例子
例4

我们把\n都去掉
#include <stdio.h>
#include<string.h>
#include<unistd.h>

int main()
{
        const char* fstr = "hello fwrite";
        //都是往屏幕打印
        printf("hello printf");
        fprintf(stdout,"hello fprintf");
        fwrite(fstr, strlen(fstr), 1, stdout);//第二个是块大小,第三个是块个数
       
        close(1);
} https://i-blog.csdnimg.cn/direct/4fbb40a588874230b9637f7c9c5812d4.png
嗯?为啥啥也没有??
为什么加上\n就有(例2),不加\n就什么也不打印(例4)?
我们接着看例子
例5

#include <stdio.h>
#include<string.h>
#include<unistd.h>

int main()
{

        const char* str = "hello write";
        write(1, str, strlen(str));

        close(1);
} https://i-blog.csdnimg.cn/direct/83eb70cb7369422792419c19c5eec2db.png
嗯?为什么它能打出来 ?
首先我们要知道这些C式接口——printf,fprintf,fwrite都是调用了系统调用write,但是在例4,例5中只有write打印了出来,为什么?
我们可以这么猜测一下:我们在调用printf,fprintf,fwrite时那些字符串已经写进缓冲区了,而write没有缓冲区,直接打印出来了,而且这个缓冲区还不是系统级的缓冲区

3.为什么要有缓冲区(节省进程IO数据的时间)

        缓冲区是一种用来暂时存储输入或输出数据的内存空间,它可以减少对磁盘或其他低速设备的读写次数,提高计算机的运行服从。缓冲区有三种类型:全缓冲、行缓冲和无缓冲,它们分别在不同的条件下进行现实的I/O操作。缓冲区也可以通过一些函数来设置或刷新。
        我们知道,假如直接将内存中的数据写到磁盘文件中,非常的斲丧时间,因为磁盘是外设,外设和内存的速度相比差距非常大,如许子内存直接读取数据的服从就会非常低,这个时间在内存中就会开发一段空间,这段空间就是缓冲区,进程会将内存中的数据拷贝到缓冲区里,最后再从缓冲区中将数据输入到磁盘外设里。所以缓冲区的意义现实上就是为了节省进程进行数据IO的时间。
        进程将内存中的数据拷贝到缓冲区,这句话大概有些晦涩难懂,但现实上这个工作就是printf,fprintf,fwrite等c式函数做的,与其说printf,fprintf,fwrite等c式函数是写入到文件的函数,倒不如理解成是拷贝函数,将数据从进程拷贝到“缓冲区”大概“外设”中!!!
        别的,缓冲区可以配及格式化输出函数把数据写成正确的格式
4.语言级缓冲区的刷新策略(c缓冲区使用的策略)

           假如有一块数据想要写入到外设中,是一次性将这么多的数据写到外设中服从高,照旧将这么多的数据多次少批量的写入到外设中服从高呢?
        答案显而易见,当然是前者,因为相较于CPU和内存的访问速度,外设的访问速度非常的慢的,假设数据output到表现器外设的时间是1s,那么大概990ms的时间都在等待表现器就绪,10ms的时间就已经完成数据的准备工作了,所以访问一个外设是非常辛劳的。
缓冲区一定会结合具体的设备,定制自己的刷新策略:
        语言级缓冲区的刷新策略是指在使用C语言等高级语言进行输入输出操作时,缓冲区何时将数据真正地传送到目标设备或文件的规则。根据不同的设备或文件类型,语言级缓冲区有以下三种刷新策略:

[*]全缓冲:只有当缓冲区被填满时才进行现实的I/O操作,这种策略一般用于对磁盘文件的读写,可以减少磁盘的访问次数,提高服从。
[*]行缓冲:只有当在输入或输出中遇到换行符时才进行现实的I/O操作,这种策略一般用于标准输入流(stdin)和标准输出流(stdout),可以保证每行数据都实时地表现或读取,提高用户体验。
[*]无缓冲:不使用缓冲区,每次输入或输出都直接进行现实的I/O操作,这种策略一般用于标准错误输出流(stderr),可以使得出错信息尽快地反馈给用户,方便调试。
除了以上三种刷新策略外,另有两种特殊环境会导致缓冲区的刷新:

[*]用户逼迫刷新:使用fflush函数或雷同的操作来显式地清空缓冲区,不管缓冲区是否已满或遇到换行符。
[*]进程退出时:作为main函数return操作的一部分,缓冲区会被自动刷新,以保证所有数据都被正确地传送到目标设备或文件。
4.1.应用场景

无缓冲:
        一般环境下,立刻刷新如许的场景非常少,好比表现错误信息的时间,例如发生标准错误的时间,编译器会立刻将错误信息输出到表现器文件上,也就是外设当中,而不是将信息先存放到缓冲区当中,应当是立刻刷新到表现器文件中。
行缓冲:
        我们知道带\n时数据就会立马表现到表现器上,而不带\n时,就只能通过fflush的方法来刷新数据。上面我们所说的缓冲区数据积聚满之后在刷新,本身就是服从很高的刷新策略,
   那为什么表现器的刷新策略是行缓冲而不是全缓冲呢?
        是因为表现器设备太特殊了,表现器不是给其他设备或机器看的,而是给人看的,而人的阅读风俗就是从左向右按照行来读取,所以为了保证表现器的刷新服从和提升用户体验,那么表现器最好就是按照行缓冲策略来刷新数据。
        假如我们写入数据没有带 \n 就 不发生刷新,也就是不进行写入, 不进行IO ,不进行系统调用 ,所以此时printf,fprintf,fwrite等c式函数函数成本很低,函数调用会非常快,数据暂存在缓冲区里。所以可以在缓冲区积蓄多份数据,统一进行刷新写入 ,而这个的本质:一次IO可以IO更多的数据,提高IO的服从
全缓冲:
        全缓冲的服从毫无疑问是最高的,因为只必要等待一次设备就绪即可,其他刷新策略等待的次数可就不止一次了,在磁盘文件读写的时间,采用的策略就是全缓冲。
5.解答问题

5.1.例题解答

首先我们要明白,那些printf,fprintf,fwrite等c式函数最后都是会调用系统的系统调用接口write

[*]printf,fprintf,fwrite等c式函数是先将数据写到语言级缓冲区里面,然后通过刷新策略刷新数据,然后才会调用系统调用接口write,如许子才能把字符串打印到屏幕上面
[*]目前的知识水平而言,单独调用write来打印的话,我们先把它看作是没有缓冲区的写方式,一执行就立马打印,例5能证实
很好,我们如今来逐一解答五个例子留下的问题
例1就不说了,我们来看例2,例4
https://i-blog.csdnimg.cn/direct/a5549e64321343c0bdd1ead97a126a6a.png
         在例2中, 我们执行这个步伐的时间是把数据打印到屏幕上,而将数据打印到表现器时所采用的就是行缓冲,执行printf,fprintf,fwrite等c式函数会先把要打印的字符串拷贝到C缓冲区,但是因为他们每句都有\n,这个会刷新C缓冲区,所以当我们执行完每一句对应代码后就立刻将数据刷新并调用系统接口write将字符串写到了表现器上,然后后面才关闭了表现器,完全不会影响到打印。
         在例4中,我们执行这个步伐的时间是把数据打印到屏幕上,而将数据打印到表现器时所采用的就是行缓冲,执行printf,fprintf,fwrite等c式函数会先把要打印的字符串拷贝到C缓冲区,但是我们没有给每个要打印的字符串加上换行符\n,然后这些数据就一直堆积在C缓冲区里面,直到进程退出时才会刷新然后调用系统接口write打印出来,但是我们在进程结束之前就关闭了屏幕,所以自然打印不出来了!!!!
        我们接着看例3
        在例3中, 我们将结果打印到屏幕的时间,数据的刷新策略是行缓冲(遇到\n就刷新),然后按照执行次序先将printf函数要打印的数据写到了C语言自带的缓冲区当中,因为他后面都有\n,所以立刻从缓冲区刷新出来并调用write打印到屏幕上面来了,而fprintf,fwrite也是如此,然后最后执行fork创建子进程,子进程共享父进程的数据,但是父子进程在fork函数之后都没有修改数据的行为,所以这个子进程到进程结束都是和父进程共享代码和数据,也就是说,子进程什么也没做,所以打印结果很符合我们的预期
        而当我们将运行结果重定向到log.txt文件时,数据的刷新策略就变为了全缓冲(等到满了才刷新),此时我们使用printf和fwrite,fprintf函数打印的数据先后都写到了C语言自带的缓冲区当中,等待缓冲区满了/进程退出刷新缓冲区,之后当我们使用fork函数创建子进程时,刚开始是父子进程共享数据和代码,而之后当父进程或是子进程对要刷新C缓冲区内容时(要么缓冲区满了,要么哪个进程先退出),本质就是对父子进程共享的数据进行了修改,此时就必要对缓冲区进行写时拷贝,至此C缓冲区就变成了两份,一份父进程的,一份子进程的,所以重定向到log.txt文件当中printf和fwrite,fprintf函数打印的数据就有两份。
        但由于write函数是系统接口,我们可以将write函数看作是没有C缓冲区的,因此write函数打印的数据就只打印了一份。
   对于例3的环境,大家大概不相信全缓冲,我们来验证一下
#include <stdio.h>
#include<string.h>
#include<unistd.h>

int main()
{
        const char* fstr = "hello fwrite\n";

        printf("hello printf\n");
        sleep(2);
        fprintf(stdout, "hello fprintf\n");
        sleep(2);
        fwrite(fstr, strlen(fstr), 1, stdout);//第二个是块大小,第三个是块个数
        sleep(2);

        const char* str = "hello write\n";
        write(1, str, strlen(str));

        sleep(5);
}我们往屏幕打印的结果是
https://i-blog.csdnimg.cn/direct/bdec55af4801411087c36d7bde4e938c.png 
我们将其重定向到log.txt里面去
while :; do cat log.txt; sleep 1; echo "*******************" ; done  https://i-blog.csdnimg.cn/direct/b518ec5e84de43fbb3f4864bc8b7fbe1.png
这阐明write没缓冲区可以直接写出来了,但是其他的C库函数都有缓冲区,等到进程结束了才全刷新出来
    5.2.这个缓冲区是谁提供的?

        printf和fwrite,fprintf是库函数,write是系统调用,库函数在系统调用的“上层”, 是对系统 调用的“封装”,但是 write 没有缓冲区,而printf和fwrite,fprintf有,足以阐明,该缓冲区是二次加上的,又因为 是C,所以是C标准库提供的
        换句话说假如说这个缓冲区是操作系统提供的,那么printf、fputs和write函数打印的数据重定向到文件后都应该打印两次。
   5.3.这个缓冲区在那边?

我们常说printf是将数据打印到stdout里面,而stdout就是一个FILE*的指针,在FILE布局体当中另有一大部分成员是用于记载缓冲区相干的信息的。
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;

/*char* _save_gptr;char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};


也就是说,这里的缓冲区是由C语言提供,在FILE布局体当中进行维护的,FILE布局体当中不仅生存了对应文件的文件描述符还生存了用户缓冲区的相干信息。
FILE里另有对应打开文件的缓冲区字段和维护信息


[*]这就阐明每个打开的文件都有一个语言缓冲区,通过它自己的文件描述符刷新出来
   这个FILE对象属于用户的照旧操作系统的?
一定是属于用户级别的 
   为什么foen返回FILE*?
https://i-blog.csdnimg.cn/direct/1c06478a91a54ef6820b0d7053f4d95d.png 
在系统层调用open,拿文件描述符,在语言层创建出一个FILE对象 
   5.4.操作系统有缓冲区吗?

        操作系统现实上也是有缓冲区的,当我们刷新用户缓冲区的数据时,并不是直接将用户缓冲区的数据刷新到磁盘或是表现器上,而是先将数据刷新到操作系统缓冲区,然后再由操作系统将数据刷新到磁盘或是表现器上。(操作系统有自己的刷新机制,我们不必关系操作系统缓冲区的刷新规则)
https://img-blog.csdnimg.cn/direct/11229cc170a64caf8d75e17c2b373b04.png

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 【Linux】基础I/O——FILE,用户缓冲区