【Linux】理解缓冲区

举报
平凡的人1 发表于 2023/01/19 20:26:46 2023/01/19
【摘要】 @[toc] 一.引入我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork有关!C接口的函数被打印了两次系统接口前后只是打印了一次:和fork函数有关,fork会创建子进程。在创建子进程的时候,数据会被处理成两份,父子进程发生写时拷贝,我们进行printf调用数据的时候,数据写到显示器外设上,就不属于父进程了,数...

@[toc]

一.引入

image-20221231115202274

我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork有关!

C接口的函数被打印了两次系统接口前后只是打印了一次:和fork函数有关,fork会创建子进程。在创建子进程的时候,数据会被处理成两份,父子进程发生写时拷贝,我们进行printf调用数据的时候,数据写到显示器外设上,就不属于父进程了,数据没被写到显示器上,依旧属于父进程,而调用printf并不一定把数据刷到显示器上,没有被显示本质就是数据没有从内存到外设,所以这份没有被显示的数据依旧属于这进程,当我们去fork的时候,进程退出要刷新缓冲区,此时刷新的过程就是把数据从内存刷新到外设,刷新到外设的同时,也会把程序内部的缓冲区的数据直接清走,这就是写入,跟写时拷贝有关系

对于这个现象的问题我们可以直接往下看👇


二.认识缓冲区

1.为什么

缓冲区的本质就是一段内存。在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。

数据如果直接从内存到磁盘,在内存中速度快,但是访问外设效率比较低,那太消耗时间了,属于外设IO,所以缓冲区的意义就是节省进程进行数据IO的时间!进程需要把数据拷贝到缓冲区里:我们并不需要拷贝,而是调用fwrite,与其理解fwrite是写入到文件的函数,倒不如理解fwrite是拷贝函数,将数据从进程拷贝到缓冲区或者外设当中

image-20230112164431962

数据可以直接拷贝到缓冲区,高速设备不用在等待低速设备,提高计算机的效率。

2.刷新策略

缓冲区的刷新策略:如果有一块数据,一次写入到外设(效率最高)vs如果有一块数据,多次少量写入到外设,需要多次IO

缓冲区一定结合具体的设备定制自己的刷新策略

1.立即刷新——无缓冲 ,场景较少,比如调用printf直接fflush

2.行刷新——行缓冲——显示器 ,数据的printf带上\n就会立马显示到显示器上。显示器为什么是行缓冲:显示器是外设,进程运行时在内存里的,把数据定期要刷新到外设,显示器设备比较特殊,是给用户来看的,从左到右,所以显示器为了保证刷新效率,并且用户体验良好,所以显示器采用行缓冲,满足用户的阅读体验并且在一定程度上效率不至于太低

3.缓冲区满——全缓冲——磁盘文件,效率最高,只需要一次IO,比如文件读写的时候,直接写到磁盘文件

但是存在特殊情况:a.用户强制刷新 b,进程退出——一般到要进行缓冲区刷新

所以对于全缓冲,缓冲区满了采取刷新,减少IO次数,提高效率。

3.在哪里

缓冲区的位置究竟在哪里:从上面的例子我们直接往显示器上打印结果为4条,往文件打印为7条,这跟缓冲区有关,同时这也说明了缓冲区一定不在内核中,为什么?如果在内核中write也应该打印两次,write是系统接口。我们之前谈论的所有缓冲区都指的是用户级语言层面提供的缓冲区。这个缓冲区,在stdout,stdin,stderr对应的类型---->FILE*,FILE是一个结构体,里面封装了fd,同时还包括了一个缓冲区!

FILE结构体缓冲区,所以我们直接要强制刷新的时候fflush(文件指针),关闭文件fclose(文件指针),这是因为传进去的文件指针对应的缓冲区

从源码出发,我们可以来看一看FILE结构体:

image-20230106144023818

image-20230106144054268

所以我们一般所说的缓冲区是语言级别的缓冲区,C语言提供的在FILE结构体里对应的缓冲区。

现在,我们现在重新来看一看刚开始的现象:

image-20230106150737623

1.如果我们没有进行重定向>,看到了4条消息,stdout默认使用的是行刷新,在进程fork之前,三条C函数已经将数据打印输出到显示器上(外设),你的FILE内部进程内部就不存在对应的数据了。

2.如果我们进行了重定向>,写入文件不在是显示器,而是普通文件,采用的刷新策略是全缓冲,之前的3条C函数虽然带了\n,但是不足以将stdout缓冲区写满,所以数据并没有刷新! 在执行fork的时候,stdout属于父进程,fork创建子进程紧接着就是进程退出,谁先退出就要进行缓冲区刷新,刷新的本质就是修改,修改的时候发生写时拷贝!所以数据最终会显示两份!上面的过程都和write无关,write没有FILE,而用的是fd,就没有C提供的缓冲区!

简单总结来说:重定向导致刷新策略发生了改变(由行缓冲变成了全缓冲)。同时发生了写时拷贝,父子进程各自刷新


三、理解缓冲区

对于缓冲区的理解我们可以自己通过代码来简单实现:

FILE_结构体的设计,这里为了避免与FILE发生冲突,我们命名为FILE_:

#define SIZE 1024
typedef struct _FILE
{
    int flags;//刷新方式
    int fileno;//文件描述符
    int cap;//buffer的总容量
    int size;//buffer当前使用量
    char buffer[SIZE];//缓冲区                                                                              }FILE_;               

主函数:

int main()        
{         
    //打开
    FILE_ *fp = fopen_("./log.txt","w");
    //打开失败
    if(fp==NULL)                        
    {                                   
        return 1;
    }            
    int cnt = 10;
    const char*masg = "hello world ";
    while(1)                         
    {
        //写入
        fwrite_(masg,strlen(masg),fp);
        //刷新
        fflush_(fp);
        //睡眠
        sleep(1);                     
        printf("count:%d\n",cnt);
        cnt--;                   
        if(cnt==0) break;        
    }   
    //关闭
    fclose_(fp);                                                                               
    return 0;                                                                                  
}                                                                                              

对于C语言来说,文件接口一旦打开成功,其余接口要带上FILE*,因为FILE结构体里包含了各种数据:

下面是我们需要自己实现的文件接口:

//打开
FILE_ * fopen_(const char*path_name,const char*mode);
//以下的接口都需要带上FILE_*
void  fwrite_(const void *ptr,int num, FILE_*fp);

void fflush_(FILE_*fp);

void fclose_(FILE_* fp);

fopen_:打开我们需要去判断具体是按什么方式打开:

FILE_ *fopen_(const char *path_name, const char *mode)
{
    int flags = 0;
    int defaultMode=0666; //设置默认权限
    //读方式
    if(strcmp(mode, "r") == 0)
    {
        flags |= O_RDONLY;
    }
    //写方式
    else if(strcmp(mode, "w") == 0)
    {
        flags |= (O_WRONLY | O_CREAT |O_TRUNC);
    }
    //追加方式
    else if(strcmp(mode, "a") == 0)
    {
        flags |= (O_WRONLY | O_CREAT |O_APPEND);
    }
    //其他
    else
    {
        //其他方式这里就不展开了
    }
    int fd = 0;

    if(flags & O_RDONLY) fd = open(path_name, flags);
    else fd = open(path_name, flags, defaultMode);
    //处理打开失败
    if(fd < 0)
    {
        const char *err = strerror(errno);
        write(2, err, strlen(err));
        return NULL; 
    }
    //打开成功
    FILE_ *fp = (FILE_*)malloc(sizeof(FILE_));
    assert(fp);

    fp->flags = SYNC_LINE; //默认设置成为行刷新
    fp->fileno = fd;
    fp->cap = SIZE;
    fp->size = 0;
    memset(fp->buffer, 0 , SIZE);
    return fp; 
}

fwrite_:

void fwrite_(const void *ptr, int num, FILE_ *fp)
{
    // 写入到缓冲区中
    memcpy(fp->buffer+fp->size, ptr, num); //不考虑缓冲区溢出的问题
    fp->size += num;
    // 判断是否刷新
    if(fp->flags & SYNC_NOW)
    {
        write(fp->fileno, fp->buffer, fp->size);
        fp->size = 0; //清空缓冲区
    }
    else if(fp->flags & SYNC_FULL)
    {
        if(fp->size == fp->cap)
        {
            write(fp->fileno, fp->buffer, fp->size);
            fp->size = 0;
        }
    }
    else if(fp->flags & SYNC_LINE)
    {
        if(fp->buffer[fp->size-1] == '\n') // abcd\nefg在这个地方不考虑
        {
            write(fp->fileno, fp->buffer, fp->size);
            fp->size = 0;
        }
    }
    else{
    }
}

fclose:

void fclose_(FILE_ * fp)
{
    fflush_(fp);
    close(fp->fileno);
}

fflush_:这里将数据强制要求操作系统进行外设刷新要用到fsync:

void fflush_(FILE_ *fp)
{
    if( fp->size > 0) write(fp->fileno, fp->buffer, fp->size);
    fsync(fp->fileno); //将数据,强制要求OS进行外设刷新!
    fp->size = 0;
}
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。