一、标准 I/O 库简介
标准I/O 库 是标准C 库中用于文件I/O 操作(例如读文件、写文件等)相关的一系列库函数的集合。
1. 标准I/O 和文件I/O 的区别:
- 虽然标准I/O 和文件I/O 都是C 语言函数,但是标准I/O 是标准C 库函数,而文件I/O 则是Linux 系统调用;
- 标准I/O 是由文件I/O 封装而来,标准I/O 内部实际上是调用文件I/O 来完成实际操作;
- 可移植性:标准I/O 相比于文件I/O ,具有更好的可移植性,通常对于不同的操作系统,其内核向应用层提供的系统调用往往都是不同,例如系统调用的定义、功能、参数列表、返回值等往往都是不一样的;而对于标准I/O 来说,由于很多操作系统都实现了标准I/O 库,标准I/O 库在不同的操作系统之间其接口定义几乎都一样的。
- 性能、效率:标准I/O 库在用户空间维护了自己的stdio 缓冲区,所以标准I/O 是带有缓存的;而文件I/O 在用户空间是不带缓存的,所以在性能、效率上,标准I/O 要优于文件I/O.
2. FILE 指针
文件I/O 函数(open(), read() , write(), lseek() )都是围绕文件描述符进行的。而对于标准I/O 库函数来说,他们的操作是围绕FILE 指针进行的。当使用标准I/O 库函数打开或创建一个文件时,会返回一个指向FILE 类型对象的指针(FILE* ),使用该FILE 指针与被打开或创建的文件相关联,然后该FILE 指针就用于后续的标准I/O 操作。
3 标准输入、标准输出、标准错误
3.1 文件I/O
标准输入 | 标准输出 | 标准错误 |
---|---|---|
文件描述符0 | 文件描述符1 | 文件描述符2 |
3.2 标准I/O
标准输入 | 标准输出 | 标准错误 |
---|---|---|
STDIN_FILENO | STDIN_FILENO1 | STDIN_FILENO2 |
或者
标准输入 | 标准输出 | 标准错误 |
---|---|---|
stdin | stdout | stderr |
二、 文件操作基本I/O
1. 打开文件 fopen()
#include <stdio.h>
FILE *fopen(const char *path, const char *mode);
参数与返回值:
- path : 指定文件路径,可以是绝对路径,也可以是相对路径。
- mode: 指定对该文件的读写权限,是一个字符串。
- 返回值:调用成功返回一个指向FILE 类型对象的指针;失败则返回NLL,并设置errno以指示错误原因。
mode 字符串说明
mode | 说明 | 对应于open函数的flags参数取值 |
---|---|---|
r | 以只读方式打开文件 | O_RDONLY |
r+ | 以可读、可写方式打开文件 | O_RDWR |
w | 以只写方式打开文件,如果文件存在,将文件长度截断为0;如果文件不存在,则创建文件 | O_WRONLY |O_CREAT|O_TRUNC |
w+ | 以可读可写方式打开文件,如果文件存在,将文件长度截断为0;如果文件不存在,则创建文件 | O_RDWR |O_CREAT|O_TRUNC |
a | 以只写 方式打开文件,打开以进行追加内容(在文件末尾写入)。如果文件不存在则创建该文件 | O_WRONLY|O_CREAT|O_APPEND |
a+ | 以可读可写方式打开文件,打开以进行追加内容(在文件末尾写入)。如果文件不存在则创建该文件 | O_RDWR|O_CREAT|O_APPEND |
2. 关闭文件 fclose()
调用fclose 库函数可以关闭一个由fopen打开的文件。
#include <stdio.h>
int fclose(FILE *stream);
参数stream 为FILE 类型指针,调用成功返回0;失败将返回EOF(也就是-1)。并且会设置errno 来指示错误原因。
3. 读文件 fread()
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
参数与返回值:
- ptr:将读取到的数据存放在参数ptr 指向的缓冲区中;
- size:从文件读取nmemb个数据项,每个数据项的大小为size 个字节 ,总共读取的数据大小为:nmemb * size 个字节。
- nmemb : 指定读取数据项的个数。
- stream : FILE 指针。
- 返回值:调用成功时返回读取到的数据项的数目;如果发生错误或到达文件末尾,则返回的值将小于参数nmemb,到底发生了错误还是到达了文件末尾,可以使用ferror()或feof() 函数来判断。
4. 写文件 fwrite()
库函数fwrite() 用于将数据写入到文件中。
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
参数与返回值:
- ptr:将ptr指向的缓冲区中的数据写入到文件中;
- size:指定每个数据项的字节大小。
- nmemb : 指定写入的数据项的个数。
- stream : FILE 指针。
- 返回值:调用成功时返回写入的数据项的数目;如果发生错误,则返回的值将小于nmemb(或者等于0)
5. 文件定位 fseek()
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
- stream : FILE 指针
- offset : 偏移量,以字节为单位。
- whence:用于定义参数offset 偏移量对应的参考值。该参数为下列其中一种:
参数 | 说明 |
---|---|
SEEK_SET | 读写偏移量将指向offset 字节位置处(从文件头部开始算) |
SEEK_CUR | 读写偏移量将指向当前位置偏移量+offset 字节位置处,offset 可以为正、也可以为负,如果是正数表示往后偏移,如果是负数则表示往前偏移 |
SEEK_END | 读写偏移量将指向末尾 + offset 字节位置处,offset 可以为正,也可以为负。 |
fseek 使用示例
将文件的读写位置移动到文件开头处:
fseek( file , 0 , SEEK_SET )
将文件的读写位置移动到文件末尾处:
fseek( file , 0 , SEEK_END )
将文件的读写位置移动到100个字节偏移处
fseek( file , 100 , SEEK_SET)
6. 返回文件偏移量 ftell()
库函数ftell() 可用于获取文件当前的读写位置偏移量。
#include <stdio.h>
long ftell(FILE *stream);
参数stream指向对应的文件,函数调用成功将返回当前读写位置偏移量;调用失败将返回-1,并会设置errno 以指示错误原因。
三、 检查或复位文件状态
1. 文件末检测 feof()
当文件的读写位置移动到文件末尾时,end-of-file 标志将会被设置。feof() 返回一个非零值。如果end-of-file 标志没有被设置,则返回0。
#include <stdio.h>
int feof(FILE *stream);
2. 文件错误标志 ferror()
库函数ferror() 用于测试参数stream 所指文件的错误标志,如果错误标志被设置了,则调用ferror()函数将返回一个非零值,如果错误标志没有被设置,则返回0 。
#include <stdio.h>
int ferror(FILE *stream);
3. 错误清除 clearerr()
库函数clearerr() 用于清除end-of-file 标志和错误标志,当调用feof() 或ferror()校验这些标志后,通常需要清除这些标志,避免下次校验时使用到的是上一次设置的值。
#include <stdio.h>
void clearerr(FILE *stream);
对于end-of-file 标志,除了使用clearerr() 显式清除之外,当调用fseek()成功时也会清除文件的end-of-file 标志。
四、 格式化 I/O
1. 格式化输入
-
int scanf(const char *format, ...);
-
int fscanf(FILE *stream, const char *format, ...);
-
int sscanf(const char *str, const char *format, ...);
scanf()函数可将用户输入(标准输入)的数据进行格式化转换;fscanf() 函数从FILE 指针指定文件中读取数据,并将数据进行格式化转换;sscanf() 函数从参数str 所指向的字符串中读取数据,并将数据进行格式化转换。
2. 格式化输出
- 输出到标准输出
int printf(const char *format, ...);
- 输出到FILE指针指向的文件
int fprintf(FILE *stream, const char *format, ...);
- 输出到文件描述符指向的文件
int dprintf(int fd, const char *format, ...);
- 输出到用户指定的buf
int sprintf(char *buf, const char *format, ...);
- 输出到用户指定的buf
int snprintf(char *buf, size_t size, const char *format, ...);
这5个函数都是可变参数,它们都有一个共同的参数format ,这是一个字符串,称为格式化控制字符串,用于指定后续的参数如何进行格式转换。
使用示例
2.1 printf()函数
输出数字5.
printf("%d\n", 5);
2.2 fprintf() 函数
将格式化数据写入到由FILE 指针指定的文件中。函数调用成功返回写入到文件中的字符数;失败则返回一个负值。
将字符串“Hello World”写入到标准错误:
fprintf(stderr, "Hello World!\n");
向标准错误写入数字5:
fprintf(stderr,"%d\n", 5)
2.3 dprintf()函数
将格式化数据写入到由文件描述符fd指定的文件中。
例如,将字符串写入标准错误:
dprintf(STDERR_FILENO,"Hello World!\n")
2.4 sprintf 函数
将格式化数据写到由参数buf 所指定的缓冲区。sprintf()函数会在字符串末端自动加上一个字符串终止字符’\0’ 。
sprintf(buf,"the sum is:%d\n",100 )
需要注意的是,sprintf() 函数可能会造成由参数buf指定的缓冲区溢出,调用者有责任确保该缓冲区足够大。
2.5 snprintf 函数
sprintf 函数有可能会发生缓冲区溢出的问题,存在安全隐患,为了解决这个问题,引入了snprintf()函数;在该函数中,使用参数size 显式指定缓冲区的大小,如果写入到缓冲区的字节数大于参数size 指定的大小,超出的部分将会被丢弃。
五、I/O 缓冲
1. 文件I/O 的内核缓冲
1.1 文件I/O 内核缓冲的概述
read()和write()系统调用在进行文件读写操作的时候,并不会直接访问磁盘设备,而是仅仅在用户空间缓冲区和内核缓冲区(kernel buffer cache)之间复制数据。例如调用write() 函数将5个字节数据从用户空间拷贝到内核空间的缓冲区中:
write(fd,"hello",5);
调用write()后仅仅只是将5个字节数据拷贝到内核空间的缓冲区中,拷贝完成之后函数就返回了,在后面的某个时刻,内核会将其缓冲区中的数据写入(刷新)到磁盘设备中。由此可知,系统调用write()与磁盘操作并不是同步的。如果在此期间,其他进程调用read()函数读取该文件的这几个 字节数据,那么内核将自动从缓冲区中读取这几个字节数据返回给应用程序。
与此同理,对于读文件而言,内核会从磁盘设备中读取文件的数据并存储到内核的缓冲区中,当调用read()函数读取数据时,read()调用将从内核缓冲区中读取数据,直至缓冲区中的数据读完。这时,内核会将文件的下一段内容读入到内核缓冲区中进行缓存。
我们把这个内核缓冲区就称为文件I/O 的内核缓冲。这样的设计,目的是为了提高文件I/O 的速度和效率。
1.2 刷新文件I/O 的内核缓冲区
强制将文件I/O 内核缓冲区中缓存的数据写入磁盘设备中,对于某些应用来说,可能是很有必要的。Linux 中提供了一些系统调用可用于控制文件I/O 内核缓冲:
- sync()
- syncfs()
- fsync()
- fdatasync()
1.2.1 fsync() 函数
系统调用fsync() 将参数fd 所指文件的内容数据和元数据写入磁盘,只有在对磁盘设备的写入操作完成之后,fsync()函数才会返回。
#include <unistd.h>
int fsync(int fd);
参数fd 表示文件描述符,函数调用成功将返回0,失败返回-1,并设置errno 以指示错误原因。
元数据并不是文件内容本身的数据,而是一些用于记录文件属性相关的数据信息,例如文件大小,时间戳,权限等等信息,这里统称为文件的元数据,这些信息也是存储在磁盘设备中。
1.2.2 fdatasync() 函数
系统调用fdatasync() 与fsync()类似,不同之处在于fdatasync() 仅将参数fd 所指文件的内容数据写入磁盘,并不包括文件的元数据。同样,只有在对磁盘设备的写入操作完成之后,fdatasync() 函数才会返回。
#include <unistd.h>
int fdatasync(int fd);
1.2.3 sync() 函数
系统调用sync() 会将所有文件I/O 内核缓冲区中的文件内容和元数据全部更新到磁盘设备中,该函数没有参数,也无返回值。
#include <unistd.h>
void sync(void);
1.2.4 控制I/O 内核缓冲的标志
调用open() 函数时指定的一些标志也可以影响到文件I/O 内核缓冲。
O_DSYNC 标志
在调用open()函数时,指定O_DSYNC 标志,其效果类似于在每个write()调用之后调用fdatasync() 函数进行数据同步。
fd = open ( filepath,O_WRONLY | O_DSYNC);
O_SYNC 标志
在调用open()函数时,指定O_SYNC 标志,使得每个write() 调用都会自动将文件内容数据和元数据刷新到磁盘设备中,其效果类似于在每个write() 调用之后调用fsync() 函数进行数据同步。
fd = open(filepath,O_WRONLY|O_SYNC);
1.2.5 同步函数对性能的影响
在程序中频繁调用fsync(),fdatasync(),sync() (或者调用open时指定O_DSYNC 或 O_SYNC 标志)对性能的影响极大,大部分的应用程序都是没有这种需求的,所以在大部分应用程序当中基本不会使用到。
2.直接I/O :绕过内核缓冲
在Linux 内核2.4 版本开始,Linux 允许应用程序在执行文件I/O 操作时绕过内核缓冲区,从用户空间直接将数据传递到文件或磁盘设备,把这种操作也称为直接I/O (direct I/O) 或者裸I/O(raw I/O) 。
我们可针对某一个文件或块设备执行直接 I/O , 在调用open() 函数打开文件时,指定O_DIRECT 标志:
fd = open ( filepath, O_WRONLY| O_DIRECT);
3. stdio 缓冲
标准I/O 是在文件I/O 基础上进行封装而实现的,但是效率、性能上标准I/O 要优于文件I/O,其原因在于标准I/O 实现维护了自己的缓冲区,我们把这个缓冲区称为stdio 缓冲区。
3.1 实现原理
文件I/O 内核缓冲是由内核维护的缓冲区,而标准I/O 的stdio 缓冲是用户空间的缓冲区。当应用程序中通过标准I/O 操作磁盘文件时,为了减少调用系统调用的次数,标准I/O 函数会将用户写入或读取文件的数据缓存在stdio 缓冲区,然后再一次性将stdio 缓冲区中缓存的数据调用系统调用I/O 写入到文件I/O 内核缓冲区或者拷贝到应用程序的buf 中。
3.2 对stdio 缓冲进行设置
C 语言提供了一些库函数可用于对标准I/O 的stdio 缓冲区进行相关的一些设置,包括:
- setbuf()
- setbuffer()
- setvbuf()
3.2.1 setvbuf() 函数
可以对文件的stdio 缓冲区进行设置,例如缓冲区的缓冲模式,缓冲区的大小,起始地址。
#include <stdio.h>
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
参数和返回值
-
stream : FILE 指针,用于指定对应的文件,每一个文件都可以设置他对应的stdio 缓冲区。
-
buf : 如果参数buf 不为NULL, 那么buf 指向size 大小的内存区域将作为该文件的stdio 缓冲区。因为stdio库会使用buf 指向的缓冲区,所以应该以动态或静态的方式在堆中为该缓冲区分配一块空间,而不是分配在栈上的局部变量。如果buf等于NULL,那么stdio 库会自动分配一块空间作为该文件的stdio 缓冲区(除非参数mode配置为非缓冲模式)。
-
mode :用于指定缓冲区的缓冲类型,可取值如下:
- _IONBF : 不对I/O 进行缓冲(无缓冲)。意味着每个标准I/O 函数将立即调用write() 或者read() ,并且忽略buf 和 size 参数,可以分别指定两个参数为NULL 和0 。 标准错误stderr 默认属于这一种类型,从而保证错误信息能够立即输出。
- _IOLBF : 采用行缓冲I/O 。当在输入或输出中遇到换行符"\n" 时,标准I/O 才会执行文件I/O 操作。对于终端设备默认采用的就是行缓冲模式,例如标准输入和标准输出。
- _IOFBF : 采用全缓冲I / O 。在填满stdio 缓冲区后才进行文件I/O 操作(read,write)。对于输出流,当fwrite 写入文件的数据填满缓冲区时,才调用write()将stdio 缓冲区中的数据刷入内核缓冲区;对于输入流,每次读取stdio缓冲区大小个字节数据。默认普通磁盘上的常规文件默认常用这种缓冲模式。
-
size : 指定缓冲区的大小。
-
返回值: 成功返回0 ,失败将返回一个非0值,并且会设置errno 来指示错误原因。
当stdio 缓冲区中的数据被刷入到内核缓冲区或被读取之后,这些数据就不会存在于缓冲区中了。
3.2.2 setbuf ()函数
setbuf()函数构建于 setvbuf()之上,执行类似的任务。
#include <stdio.h>
void setbuf(FILE *stream, char *buf);
setbuf()调用除了不返回函数结果外,就相当于:
setvbuf(stream,buf, buf ? _IOFBF: _IONBF, BUFSIZ) ;
要么将buf设置为NULL,以表示无缓冲,要么指向由调用者分配的BUFSIZ 个字节大小的缓冲区。(BUFSIZ 定义于头文件<stdio.h>中,该值通常为8192)。
3.2.3 setbuffer() 函数
setbuffer() 函数类似于setbuf() ,但是允许调用者指定buf缓冲区的大小。
#include <stdio.h>
void setbuffer(FILE *stream, char *buf, size_t size);
相当于:
setvbuf(stream, buf, buf ? _IOFBF : _IONBF, size);
3.3 刷新stdio缓冲区
- 调用fflush 库函数可强制刷新指定文件的stdio 缓冲区。
- 调用fclose 关闭文件会自动刷新文件的stdio 缓冲区。
- 程序退出时会自动刷新stdio缓冲区。这里分两种情况:
- 刷新stdio 缓冲区: 使用exit() ,return 或不显式调用相关退出函数时
- 不刷新stdio缓冲区:使用_exit 或者 _Exit()终止程序。