第四章 标准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

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

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

  • fclose
  • fread、fwrite
#include  
 
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); 
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
#include  
 
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

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

#include  
 
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函数

#include  
int fsync(int fd); 

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

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

b. sync函数

#include  
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是对磁盘的直接访问,所以执行时,需要遵守三个对齐原则要求,否则会返回错误

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

** 定义一个用于存放数据的 buf,起始地址以 4096 字节进行对其 **/ 

static char buf[8192] __attribute((aligned (4096))); 

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

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

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

使用df -h查看

第四章 标准IO库_第1张图片

3.4.  stdio缓冲

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

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

具体细节不在详述。

3.5. IO缓冲小结

第四章 标准IO库_第2张图片

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

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

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

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

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

你可能感兴趣的