• Linux--基础IO(2)


    1. 缓冲区

    1.1 概念

    执行下列代码:

    int main()
    {
        close(1);//重定向
        int fd = open("log.txt",O_CREAT|O_WRONLY,0644);
        printf("hello\n");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    查看log.txt发现里面打印了hello,但是如果把fd关闭:

    int main()
    {
        close(1);重定向
        int fd = open("log.txt",O_CREAT|O_WRONLY,0644);
        printf("hello\n");
        close(fd);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    会发现log.txt里面已经没有内容。这是为什么?

    这就要涉及到缓冲区的问题:

    1.2 C缓冲区

    在这里插入图片描述

    平时我们输入的内容其实都是输入到了C缓冲区里,而不是直接刷新。

    而这个刷新的时间,就是进程退出的时候,会将C缓冲区的数据到OS缓冲区。

    所以对于上面的例子来说,所谓的打印不过是把数据放到了C缓冲区中;随后再通过系统调用接口刷新到内核缓冲区,最后再刷新到外设中。

    • 对于这个过程有几点值得提出:
    1. 这个所谓的C缓冲区在哪里?

    存在于FILE指针结构体中。

    之前说到FILE结构体里封装了fd,用于描述文件位置;其实里面还封装了有关维护C缓冲区的内容

    在这里插入图片描述

    1. 这个C缓冲区刷新到OS缓冲区的过程是如何实现的?

    这其实相当于对一个文件进行写入,那么就肯定要用到fd,并且这个刷新过程是由固定的刷新策略决定的。

    1.3 刷新策略

    1.3.1 三种刷新方式
    1. 立即刷新(不缓冲,有数据就马上刷新);
    2. 行缓冲(通过\n区分,比如显示器打印);
    3. 全缓冲(缓冲区满了才刷新,比如磁盘)。

    这几种方式对于OS缓冲区和硬件都适用。

    而对于重定向:显示器重定向到文件中,其实就是说刷新策略由行缓冲变成全缓冲。

    • 解析

    而对于一开始的现象,可以这么解释:

    1. 如果有重定向

    能显示出来是因为重定向到了log.txt文件内,策略变成了全缓冲那么数据刷新过程就是先刷新到了C缓冲区,进程结束后(这是条件)就会刷新到OS缓冲区然后显示在文件内;这是没有close(fd)的时候;

    但是有close(fd)以后,内容还在C缓冲区,由于全缓冲,还未刷新到OS缓冲区内(进程未结束),就没有显示内容。

    1. 如果没有重定向

    如果没有重定向,无论有没有close(fd)都会输出,因为没有重定向就是打印到显示器,采用行刷新,只要C缓冲区识别到了\n就可以直接刷新。

    而对于无法显示内容,我们可以采用刷新的方式将内容从C缓冲区刷新到OS缓冲区:

    fflush(stdout);
    
    • 1

    运行下列代码:

    int main()
    {
        const char* a = "hello a\n";
        write(1,a,strlen(a));
        printf("hello printf\n");
        fprintf("hello fprintf\n");
        close(fd);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    将可执行文件重定向到任一文件中,会发现:只有"hello a"被打印在了文件中,而其他都没有。

    都是一起重定向到文件中,为什么其他两个没有显示?

    这是close(1)的问题:由于重定向到文件中,那就是全缓冲策略,和前面一样,由于进程还没关闭就关掉了标准输出流1,导致内容在C缓冲区中刷新不出来。

    那为什么"hello a"可以?

    因为write是写方式,这是系统调用,不需要通过C缓冲区,自然也就不存在“缓冲区还未满,等待进程结束再刷新到OS缓冲区”的过程。

    从上面的例子可以知道,平时我们打开文件以后需要关闭对应的文件描述符,就是因为不这样做数据刷新不出来。

    1.3.2 系统调用的问题

    观察下列代码

    int main()
    {
        const char* a = "hello a\n";
        write(1,a,strlen(a));
        printf("hello printf\n");
        fprintf("hello fprintf\n");
        fork();//创建子进程
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可以看见最后创建了子进程。正常运行没有问题,都打印在了显示器;但是将其可执行程序重定向到任一文件后,发现不仅有"hello a",还有两倍的"hello printf"和"hello fprintf"。这又是为什么?

    这是因为C函数打印本质上是在向stdout打印,而stdout是FILE*类型,会找到FILE结构体,从而找到其封装的对应缓冲区信息再找到对应缓冲区buffer,然后进行写入。

    又因为这是父进程的缓冲区(子进程一模一样拷贝了一份),这个缓冲区是用户层提供,也就是C缓冲区,并且是重定向到文件里,刷新策略是全缓冲,所以等到进程结束后就会刷新到文件里。
    在这里插入图片描述

    但是这个过程相当于子进程对共有的内容进行了修改,就会引发写时拷贝,那么子进程也会刷新一次,导致父子进程都刷新一次就打印了两次。

    本质还是因为刷新策略改变了,所以同样的采用强制刷新的方式可以只打印一次,这是因为直接将内容刷新到OS缓冲区,进程结束后C缓冲区里已经没有内容,也就不会引发写时拷贝,最后只打印OS缓冲区里的一次。

    可是为什么write只有一份?

    这就验证了平时我们说的缓冲区其实是用户层的C缓冲区,而不是存在于操作系统内核里,否则的话全部都会刷新两次。

    • 类比

    C++打印中的std::endl其实也是行缓冲的道理,endl作用就是刷新缓冲区的数据到显示器中。

  • 相关阅读:
    Android帧绘制流程深度解析 (二)
    MySQL8(增删改)
    Mysql互不关联的联表查询(减少了查询的次数)
    对前端构建工具的一些理解
    汇川PLC学习Day2:编写检测IO端口状态程序
    中创算力:打造区块链产业生态,助力郑州创建国家级区块链先导区​
    恶意代码防范技术笔记(十)
    Immutable!任何变更都需要发布
    vue中全局修改elementui,message修改时长
    misc刷题记录2[陇剑杯 2021]
  • 原文地址:https://blog.csdn.net/weixin_52669146/article/details/127559234