标准I/O不仅仅是对文件IO的封装,还会处理很多其他细节
本章主要介绍下面几个点
所谓标准IO库,是标准C库中用于文件IO操作的一系列函数的集合,通常位于
既然有系统IO(文件IO)了,为什么还要用标准IO,直接使用文件IO不就好了?
并非如此,前面也讲了,设计库函数目的:更好用、更方便、更高效,标准IO和文件IO区别如下
前面章节将的系统调用,open、read、write、lseek等,都是围绕文件描述符进行的,而标准IO,都是围绕FILE类型对象的指针进行的,当使用标准IO库函数打开文件时,会返回一个指向FILE类型对象的指针FILE*,使用该FILE指针与被打开的文件相关联,然后该指针就能用于后续的IO操作,由此可见,FILE指针作用相当于文件描述符
FILE是一个结构体类型数据,它包含了标准IO库函数为管理文件所需的所有信息,包括
FILE定义在stdio.h中
文件io都是直接通过系统调用open打开或创建文件的,而在标准IO中,我们将使用fopen
- #include
- FILE *fopen(const char *path, const char *mode);
成功则返回FILE指针,失败返回NULL
- #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参数类似,不赘述。。。
- #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字节偏移处
用于获取文件当前的读写位置偏移量
- #include
-
- long ftell(FILE *stream);
成功:返回偏移量
失败:-1
出于速度和效率的考虑,系统IO调用和标准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都会执行一次磁盘操作。
可以强制将内核缓冲区的数据写入到磁盘中
场景:
ubuntu下拷贝文件到U盘时,we年拷贝完成后,通常在拔掉U盘之前,执行sync命令进行同步操作,这个同步就是将文件IO内核缓冲区数据更新到U盘硬件设备中。如果没有执行sync命令就把掉U盘,很可能将数据破坏掉。
Linux中提供了一些系统调用用于控制文件IO内核缓冲、
a. fsync函数
- #include
- int fsync(int fd);
将fd指定的文件的文件内容和元数据,只有在磁盘设备写入完成后,fync函数才会返回。
元数据:元数据并不是文件内容本身的数据,而是一些用于记录文件属性相关的数据信息,如:文件大小,时间戳,权限...
b. sync函数
- #include
- void sync(void);
将内核缓冲区中所有的缓存和元数据都写入到磁盘中。
从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是在文件IO基础上进行的封装,但是在效率上,性能上,标准IO要由于文件IO,其原因是在于标准IO实现维护了自己的缓冲区,即stdio缓冲区。
C提供了一些库函数,可以对标准IO进行缓冲区设置,包括:setbuf()、setbuffer()、setvbuf()
具体细节不在详述。
图中自上而下,首先应用程序调用标准IO库函数将用户数据写到stdio缓冲区中,stdio缓冲区是由stdio库所维护的用户空间缓冲区,针对不用的缓冲模式,当满足条件时,stdio库会调用文件IO将stdio缓冲区中缓存的数据写入到内核缓冲区中,内核缓冲区位于内核空间。最终由内核向磁盘设备发起读写操作,将内核缓冲数据写入到磁盘中(或者从磁盘读取到内核缓冲区中)。
应用程序可以调用库函数对stdio进行设置:
对内核缓冲区来说,应用程序可以调用指定的系统调用对内核缓冲区进行控制: