【Linux】缓冲区
创始人
2024-02-15 15:59:23
0

目录

  • 🌈前言
  • 🌷1、缓冲区
    • 🍡1.1、缓冲区的理解
    • 🍢1.2、缓冲区在哪里?
    • 🍣1.3、缓冲区的刷新策略
    • 🍣1.4、模拟实现C库函数
  • 🌸2、标准输出流与错误流的区别
    • 🍤2.1、概念
    • 🍥2.3、perror
    • 🍤2.2、标准错误流的意义

🌈前言

本篇文章进行操作系统中缓冲区的学习!!!


🌷1、缓冲区

🍡1.1、缓冲区的理解

什么是缓冲区呢?

  • 缓冲区的本质:就是一段内存

为什么要有缓冲区呢?

  • 解放使用缓冲区的进程的时间(将数据放到缓冲区后,进程继续执行自己的代码)

  • 缓冲区的存在可以集中处理数据刷新,减少I/O的次数,从而达到提高整机的效率!!!

在这里插入图片描述


🍢1.2、缓冲区在哪里?

代码验证:

字符串带‘\n’,会立即刷新到文件中,这是“行刷新”

[lyh_sky@localhost lesson20]$ cat cache.c 
#include 
#include 
#include int main()
{// stdout -> 1号文件描述符printf("hello printf!!!\n");const char* msg = "hello write!!!\n";// 1号文件描述符 -> stdoutwrite(1, msg, strlen(msg));return 0;
}
[lyh_sky@localhost lesson20]$ ./cache 
hello printf!!!
hello write!!!

如果不带回车有什么现象呢?

[lyh_sky@localhost lesson20]$ cat cache.c 
#include 
#include 
#include int main()
{// stdout -> 1号文件描述符 -- 底层封装了writeprintf("hello printf!!!");const char* msg = "hello write!!!";// 1号文件描述符 -> stdoutwrite(1, msg, strlen(msg));sleep(3);return 0;
}
[lyh_sky@localhost lesson20]$ ./cache 
hello write!!!hello printf!!![lyh_sky@localhost lesson20]$ 
  • printf底层封装了write却没有立即刷新的原因,是因为有缓冲区的存在

  • write系统调用是立即刷新缓冲区的

  • 这个缓冲区一定不在write内部!我们曾经所说的缓冲区,不是内核级别的缓冲区!

  • 那么这个缓冲区只能是语言级别的,由C语言提供

FILE是一个结构体,结构体里封装了很多属性,其中必定包含fd、对应语言级别的缓冲区

在这里插入图片描述

  • 既然缓冲区在FILE内部,在C语言中,我们每打开一个文件,都有一个FILE*文件指针返回

  • 意味着,我们没打开一个文件,都有一个fd和属于自己的对应语言级别的缓冲区!!!


🍣1.3、缓冲区的刷新策略

缓冲区的刷新策略分为三种:

  • 无缓冲:数据立即刷新到外设当中 – write()

  • 行缓冲:数据遇到回车换行(‘\n’)后,刷新到外设当中 – 逐行刷新

  • 全缓冲:缓冲区满了后,就刷新到外设当中 – 块设备对应的文件,磁盘文件

注意:全缓冲不一定是要缓冲区满了才会刷新,进程退出和用户强制刷新也会刷新缓冲区!!!

特殊的刷新策略:

  • 进程退出,刷新缓冲区 – 程序退出、exit()

  • 用户强制刷新 – fflush函数

[lyh_sky@localhost lesson20]$ cat cache.c 
#include 
#include 
#include int main()
{// stdout -> 1号文件描述符 -- 底层封装了writeprintf("hello printf!!!");const char* msg = "hello write!!!";fflush(stdout); // 强制刷新输出缓冲区// 1号文件描述符 -> stdoutwrite(1, msg, strlen(msg));sleep(3);return 0;
}
[lyh_sky@localhost lesson20]$ ./cache 
hello printf!!!hello write!!![lyh_sky@localhost lesson20]$ 

如果在刷新之前,关了fd会怎么样呢???

[lyh_sky@localhost lesson20]$ cat cache.c 
#include 
#include 
#include int main()
{printf("hello printf!!!");            // stdout -> 1const char* msg = "hello write!!!";   // 1 -> stdout// 刷新之前关闭1号文件描述符write(1, msg, strlen(msg));close(1);return 0;
}// 只打印了write写入的数据 -- write是立即刷新缓冲区
hello write!!![lyh_sky@localhost lesson20]$ ./cache 

为什么没有回显内容呢?

  • 因为数据一开始被写入到缓冲区中,但是1号文件描述符已经关闭了

  • 当进程退出后,刷新缓冲区,调用write就失败了!!!所以没有回显到显示器当中!


  • printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区

  • 另外,我们这里所说的缓冲区,都是用户级缓冲区

  • 其实为了提升整机性能,OS也会提供相关内核级缓冲区

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 0int _blksize;#elseint _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
};

综合测试题:

[lyh_sky@localhost lesson20]$  cat cache.c 
#include 
#include 
#include 
#include 
#include 
#include int main()
{const char* str1 = "hello printf\n";const char* str2 = "hello fprintf\n";const char* str3 = "hello fputs\n";const char* str4 = "hello write\n";// C库函数printf(str1);fprintf(stdout, str2);fputs(str3, stdout);// 系统调用write(1, str4, strlen(str4));// 创建子进程 -- 执行上面的代码后子进程才开始执行fork();return 0;
}[lyh_sky@localhost lesson20]$ ls
cache  cache.c  makefile
[lyh_sky@localhost lesson20]$ ./cache 
hello printf
hello fprintf
hello fputs
hello write// 重定向到写入到log.txt文件
[lyh_sky@localhost lesson20]$ ./cache > log.txt
[lyh_sky@localhost lesson20]$ ls
cache  cache.c  log.txt  makefile[lyh_sky@localhost lesson20]$ cat log.txt 
hello write
hello printf
hello fprintf
hello fputs
hello printf
hello fprintf
hello fputs

为什么重定向后除了write系统接口,其他C库函数都回显了二次呢???

理论:

  • 刷新的本质:把缓冲区的数据write到OS内部,清空缓冲区,end置为0

  • 缓冲区是自己的FILE结构体内部维护的,属于父进程内部的数据区域

原因:

注意:如果没有重定向就是“行缓冲”,逐行刷新(遇到\n)

  • 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲

  • printf fwrite 库函数会自带缓冲区(之前的很多例子可以说明),当发生重定向到普通文件时,数据的缓冲方式由“行缓冲”变成了“全缓冲”

  • 重定向的本质是全缓冲(里面必定调用了dup2系统接口),数据会暂存到缓冲区中,当执行到fork()时,创建子进程,子进程直接走到retrun

  • 父子进程在退出时,数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据

  • write因为不存在缓冲区,所以不会进行写时拷贝,所以才打印了一次!

  • 进程中某个数据发生改变,就会写时拷贝某个数据


🍣1.4、模拟实现C库函数

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include #define NUM 1024// 刷新策略标记位
#define NONE_FLUSH 0x0 		// 无缓冲
#define LINE_FLUSH 0x1 		// 行缓冲 
#define FULL_FLUSH 0x2 		// 全缓冲typedef struct _MyFILE{int _fileno; 			// 文件描述符char _buffer[NUM]; 		// 缓冲区int _end; 			    // 记录缓冲区尾部下标int _flags; 			// fflush method
}MyFILE;MyFILE *my_fopen(const char *filename, const char *method)
{assert(filename);assert(method);int flags = O_RDONLY;if(strcmp(method, "r") == 0){flags = O_RDONLY;}else if(strcmp(method, "r+") == 0){flags = O_RDWR;}else if(strcmp(method, "w") == 0){flags = O_WRONLY | O_CREAT | O_TRUNC;}else if(strcmp(method, "w+") == 0){flags = O_RDWR | O_CREAT | O_TRUNC;}else if(strcmp(method, "a") == 0){flags = O_WRONLY | O_CREAT | O_APPEND;}else if(strcmp(method, "a+") == 0){flags = O_RDWR | O_CREAT | O_APPEND;}int fileno = open(filename, flags, 0666);if(fileno < 0){return NULL;}MyFILE *fp = (MyFILE *)malloc(sizeof(MyFILE));if(fp == NULL) return fp;memset(fp, 0, sizeof(MyFILE));fp->_fileno = fileno;fp->_flags |= LINE_FLUSH;fp->_end = 0;return fp;
}void my_fflush(MyFILE *fp)
{assert(fp);if(fp->_end > 0){write(fp->_fileno, fp->_buffer, fp->_end);fp->_end = 0;syncfs(fp->_fileno);}
}void my_fwrite(MyFILE *fp, const char *start, int len)
{assert(fp);assert(start);assert(len > 0);// abcde123// 写入到缓冲区里面strncpy(fp->_buffer+fp->_end, start, len); //将数据写入到缓冲区了fp->_end += len;if(fp->_flags & NONE_FLUSH){}else if(fp->_flags & LINE_FLUSH){if(fp->_end > 0 && fp->_buffer[fp->_end-1] == '\n'){//仅仅是写入到内核中write(fp->_fileno, fp->_buffer, fp->_end);fp->_end = 0;syncfs(fp->_fileno);}}else if(fp->_flags & FULL_FLUSH){// 如果写入缓冲区的数据长度等于缓冲区的最大存储数量,则刷新缓冲区if (len == NUM){write(fp->_fileno, fp->_buffer, fp->_end);fp->_end = 0;syncfs(fp->_fileno)}}
}void my_fclose(MyFILE *fp)
{my_fflush(fp);close(fp->_fileno);free(fp);
}int main()
{MyFILE *fp = my_fopen("log.txt", "w");if(fp == NULL){printf("my_fopen error\n");return 1;}//模拟进程退出my_fclose(fp);return 0;
}

🌸2、标准输出流与错误流的区别

🍤2.1、概念

  • 我们都知道输出流和错误流对应的文件描述符是1和2

  • 1和2对应的外设都是显示器,对其写入就是回显到显示器上

代码验证

#include 
#include int main()
{// stdout->1printf("hello printf->stdout->1\n");fprintf(stdout, "hello fprintf->stdout->1\n");fputs("hello fputs->stdout->1\n", stdout);std::cout << "hello cout->stdout->1" << std::endl;std::cout << std::endl;// stderr->2fprintf(stderr, "hello fprintf->stderr->2\n");fputs("hello fputs->stderr->2\n", stderr);perror("hello perror");std::cerr << "hello cerr->stderr->2" << std::endl;return 0;
}// 输出流和错误流向显示器写入的内容都回显到显示器中了!!!
[lyh_sky@localhost out_errno]$ ./test 
hello printf->stdout->1
hello fprintf->stdout->1
hello fputs->stdout->1
hello cout->stdout->1hello fprintf->stderr->2
hello fputs->stderr->2
hello perror: Success
hello cerr->stderr->2

我们对该代码进行输出重定向,看看有什么区别!!!

[lyh_sky@localhost out_errno]$ ls
makefile  test  Test.cc
[lyh_sky@localhost out_errno]$ ./test > log.txt
hello fprintf->stderr->2
hello fputs->stderr->2
hello perror: Success
hello cerr->stderr->2[lyh_sky@localhost out_errno]$ cat log.txt 
hello printf->stdout->1
hello fprintf->stdout->1
hello fputs->stdout->1
hello cout->stdout->1
  • 我们发现只有向1号文件描述符写入的数据被重定向到了文件当中

但是错误流输出的数据被回显到显示器当中,为什么呢?

  • 因为只进行了输出重定向,输出重定向是指把写入stdout的数据重定向到指向的文件中

  • 而stderr是2号fd,它不会写入到stdout,所以会回显到显示器中!!!

在这里插入图片描述

如何将错误流的数据重定向到文件中呢?

  • 使用【./可执行程序 2> 文件名】,即可将错误流的数据重定向到指定文件中
[lyh_sky@localhost out_errno]$ ls
makefile  test  Test.cc// 这里是将向stdout写入数据重定向到log.txt,向stderr写入数据重定向到errno.tx
[lyh_sky@localhost out_errno]$ ./test > log.txt 2> errno.txt
[lyh_sky@localhost out_errno]$ ls
errno.txt  log.txt  makefile  test  Test.c[lyh_sky@localhost out_errno]$ cat log.txt 
hello printf->stdout->1
hello fprintf->stdout->1
hello fputs->stdout->1
hello cout->stdout->1[lyh_sky@localhost out_errno]$ cat errno.txt 
hello fprintf->stderr->2
hello fputs->stderr->2
hello perror: Success
hello cerr->stderr->2

🍥2.3、perror

#include 
void perror(const char *s);
  • 在标准错误输出上生成一条消息,描述在调用系统或库函数时遇到的最后一个错误

  • 第一个参数如果s不为NULL并且*s不是空字节(“\0”),将打印参数字符串s,后跟冒号和空白

模拟实现perror

#include 
char *strerror(int errnum);
  • 该函数用于获取指向错误消息字符串的指针

  • 可以通过errno获取错误码,然后传递给它,就能获取最近一次的错误信息!!!

[lyh_sky@localhost out_errno]$ cat Test.cc
#include 
#include 
#include 
#include 
#include 
#include 
#include void my_perror(const char *info)
{fprintf(stderr, "%s: %s\n", info, strerror(errno));
}int main()
{//fopen: C库函数int fd = open("log.txt", O_RDONLY);	//必定失败的 -- 当前进程工作路径下没有该文件if(fd < 0){//perror("open");my_perror("my open");return 1;}return 0;
}
[lyh_sky@localhost out_errno]$ ls
makefile  test  Test.cc
[lyh_sky@localhost out_errno]$ ./test
my open: No such file or directory
[lyh_sky@localhost out_errno]$ echo $?
1
  • 当系统调用失败时,它通常返回-1,并将变量errno(全局变量)设置为一个描述错误的值(错误码)。(这些值可以在中找到!!!

  • 语言中会有自己一套的错误码,我们也可以使用exit指定进程退出错误码,或者使用return …

  • 如果调用失败的之后没有立即调用perror(),则errno的值也会被保存下来

  • 函数perror()用于将此错误代码转换为一段字符串,回显到显示器


🍤2.2、标准错误流的意义

意义:

  • 可以区分那些是日常程序的输出,哪些是错误

  • 可以帮助我们以后写项目时,快速的差错,这就是“日志”!!!

  • 我们现在写的程序虽然都用不着,但是还得了解一下

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
苏州离哪个飞机场近(苏州离哪个... 本篇文章极速百科小编给大家谈谈苏州离哪个飞机场近,以及苏州离哪个飞机场近点对应的知识点,希望对各位有...
客厅放八骏马摆件可以吗(家里摆... 今天给各位分享客厅放八骏马摆件可以吗的知识,其中也会对家里摆八骏马摆件好吗进行解释,如果能碰巧解决你...