• [ Linux ] 缓冲区的理解 以及简易模拟实现封装C标准库


    在输出重定向的时候为什么必须fflush(stdout)才能将内容刷新到指定文件呢?我们当时回答是因为存在缓冲区。那么本篇文章我们将重点了解认识一下缓冲区。

    目录

    0.什么是缓冲区?

    1.为什么要有缓冲区?

    2.缓冲区在哪里?

    3.缓冲区的刷新策略

    3.1 刷新策略问题

    常规

    特殊

    4.奇怪的问题

    5.模拟实现一下自己封装C标准库


    0.什么是缓冲区?

    缓冲区的本质就是一段内存。 那么这段内存在哪里呢?我们接下来将会说明这个问题。

    1.为什么要有缓冲区?

    我们举个例子来理解这个概念:

    假设你在北京大学上学,你的朋友在上海交通大学上学,你有10本书想给你的朋友,你打算怎么将这些书送给你的同学呢?

    第一种方式:你自己带着10本书从北京到上海,亲自送给你的朋友。但是这种方式成本明显过于大,并且耽误你的时间。因此我们通常是采用第二种方式。

    第二种方式:你在北京大学门口菜鸟驿站将10本书打包成快递发给你在上海交通大学的朋友。当你发送完快递后你就什么也不用管了,静静地等着你朋友收到快递的消息即可。

    因此这个快递存在的最大价值是解放你的时间。这里快递存在意义等同于缓冲区的意义。

    缓冲区的意义:

    1. 解放使用缓冲区的进程时间。
    2. 缓冲区的存在可以集中处理数据刷新,减少IO的次数,从而达到提高整机的效率。

    2.缓冲区在哪里?

    我们使用一段代码来理解

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main()
    8. {
    9. printf("hello printf");// stdout -> 1
    10. const char* msg = "hello write";
    11. write(1,msg,strlen(msg));
    12. sleep(5);
    13. return 0;
    14. }

    printf内部封装了write,而printf不显示的原因是因为printf的内容在缓冲区内,当sleep时,内容存在在缓冲区内,当我们不带'\n'时,不会被理解刷新出来,数据被暂存在缓冲区内。

    但是我们看到hello write被立马刷新,那么printf封装了write,那么这个缓冲区在哪里呢?

    我们通过现象可以回答的是这个缓冲区一定不在write内。因此这个缓冲区只能是语言提供的(C语言)。因此这个缓冲区是一个语言级别的缓冲区。

    那么我们来具体深挖一下缓冲区的位置.stdout的返回值是FILE,FILE内部有struct结构体,结构体内封装了很多的属性,其中包括上篇我们提到的文件描述符fd,除此之外还有该File对应的语言级别的缓冲区!

    printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,

    都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是 C,所以由C标准库提供

    我们也可以一起看看FILE结构体

    1. ///usr/include/libio.h
    2. struct _IO_FILE
    3. {
    4. int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
    5. #define _IO_file_flags _flags
    6. //缓冲区相关
    7. /* The following pointers correspond to the C++ streambuf protocol. */
    8. /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
    9. char *_IO_read_ptr; /* Current read pointer */
    10. char *_IO_read_end; /* End of get area. */
    11. char *_IO_read_base; /* Start of putback+get area. */
    12. char *_IO_write_base; /* Start of put area. */
    13. char *_IO_write_ptr; /* Current put pointer. */
    14. char *_IO_write_end; /* End of put area. */
    15. char *_IO_buf_base; /* Start of reserve area. */
    16. char *_IO_buf_end; /* End of reserve area. */
    17. /* The following fields are used to support backing up and undo. */
    18. char *_IO_save_base; /* Pointer to start of non-current get area. */
    19. char *_IO_backup_base; /* Pointer to first valid character of backup area */
    20. char *_IO_save_end; /* Pointer to end of non-current get area. */
    21. struct _IO_marker *_markers;
    22. struct _IO_FILE *_chain;
    23. int _fileno; //封装的文件描述符
    24. #if 0
    25. int _blksize;
    26. #else
    27. int _flags2;
    28. #endif
    29. _IO_off_t _old_offset; /* This used to be _offset but it's too small. */
    30. #define __HAVE_COLUMN /* temporary */
    31. /* 1+column number of pbase(); 0 is unknown. */
    32. unsigned short _cur_column;
    33. signed char _vtable_offset;
    34. char _shortbuf[1];
    35. /* char* _save_gptr; char* _save_egptr; */
    36. _IO_lock_t *_lock;
    37. #ifdef _IO_USE_OLD_IO_FILE
    38. };

    3.缓冲区的刷新策略

    3.1 刷新策略问题

    刷新策略说白了就是什么时候刷新?

    常规

    1. 无缓冲(立即刷新)
    2. 行缓冲(逐行刷新)显示器文件
    3. 全缓冲(缓冲区写满再刷新) 块设备对应的文,磁盘文件

    特殊

    1. 进程退出
    2. 用户强制刷新(fflush)

    4.奇怪的问题

    结合上面的之后,下面的这段代码的执行结果是什么?

    1. #include <stdio.h>
    2. #include <string.h>
    3. #include <fcntl.h>
    4. #include <unistd.h>
    5. #include <sys/types.h>
    6. #include <sys/stat.h>
    7. int main()
    8. {
    9. const char *str1 = "hello printf\n";
    10. const char *str2 = "hello fprintf\n";
    11. const char *str3 = "hello fputs\n";
    12. const char *str4 = "hello write\n";
    13. //C库函数
    14. printf(str1);
    15. fprintf(stdout,str2);
    16. fputs(str3,stdout);
    17. //系统接口
    18. write(1,str4,strlen(str4));
    19. //是调用完了上面的代码才执行的fork
    20. fork();
    21. return 0;
    22. }

    我们运行上述代码后,将结果重定向到log.txt内部,为什么会有7条消息?


    答:当我们重定向后,本来要把显示在显示器的文件重定向到指定文件时,缓冲区的刷新策略由行缓冲(显示器文件)切换成了全缓冲(磁盘文件)。答案一定是和fork()有关系。我们可以这样理解,当str1,str2,str3把数据打印到文件里,此时已经重定向到log.txt,数据不会立即刷新,而变成了全缓冲,所以前三条信息暂存在了log.txt缓冲区内部,当我们调用fork()时,fork()要创建子进程,fork之后父子进程同时退出,退出之后父子进程就要刷新缓冲区了,而刷新的本质就是把缓冲区的数据写入到操作系统内部,并清空缓冲区。这里的缓冲区是自己的FILE内部维护的,属于父进程内部的数据区域,当我们刷新的时候,代码和数据要发生写时拷贝,因此这份代码父进程刷一份,子进程刷一份,因此我们就看到了有2个str1,2个str2,2个str3刷到了log.txt。

    5.模拟实现一下自己封装C标准库

    我们写的是样例代码不代表全部的标准的实现。从代码层面上理解一下原理

    1. #include <stdio.h>
    2. #include <string.h>
    3. #include <stdlib.h>
    4. #include <assert.h>
    5. #include <unistd.h>
    6. #include <sys/stat.h>
    7. #include <sys/types.h>
    8. #include <fcntl.h>
    9. #define NUM 1024
    10. #define NONE_FLUSH 0x0
    11. #define LINE_FLUSH 0x1
    12. #define FULL_FLUSH 0x2
    13. typedef struct _MyFILE
    14. {
    15. int _fileno;
    16. char _buffer[NUM];
    17. int _end;
    18. int _flags;//fflush method
    19. }MyFILE;
    20. MyFILE *my_fopen(const char* filename,const char*method)
    21. {
    22. assert(filename);
    23. assert(method);
    24. int flags = O_RDONLY;
    25. if(strcmp(method,"r") == 0)
    26. {
    27. }
    28. else if(strcmp(method,"r+") == 0)
    29. {}
    30. else if(strcmp(method,"w") == 0)
    31. {
    32. flags = O_WRONLY | O_CREAT |O_TRUNC;
    33. }
    34. else if(strcmp(method,"w+") == 0)
    35. {}
    36. else if(strcmp(method,"a") == 0)
    37. {
    38. flags = O_WRONLY | O_CREAT |O_APPEND;
    39. }
    40. else if(strcmp(method,"a+") == 0)
    41. {}
    42. int fileno = open(filename,flags,0666);
    43. if(fileno < 0)
    44. {
    45. return NULL;
    46. }
    47. MyFILE *fp = (MyFILE*)malloc(sizeof(MyFILE));
    48. if(fp == NULL ) return fp;
    49. memset(fp,0,sizeof(MyFILE));
    50. fp->_fileno = fileno;
    51. fp->_flags |= LINE_FLUSH;
    52. fp->_end = 0;
    53. return fp;
    54. }
    55. void my_fflush(MyFILE* fp)
    56. {
    57. assert(fp);
    58. if(fp->_end > 0)
    59. {
    60. write(fp->_fileno,fp->_buffer,fp->_end);
    61. fp->_end =0;
    62. syncfs(fp->_fileno);
    63. }
    64. }
    65. void my_fwrite(MyFILE* fp,const char* start,int len)
    66. {
    67. assert(fp);
    68. assert(start);
    69. assert(len>0);
    70. // abcde->追加
    71. strncpy(fp->_buffer+fp->_end,start,len);//将数据写入缓冲区
    72. fp->_end += len;
    73. if(fp->_flags & NONE_FLUSH){}
    74. if(fp->_flags & LINE_FLUSH)
    75. {
    76. if(fp->_end > 0 && fp->_buffer[fp->_end-1] == '\n')
    77. {
    78. write(fp->_fileno,fp->_buffer,fp->_end);
    79. fp->_end = 0;
    80. syncfs(fp->_fileno);
    81. }
    82. }
    83. if(fp->_flags & FULL_FLUSH){}
    84. }
    85. void my_fclose(MyFILE* fp)
    86. {
    87. my_fflush(fp);
    88. close(fp->_fileno);
    89. free(fp);
    90. }
    91. int main()
    92. {
    93. MyFILE * fp = my_fopen("log.txt","w");
    94. if(fp == NULL)
    95. {
    96. printf("my_fopen error\n");
    97. return 1;
    98. }
    99. const char *msg = "hello my_file 11111111\n";
    100. my_fwrite(fp,msg,strlen(msg));
    101. printf("hello my_file 11111111消息立即刷新\n");
    102. sleep(3);
    103. const char *mssg = "hello 222222222";
    104. my_fwrite(fp,mssg,strlen(mssg));
    105. sleep(3);
    106. printf("写入了一个不满足条件的字符串hello 222222222\n");
    107. const char *msssg = "hello 33333333";
    108. my_fwrite(fp,msssg,strlen(msssg));
    109. sleep(3);
    110. printf("写入了一个不满足条件的字符串hello 33333333\n");
    111. const char *mssssg = "end\n";
    112. my_fwrite(fp,mssssg,strlen(mssssg));
    113. printf("写了一个满足条件的字符串end\n");
    114. sleep(3);
    115. const char *msssssg = "aaaaaaa";
    116. my_fwrite(fp,msssssg,strlen(msssssg));
    117. printf("写了一个满足条件的字符串aaaaaaa\n");
    118. sleep(1);
    119. my_fflush(fp);
    120. sleep(3);
    121. my_fclose(fp);
    122. return 0;
    123. }

    我们也可以模拟进程退出

    (本篇完)

  • 相关阅读:
    Go/Golang语言学习实践[回顾]教程04--安装一个Go语言的集成开发环境
    Cadence Allegro PCB设计88问解析(十三) 之 Allegro中artwork层的建立
    中国爱尔兰威士忌市场消费状况与销售策略分析报告2022-2028年
    微信小程序标题栏封装
    【微服务 SpringCloud】实用篇 · 服务拆分和远程调用
    反转单链表
    如何熟练使用vim工具?
    【C++】AVL树的4中旋转调整
    【OpenCV】 红绿灯识别检测
    rust内存优化
  • 原文地址:https://blog.csdn.net/qq_58325487/article/details/127793946