• 第四章 标准IO库


    标准I/O不仅仅是对文件IO的封装,还会处理很多其他细节

    • 分配stdio缓冲区
    • 以优化的块长度执行IO
    • ...

    本章主要介绍下面几个点

    • 标准IO库简介
    • 流和FILE对象
    • 标准输入/出/错误
    • 使用标准IO打开、读写、关闭文件
    • 格式化IO
    • 文件IO缓冲,内核缓冲区和stdio缓冲区
    • 文件IO与标准IO混合编程

    1. 标准IO库简介

    所谓标准IO库,是标准C库中用于文件IO操作的一系列函数的集合,通常位于

    • fopen
    • fread
    • fwrite
    • ...

    既然有系统IO(文件IO)了,为什么还要用标准IO,直接使用文件IO不就好了?

    并非如此,前面也讲了,设计库函数目的:更好用、更方便、更高效,标准IO和文件IO区别如下

    • 虽然标准IO和文件IO都是C语言函数,但是标准IO是标准C库函数,而文件IO时Linux系统调用
    • 标准IO是文件IO封装而来,标准IO内部是通过文件IO来实际操作的。
    • 标准IO比文件IO更易移植,不同操作系统系统调用可能不同,而不同操作系统都实现了标准IO,且标准IO的接口定义几乎都是一致的。所以移植性更好。
    • 性能,效率,标准IO库在用户层维护了自己的stdio缓冲区,所以标准IO是带缓冲的,而文件IO在用户空间是不带缓冲的,所以在性能上,标准IO优于文件IO

    2. FILE指针

    前面章节将的系统调用,open、read、write、lseek等,都是围绕文件描述符进行的,而标准IO,都是围绕FILE类型对象的指针进行的,当使用标准IO库函数打开文件时,会返回一个指向FILE类型对象的指针FILE*,使用该FILE指针与被打开的文件相关联,然后该指针就能用于后续的IO操作,由此可见,FILE指针作用相当于文件描述符

            FILE是一个结构体类型数据,它包含了标准IO库函数为管理文件所需的所有信息,包括

    • 文件描述符
    • 指向文件缓冲区的指针
    • 缓冲区长度
    • 当前缓冲区中的字节数及错误标志

    FILE定义在stdio.h中

    2.1 文件操作

    • fopen

    文件io都是直接通过系统调用open打开或创建文件的,而在标准IO中,我们将使用fopen

    1. #include
    2. FILE *fopen(const char *path, const char *mode);

    成功则返回FILE指针,失败返回NULL

    • fclose
    • fread、fwrite
    1. #include
    2. size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
    3. size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

    fread参数:

    ptr,将读取到的数据存放在ptr缓冲区中

    size: fread从文件读取nmenb个数据项,每个数据项的大小为size字节,所以总共读取size *         nmenb字节

    fwrite参数类似,不赘述。。。

    • fseek
    1. #include
    2. int fseek(FILE *stream, long offset, int whence);

    fseek作用类似于lseek,用于设置文件读写的偏移量,lseek作用于系统调用,fseek作用于标准IO

    返回值:成功返回0,失败返回-1,并会将错误原因,设置到errno中

    使用库函数fread、fwrite读写文件时,文件的读写位置偏移量会自动递增,使用fseek可手动设置文件当前的读写位置偏移量

    如:

    fseek(file, 0, SEEK_SET);  将文件的读写位置,移动到文件开头处

    fseek(file, 0, SEEK_END); 将文件读写位置,移动到文件末尾

    seek(file, 100, SEEK_SET); 将文件读写位置,偏移到100字节偏移处

    • ftell

    用于获取文件当前的读写位置偏移量

    1. #include
    2. long ftell(FILE *stream);

    成功:返回偏移量

    失败:-1

    3. IO缓冲

            出于速度和效率的考虑,系统IO调用和标准IO在操作磁盘文件时,都会对数据进行缓冲。

    3.1. 系统IO的内核缓冲

            read、write系统调用在进行文件读写时,不会直接访问磁盘设备,而是仅仅在用户空间缓冲区和内核缓冲区之间复制数据,如

    write(fd, "Hello", 5) // 写入5个字节

            调用write后,仅仅是将5个字节拷贝到内核缓冲区中,拷贝完成后就会返回,在后面某个时刻,内核会将内核缓冲区数据写入(刷新)到磁盘设备上,由此可以,系统调用weite与磁盘操作,并不是同步的,write函数并不会等到数据真正写入到磁盘之后再返回。如果在此期间,其他进程调用read函数读取该文件的这几个字节数据,那么内核将自动从缓冲区中读取这几个字节数据,返回给应用程序。

            同理,读文件时,内核会从磁盘中读取文件数据,并存储到内核缓冲区中,当调用read读取数据时,将从缓冲区中读取数据,直至把缓冲区读完,这次内核会将文件的下一段内容读入到内核缓冲区中进行缓存。

            我们将这个内核缓冲区称为文件IO内核缓冲,这样的设计,目的是为了提高文件IO速度和效率,使得系统调用read、write更快更高效,不需要等待磁盘操作,譬如:

    线程1调用write向文件写入"aaaa",

    线程2也调用write向文件写入“bbbb”

    这样的话,数据会被缓冲到内核缓冲区中,在稍后,内核会将他们一起写入磁盘,只发起一次磁盘操作请求,加入没有内核缓冲区,每次调用write都会执行一次磁盘操作。

    3.2. 刷新文件IO的内核缓冲区

    可以强制将内核缓冲区的数据写入到磁盘中

    场景:

            ubuntu下拷贝文件到U盘时,we年拷贝完成后,通常在拔掉U盘之前,执行sync命令进行同步操作,这个同步就是将文件IO内核缓冲区数据更新到U盘硬件设备中。如果没有执行sync命令就把掉U盘,很可能将数据破坏掉。

    Linux中提供了一些系统调用用于控制文件IO内核缓冲、

    • synv
    • syncfs
    • fsync

    a. fsync函数

    1. #include
    2. int fsync(int fd);

    将fd指定的文件的文件内容元数据,只有在磁盘设备写入完成后,fync函数才会返回。

    元数据:元数据并不是文件内容本身的数据,而是一些用于记录文件属性相关的数据信息,如:文件大小,时间戳,权限...

    b. sync函数

    1. #include
    2. void sync(void);

    将内核缓冲区中所有的缓存和元数据都写入到磁盘中。

    3.3. 直接IO: 绕过内核缓冲

    从Linux内核2.4开始,就可以直接io了,某些情况下适用:

    如,某程序作用是测试磁盘设备的读写速率,这种情况下,read、write需要直接访问磁盘设备

    直接IO效率很低,因为内核针对文件IO内核缓冲区做了不少的优化,如按顺序预读取、在成簇磁盘块上执行IO、允许访问同一文件的多个线程共享高速缓冲区。

    fd = open(filepath, O_WRONLY | O_DIRECT)

    使用O_DIRECT可以进行直接IO

    因为直接IO是对磁盘的直接访问,所以执行时,需要遵守三个对齐原则要求,否则会返回错误

    • 应用程序中存放数据的缓冲区,起始地址必须是块大小的整数倍
    • 写文件时,文件偏移量必须是块大小的整数倍
    • 写入文件数据大小必须是块大小整数倍

    1. ** 定义一个用于存放数据的 buf,起始地址以 4096 字节进行对其 **/
    2. static char buf[8192] __attribute((aligned (4096)));

    tune2fs -l /dev/sda1 | grep "Block size" 
    

    可以查看块大小,-l后面指定了需要查看的磁盘分区。

    /dev/sda1是ubuntu系统的根文件系统所挂载的磁盘分区

    使用df -h查看

    3.4.  stdio缓冲

    虽然标准IO是在文件IO基础上进行的封装,但是在效率上,性能上,标准IO要由于文件IO,其原因是在于标准IO实现维护了自己的缓冲区,即stdio缓冲区。

    C提供了一些库函数,可以对标准IO进行缓冲区设置,包括:setbuf()、setbuffer()、setvbuf()

    具体细节不在详述。

    3.5. IO缓冲小结

     图中自上而下,首先应用程序调用标准IO库函数将用户数据写到stdio缓冲区中,stdio缓冲区是由stdio库所维护的用户空间缓冲区,针对不用的缓冲模式,当满足条件时,stdio库会调用文件IO将stdio缓冲区中缓存的数据写入到内核缓冲区中,内核缓冲区位于内核空间。最终由内核向磁盘设备发起读写操作,将内核缓冲数据写入到磁盘中(或者从磁盘读取到内核缓冲区中)。

    应用程序可以调用库函数对stdio进行设置:

    • 缓冲区缓冲模式
    • 缓冲区大小
    • 指定空间作为stdio缓冲区
    • 强制刷新缓冲区fflush

    对内核缓冲区来说,应用程序可以调用指定的系统调用对内核缓冲区进行控制:

    • fsync。。。刷新内核缓冲区
    • 直接IO绕过内核缓冲区

  • 相关阅读:
    大模型分布式训练并行技术(六)-多维混合并行
    【C语言深入理解指针(4)】
    SpringBoot 如何实现文件上传和下载
    【iOS】音频中断
    有才有料有趣,聊聊技术Demo的二三事
    华为终于要“三分天下”
    Java实现Excel的导入以及导出,极其简单
    VUE的10个常用指令
    有自动交易股票的软件么,怎么实现全自动交易?
    基于51单片机环境监测控制系统-proteus仿真-源程序
  • 原文地址:https://blog.csdn.net/bocai1215/article/details/126634177