Linux:深刻理解缓冲区

一、缓冲区的概念

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

二、认识缓冲区

1.用户级缓冲区

2.文件内核级缓冲区

read函数和write函数本质上是系统调用级的拷贝函数

思考一下:

这个函数频繁调用,将内容拷贝到文件内核缓冲区,然后操作系统再刷新到磁盘,频繁刷新,这样的效率是不是太低了,(系统调用级函数也是有成本的,操作系统刷新数据也是有成本的)

所以我们可以把文件级缓冲区里面的内容写满了,再让操作系统刷新到磁盘里面

但是,这样用户使用的成本就太高了,所以就有了语言级缓冲区

3.语言级缓冲区

fputs函数的底层是调用了write系统调用级函数,但是**fputs函数会先将用户的数据存储在输入缓冲区中,当缓冲区满了或者满足一定的条件后,**会将输入缓冲区中的内容传给write函数,然后交操作系统来处理。

有了语言缓冲区这一层,IO的效率会大大提升

三、理解打开文件和关闭文件

**思考:**输入缓冲区和输出缓冲区在哪里呢???

答:在FILE结构体中

当调用c语言中的fopen的时候,返回的是一个FILE结构体指针,这个FILE* 指向的结构体对象是在什么时候创建的呢??

答:在调用fopen函数的时候创建的,调用fopen函数的时候会创建FILE结构体,该结构体里面有文件描述符,输入缓冲区,输出缓冲区等内容。

在调用fclose函数的时候,c语言会释放FILE结构体的内容,并刷新缓冲区。

C 语言 fopen 和 fclose 详解

C 中 FILE 内存位置

四、缓冲区刷新机制

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

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

1.语言级缓冲区的刷新

  • 进程结束的时候,会自动刷新
  • 如果目标文件是显示器,行刷新(行缓冲)
  • 普通文件,一般是全缓冲,缓冲区写满了,才会刷新

2.内核级缓冲区的刷新

细节1:只要把数据从用户缓冲区拷贝到了内核文件缓冲区,就相当于交给了硬件!!!

细节2:客观上,就是写给了file对应的文件内核缓冲区->OS->自主刷新->磁盘

OS有自己的刷新策略(立即刷新或者等OS不忙了再自主刷新,OS自己定)

当然有系统调用,可以让OS立即刷新内核缓冲区:

cpp 复制代码
#include <unistd.h>
int fsync(int fd);

3.用3种现象来验证

1.运行下面代码,按道理来说log.txt文件中是应该有数据的,但是实际为什么没有呢?

cpp 复制代码
int main()
{
    close(1);
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC);
    printf("fd is:%d\n", fd);
    close(fd);
    return 0;
}

解释:文件描述符1关闭了,fd的内容就存储在了1位置上,fd是普通文件,普通文件需要在缓冲区写满才会刷新,或者进程结束了自动刷新,又因为进程结束前close把文件给关闭了,所以缓冲区的内容没有刷新到文件中。

2.exit和_exit函数

exit会刷新语言级缓冲区里面的内容,而_exit是系统级的函数,不会刷新语言级缓冲区里面的内容,这里就很清楚的理解了

3.看下面代码和运行结果:

cpp 复制代码
  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <sys/types.h>
  4 #include <sys/stat.h>
  5 #include <fcntl.h>
  6 #include <stdlib.h>
  7 #include <string.h>
  8 
  9 char buffer[1024];
 10 
 11 int main()
 12 {
 13     read(3, buffer, sizeof(buffer));                                                                                                                                   
 14     write(3, buffer, sizeof(buffer));
 15     // 向显示器打印字符串
 16     printf("hello print\n");
 17     fprintf(stdout, "hello fprintf\n");
 18     const char* s = "hello fputs\n";
 19     fputs(s, stdout);
 20 
 21 
 22     // 系统调用
 23     const char* ss = "hello write\n";
 24     write(1, ss ,strlen(ss));
 25 
 26 
 27     fork();
 28     return 0;
 29 }

运行结果:

bash 复制代码
[zhangsan@hcss-ecs-f571 IO]$ gcc operfile2.c -o operfile2
[zhangsan@hcss-ecs-f571 IO]$ ./operfile2
hello print
hello fprintf
hello fputs
hello write
[zhangsan@hcss-ecs-f571 IO]$ ./operfile2 > log.txt 
[zhangsan@hcss-ecs-f571 IO]$ cat log.txt 
hello write
hello print
hello fprintf
hello fputs
hello print
hello fprintf
hello fputs
[zhangsan@hcss-ecs-f571 IO]$ 

运行结果会发现:

正常执行程序,是正常的输出打印 ,将程序输出的内容重定向到"log.txt"文件中,输出结果为什么会有重复打印的呢???

解释:父进程正常运行打印,fork函数,创建子进程后,在语言级文件缓冲区中还有内容存在(没有write函数写的内容,因为这是一个系统级的调用),重定向后,子进程是向"log.txt"文件中写入这是一个普通文件,刷新规则是:进程结束的时候,自动刷新。所以子进程将语言缓冲区里面的内容又刷新了一遍,所以就会多打印一遍。


五、理解为什么有标准错误

创建一个进程,默认会打开三个文件,标准输入(0,对应键盘),标准输出(1,对应显示器),标准错误(2,对应显示器)

标准输入是作为数据的源头,标准输出可以将处理后的数据打印出来,那标准错误是干什么的呢???

看一下代码和运行结果:

代码:

cpp 复制代码
  1 #include <stdio.h>                                                                                                                                                     
  2 #include <string.h>
  3 #include <unistd.h>
  4 
  5 
  6 int main()
  7 {
  8     // 标准输出:1
  9     printf("这时一个正常消息\n");
 10     fprintf(stdout, "这也是一个正常消息\n");
 11     const char* s1 = "这时一个正常消息,write\n";
 12     write(1, s1, strlen(s1));
 13 
 14     // 标准错误:2
 15     fprintf(stderr, "这是一个错误的消息\n");
 16     const char* s2 = "这是一个错误的消息,write\n";
 17     write(2,s2, strlen(s2));
 18     perror("perror, hello");
 19 
 20     return 0;
 21 }

运行结果:

bash 复制代码
[zhangsan@hcss-ecs-f571 stderr]$ ./a.out 
这时一个正常消息
这也是一个正常消息
这时一个正常消息,write
这是一个错误的消息
这是一个错误的消息,write
perror, hello: Success
[zhangsan@hcss-ecs-f571 stderr]$ ls
a.out  error.txt  normal.txt  test.c
[zhangsan@hcss-ecs-f571 stderr]$ ./a.out > normal.txt 
这是一个错误的消息
这是一个错误的消息,write
perror, hello: Success
[zhangsan@hcss-ecs-f571 stderr]$ cat normal.txt 
这时一个正常消息,write
这时一个正常消息
这也是一个正常消息
[zhangsan@hcss-ecs-f571 stderr]$ 

会发现标准输出的内容重定向到了normal.txt文件中,而标准错误中的消息没有重定向到normal.txt文件中,这个也比较好理解,因为重定向只改变的标准输出(1),把文件描述符1指向的内容从显示器改成了normal.txt文件中

那怎么把标准错误也输出到文件中呢?

cpp 复制代码
[zhangsan@hcss-ecs-f571 stderr]$ ./a.out 2> error.txt
这时一个正常消息
这也是一个正常消息
这时一个正常消息,write
[zhangsan@hcss-ecs-f571 stderr]$ cat error.txt 
这是一个错误的消息
这是一个错误的消息,write
perror, hello: Success
[zhangsan@hcss-ecs-f571 stderr]$ 

解释一下:把error.txt文件描述符指向的内容拷贝到2号下标中,所以标准错误可以重定向到error.txt文件中。

所以就可以这样(正确信息和错误信息实现分离):

bash 复制代码
[zhangsan@hcss-ecs-f571 stderr]$ ./a.out 1>normal.txt 2>error.txt 
[zhangsan@hcss-ecs-f571 stderr]$ cat normal.txt 
这时一个正常消息,write
这时一个正常消息
这也是一个正常消息
[zhangsan@hcss-ecs-f571 stderr]$ cat error.txt 
这是一个错误的消息
这是一个错误的消息,write
perror, hello: Success
[zhangsan@hcss-ecs-f571 stderr]$ 

将内容全部写入到一个文件的实现:

cpp 复制代码
[zhangsan@hcss-ecs-f571 stderr]$ ./a.out > ok.txt 2>&1
[zhangsan@hcss-ecs-f571 stderr]$ cat ok.txt
这时一个正常消息,write
这是一个错误的消息
这是一个错误的消息,write
perror, hello: Success
这时一个正常消息
这也是一个正常消息

这样可以实现,解释一下:

bash 复制代码
[zhangsan@hcss-ecs-f571 stderr]$ ./a.out > ok.txt 2>&1

前半部分是将ok.txt文件描述符的内容拷贝到1中后半部分是将文件描述符1中的内容拷贝到2中,所以就实现了标准输出和标准错误的重定向。

Bash 专用简化写法(更简洁,效果同上)

bash 复制代码
./a.out &> ok.txt

下面这种写法也可以:

bash 复制代码
./a.out >& ok.txt

六、自己封装一个c语言文件级函数

分3个文件:

mystdio.h 头文件

mystdio.c 用来实现函数

main.c 用来进行测试

mystdio.h:

cpp 复制代码
// mystdio.h                                                                                                                                            
  1 #pragma once 
  2 
  3 #include <stdio.h>
  4 
  5 #define SIZE 1024
  6 #define NON_BUFFER  1 // 0001 
  7 #define LINE_BUFFER 2 // 0010
  8 #define FULL_BUFFER 4 // 0100
  9 
 10 
 11 #define MODE 0666
 12 typedef struct _myFILE
 13 {
 14     int fd;
 15     int flags; // 标记打开模式                                                                                                                                         
 16     int flush_mode;
 17     char outbuffer[SIZE];
 18     int pos; // buffer 下标位置
 19     int cap; // buffer 总容量
 20 
 21 
 22 }myFILE;
 23 
 24 myFILE* myfopen(const char* pathname, const char* mode); // r, w, a, r+, w+...
 25 int myfputs(const char* str, myFILE* fp);
 26 void myfflush(myFILE* fp);
 27 void myfclose(myFILE* fp);
 28

mystdio.c:

cpp 复制代码
// mystdio.c                                                                                                                                           
  1 #include "mystdio.h"
  2 #include <string.h> // 也可以自己封装,这里不是重点
  3 #include <stdlib.h>
  4 #include <sys/stat.h>
  5 #include <sys/types.h>
  6 #include <fcntl.h>
  7 #include <unistd.h>
  8 
  9 
 10 #define TRY_FLUSH 1
 11 #define MUST_FLUSH 2
 12 myFILE* myfopen(const char* pathname, const char* mode) // r, w, a, r+, w+...
 13 {
 14     int fd = -1;
 15     int flags = 0;
 16     if(strcmp(mode, "r") == 0)
 17     {
 18         flags = O_RDONLY;
 19         fd = open(pathname, flags);
 20     }
 21     else if(strcmp(mode, "w") == 0)
 22     {
 23         flags = O_WRONLY | O_CREAT | O_TRUNC;
 24         fd = open(pathname, flags, MODE);
 25 
 26     }
 27     else if(strcmp(mode, "a") == 0)
 28     {
 29         flags = O_WRONLY | O_CREAT | O_APPEND;
 30         fd = open(pathname, flags, MODE);
 31     }
 32     else 
 33     {
 34         //TODO
 35     }
 36     if(fd < 0) return NULL; // 打开文件失败,返回空
 37     myFILE* fp = (myFILE*)malloc(sizeof(myFILE));    
 38     if(fp == NULL) 
 39         return NULL;
 40     fp->fd = fd;
 41     fp->flags = flags;                                                                                                                                                 
 42     fp->flush_mode = LINE_BUFFER; 
 43     fp->cap = SIZE;
 44     fp->pos = 0;
 45     return fp; 
 46 }
 47 static void myfflushcore(myFILE* fp, int flag) //只在当前文件有效
 48 {
 49     if(fp->pos == 0) return; 
 50     // 这里只实现了行缓冲,其他类型的可以添加实现
 51     if((fp->flush_mode & LINE_BUFFER) || (flag & MUST_FLUSH))                                                                                                          
 52     {
 53         //"abcd\n"
 54         if((fp->outbuffer[fp->pos-1] == '\n') || (flag & MUST_FLUSH))
 55         {
 56            //  myfflush()
 57            // 写到内核中
 58             write(fp->fd, fp->outbuffer, fp->pos);
 59             memset(fp->outbuffer, 0, fp->pos);
 60             fp->pos = 0; // 清空缓冲区
 61 
 62         }
 63     }
 64     else if(fp->flush_mode & FULL_BUFFER) // 缓存区满了刷新
 65     {
 66         if(fp->pos == fp->cap)
 67         {
 68             // ...
 69         }
 70     }
 71     else if(fp->flush_mode & NON_BUFFER) // 无缓存模式
 72     {
 73         // write(); 
 74     }
 75 
 76 }
 77 void myfflush(myFILE* fp)
 78 {
 79     myfflushcore(fp, MUST_FLUSH);
 80 }
 81 
 82 int myfputs(const char* str, myFILE* fp)
 83 {
 84     if(strlen(str) == 0)
 85         return 0;
 86     // step1:向文件流里面写,本质是:写到文件缓冲(拷贝)
 87     memcpy(fp->outbuffer + fp->pos,str, strlen(str)); //拷贝到缓冲区中,因为可能是多次fputs所以需要有pos标记位
 88     fp->pos += strlen(str);
 89     // step2:如果条件运行,可以自己刷新
 90     myfflushcore(fp, TRY_FLUSH);
 91     return strlen(str);
 92 }
 93 void myfclose(myFILE* fp)
 94 {
 95     //1.强制刷新到内核
 96     myfflush(fp);
 97 
 98     //1.2 强制刷新到磁盘
 99     fsync(fp->fd);// 不是必须的,内核会自己刷新的
100 
101     //2.关闭文件
102     close(fp->fd);
103 
104     //3.free 
105     free(fp);
106 
107 }

main.c文件:

cpp 复制代码
  1 #include "mystdio.h"
  2 #include <unistd.h>
  3 int main()
  4 {
  5     myFILE* fp = myfopen("log.txt", "a");                                                                                                                              
  6     if(fp == NULL)
  7     {
  8         perror("myfopen error!\n");
  9     }
 10 
 11     int cnt = 10;
 12     const char* msg = "hello word\n";
 13     while(cnt--)
 14     {
 15         myfputs(msg, fp);
 16         sleep(1);
 17         printf("debug:outbuffer = %s,pos =  %d\n",fp->outbuffer, fp->pos);
 18     }
 19 
 20     myfclose(fp);
 21     printf("write file done!\n");
 22     return 0;
 23 }
相关推荐
工程师老罗2 小时前
龙芯2k0300 PMON取消Linux自启动
linux·运维·服务器
千百元2 小时前
centos如何删除恶心定时任务
linux·运维·centos
oMcLin5 小时前
如何在Manjaro Linux上配置并优化Caddy Web服务器,确保高并发流量下的稳定性与安全性?
linux·服务器·前端
济6175 小时前
linux(第七期)--gcc编译软件-- Ubuntu20.04
linux·运维·服务器
corpse20105 小时前
Linux监控软件Monitorix 安装部署
linux·安全
wdfk_prog5 小时前
[Linux]学习笔记系列 -- [fs]super
linux·笔记·学习
姚青&5 小时前
四.文件处理命令-文本编辑
linux
oMcLin5 小时前
如何在 Red Hat Linux 8 上实现 Kubernetes 自定义资源管理器(CRD)扩展,支持微服务架构
linux·架构·kubernetes
济6176 小时前
linux(第十一期)--Makefile 语法简述-- Ubuntu20.04
linux
hwlfly6 小时前
Linux内核TCP网络模块深度分析
linux