1、标准 I/O 库简介
标准 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 在不同操作系统之间相比于文件 I/O 具有更好的可移植性。
⚫ 性能、效率:标准 I/O 库在用户空间维护了自己的 stdio 缓冲区,所以标准 I/O 是带有缓存的,而文件 I/O 在用户空间是不带有缓存的,所以在性能、效率上,标准 I/O 要优于文件 I/O。
2、FILE 指针
FILE 指针的作用相当于文件描述符,只不过 FILE 指针用于标准 I/O 库函数中、而文件描述符则用于文件I/O 系统调用中。
FILE 是一个结构体数据类型,它包含了标准 I/O 库函数为管理文件所需要的所有信息,包括用于实际I/O 的文件描述符、指向文件缓冲区的指针、缓冲区的长度、当前缓冲区中的字节数以及出错标志等。FILE数据结构定义在标准 I/O 库函数头文件 stdio.h 中。
3、标准输入、标准输出和标准错误
所谓标准输入设备指的就是计算机系统的标准的输入设备,通常指的是计算机所连接的键盘;而标准输出设备指的是计算机系统中用于输出标准信息的设备,通常指的是计算机所连接的显示器;标准错误设备则指的是计算机系统中用于显示错误信息的设备,通常也指的是显示器设备。
每个进程启动之后都会默认打开标准输入、标准输出以及标准错误,得到三个文件描述符,即 0、1、2,其中 0 代表标准输入、1 代表标准输出、2 代表标准错误;在应用编程中可以使用宏 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 分别代表 0、1、2,这些宏定义在 unistd.h 头文件中:
/* Standard file descriptors. */
#define STDIN_FILENO 0 /* Standard input. */
#define STDOUT_FILENO 1 /* Standard output. */
#define STDERR_FILENO 2 /* Standard error output. */
0、1、2 这三个是文件描述符,只能用于文件 I/O(read()、write()等),那么在标准 I/O 中,自然是无法使用文件描述符来对文件进行 I/O 操作的,它们需要围绕 FILE 类型指针来进行,在 stdio.h 头文件中有相应的定义,如下:
/* Standard streams. */
extern struct _IO_FILE *stdin; /* Standard input stream. */
extern struct _IO_FILE *stdout; /* Standard output stream. */
extern struct _IO_FILE *stderr; /* Standard error output stream. */
/* C89/C99 say they're macros. Make them happy. */
#define stdin stdin
#define stdout stdout
#define stderr stderr
Tips:struct _IO_FILE 结构体就是 FILE 结构体,使用了 typedef 进行了重命名。
所以,在标准 I/O 中,可以使用 stdin、stdout、stderr 来表示标准输入、标准输出和标准错误。
4、打开文件 fopen()
#include
FILE *fopen(const char *path, const char *mode);
函数参数和返回值含义如下:
path:参数 path 指向文件路径,可以是绝对路径、也可以是相对路径。
mode:参数 mode 指定了对该文件的读写权限,是一个字符串,稍后介绍。
返回值:调用成功返回一个指向 FILE 类型对象的指针(FILE *),该指针与打开或创建的文件相关联,后续的标准 I/O 操作将围绕 FILE 指针进行。如果失败则返回 NULL,并设置 errno 以指示错误原因。
使用示例
使用只读方式打开文件:
fopen(path, "r");
使用可读、可写方式打开文件:
fopen(path, "r+");
使用只写方式打开文件,并将文件长度截断为 0,如果文件不存在则创建该文件:
fopen(path, "w");
5、fclose()关闭文件
#include
int fclose(FILE *stream);
参数 stream 为 FILE 类型指针,调用成功返回 0;失败将返回 EOF(也就是-1),并且会设置 errno 来指示错误原因。
#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);
使用示例
#include
#include
int main(void)
{
char buf[] = "Hello World!\n";
FILE *fp = NULL;
/* 打开文件 */
if (NULL == (fp = fopen("./test_file", "w")))
{
perror("fopen error");
exit(-1);
}
printf("文件打开成功!\n");
/* 写入数据 */
if(sizeof(buf) > fwrite(buf,1,sizeof(buf),fp))//指向的缓冲区中的数据写入到文件中 每个数据项的字节大小 数据项个数 FILE 指针
{
printf("fwrite error\n");
fclose(fp);
exit(-1);
}
printf("数据写入成功!\n");
/* 关闭文件 */
fclose(fp);
exit(0);
}
首先使用 fopen()函数将当前目录下的 test_file 文件打开,调用 fopen()时 mode 参数设置为"w",表示以只写的方式打开文件,并将文件的长度截断为 0,如果指定文件不存在则创建该文件。打开文件之后调用fwrite()函数将"Hello World!"字符串数据写入到文件中。
#include
#include
int main(void)
{
char buf[50] = {0};
FILE *fp = NULL;
int size;
/* 打开文件 */
if (NULL == (fp = fopen("./test_file", "r"))) {
perror("fopen error");
exit(-1);
}
printf("文件打开成功!\n");
/* 读取数据 */
if (12 > (size = fread(buf, 1, 12, fp)))
{
if (ferror(fp))
{ //使用 ferror 判断是否是发生错误
printf("fread error\n");
fclose(fp);
exit(-1);
}
/* 如果未发生错误则意味着已经到达了文件末尾 */
}
printf("成功读取%d 个字节数据: %s\n", size, buf);
/* 关闭文件 */
fclose(fp);
exit(0);
}
1、fseek 定位
#include
int fseek(FILE *stream, long offset, int whence);
函数参数和返回值含义如下:
stream:FILE 指针。
offset:与 lseek()函数的 offset 参数意义相同。
whence:与 lseek()函数的 whence 参数意义相同。
返回值:成功返回 0;发生错误将返回-1,并且会设置 errno 以指示错误原因;与 lseek()函数的返回值意义不同,这里要注意!
将文件的读写位置移动到文件开头处:
fseek(file, 0, SEEK_SET);
将文件的读写位置移动到文件末尾:
fseek(file, 0, SEEK_END);
将文件的读写位置移动到 100 个字节偏移量处:
fseek(file, 100, SEEK_SET);
2、ftell()函数
库函数 ftell()可用于获取文件当前的读写位置偏移量
#include
long ftell(FILE *stream);
#include
#include
int main(void)
{
FILE *fp = NULL;
int ret;
/* 打开文件 */
if (NULL == (fp = fopen("./testApp.c", "r"))) {
perror("fopen error");
exit(-1);
}
printf("文件打开成功!\n");
/* 将读写位置移动到文件末尾 */
if (0 > fseek(fp, 0, SEEK_END)) {
perror("fseek error");
fclose(fp);
exit(-1);
}
/* 获取当前位置偏移量 */
if (0 > (ret = ftell(fp))) {
perror("ftell error");
fclose(fp);
exit(-1);
}
printf("文件大小: %d 个字节\n", ret);
/* 关闭文件 */
fclose(fp);
exit(0);
}
首先打开当前目录下的 testApp.c 文件,将文件的读写位置移动到文件末尾,然后再获取当前的位置偏移量,也就得到了整个文件的大小。
#include
int feof(FILE *stream);
#include
int ferror(FILE *stream);
#include
void clearerr(FILE *stream);
#include
#include
int main(void)
{
FILE *fp = NULL;
char buf[20] = {0};
/* 打开文件 */
if (NULL == (fp = fopen("./testApp.c", "r")))
{
perror("fopen error");
exit(-1);
}
printf("文件打开成功!\n");
/* 将读写位置移动到文件末尾 */
if (0 > fseek(fp, 0, SEEK_END))
{
perror("fseek error");
fclose(fp);
exit(-1);
}
/* 读文件 */
if (10 > fread(buf, 1, 10, fp))
{
if (feof(fp))
printf("end-of-file 标志被设置,已到文件末尾!\n");
clearerr(fp); //清除标志
}
/* 关闭文件 */
fclose(fp);
exit(0);
}
#include
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int dprintf(int fd, const char *format, ...);
int sprintf(char *buf, const char *format, ...);
int snprintf(char *buf, size_t size, const char *format, ...);
#include
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);
控制文件 I/O 内核缓冲的系统调用
1、fsync()函数
系统调用 fsync()将参数 fd 所指文件的内容数据和元数据写入磁盘,只有在对磁盘设备的写入操作完成之后,fsync()函数才会返回,其函数原型如下所示:
#include
int fsync(int fd);
参数 fd 表示文件描述符,函数调用成功将返回 0,失败返回-1 并设置 errno 以指示错误原因。
使用示例
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 4096
#define READ_FILE "./rfile"
#define WRITE_FILE "./wfile"
static char buf[BUF_SIZE];
int main(void)
{
int rfd, wfd;
size_t size;
/* 打开源文件 */
rfd = open(READ_FILE, O_RDONLY);
if (0 > rfd)
{
perror("open error");
exit(-1);
}
/* 打开目标文件 */
wfd = open(WRITE_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0664);
if (0 > wfd)
{
perror("open error");
exit(-1);
}
/* 拷贝数据 */
while(0 < (size = read(rfd, buf, BUF_SIZE)))
write(wfd, buf, size);
/* 对目标文件执行 fsync 同步 */
fsync(wfd);
/* 关闭文件退出程序 */
close(rfd);
close(wfd);
exit(0);
}
2、fdatasync()函数
#include
int fdatasync(int fd);
3、sync()函数
#include
void sync(void);
在 Linux实现中,调用 sync()函数仅在所有数据已经写入到磁盘设备之后才会返回;然后在其它系统中,sync()实现只是简单调度一下 I/O 传递,在动作未完成之后即可返回。
控制文件 I/O 内核缓冲的标志
1、O_DSYNC 标志
在调用 open()函数时,指定 O_DSYNC 标志,其效果类似于在每个 write()调用之后调用 fdatasync()函数进行数据同步。
fd = open(filepath, O_WRONLY | O_DSYNC);
2、O_SYNC 标志
在调用 open()函数时,指定 O_SYNC 标志,使得每个 write()调用都会自动将文件内容数据和元数据刷新到磁盘设备中,其效果类似于在每个 write()调用之后调用 fsync()函数进行数据同步,譬如:
fd = open(filepath, O_WRONLY | O_SYNC);
在程序中频繁调用 fsync()、fdatasync()、sync()(或者调用 open 时指定 O_DSYNC 或 O_SYNC 标志)对性能的影响极大,大部分的应用程序是没有这种需求的,所以在大部分应用程序当中基本不会使用到。
Linux 允许应用程序在执行文件 I/O 操作时绕过内核缓冲区,从用户空间直接将数据传递到文件或磁盘设备,把这种操作也称为直接 I/O(direct I/O)或裸 I/O(raw I/O)。直接 I/O 只在一些特定的需求场合,譬如磁盘速率测试工具、数据库系统等。
使用直接I/O需至 Linux 内核 2.4.10 版本开始生效,譬如:
fd = open(filepath, O_WRONLY | O_DIRECT);
直接 I/O 的对齐限制
在执行直接 I/O 时,必须要遵守以下三个对齐限制要求:
⚫ 应用程序中用于存放数据的缓冲区,其内存起始地址必须以块大小的整数倍进行对齐;
⚫ 写文件时,文件的位置偏移量必须是块大小的整数倍;
⚫ 写入到文件的数据大小必须是块大小的整数倍。
使用 tune2fs 命令进行查看磁盘分区块大小,如下所示:
tune2fs -l /dev/sda1 | grep "Block size"
标准 I/O(fopen、fread、fwrite、fclose、fseek 等)是 C 语言标准库函数,而文件 I/O(open、read、write、close、lseek 等)是系统调用,虽然标准 I/O 是在文件 I/O 基础上进行封装而实现(譬如 fopen 内部实际上调用了 open、fread 内部调用了 read 等),但在效率、性能上标准 I/O 要优于文件 I/O,其原因在于标准 I/O 实现维护了自己的缓冲区,我们把这个缓冲区称为 stdio 缓冲区。
1、对 stdio 缓冲进行设置
1.1、setvbuf()函数
调用 setvbuf()库函数可以对文件的 stdio 缓冲区进行设置,譬如缓冲区的缓冲模式、缓冲区的大小、起始地址等。
#include
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。
⚫ _IOLBF:采用行缓冲 I/O。在这种情况下,当在输入或输出中遇到换行符"\n"时,标准 I/O 才会执行文件 I/O 操作。
⚫ _IOFBF:采用全缓冲 I/O。在这种情况下,在填满 stdio 缓冲区后才进行文件 I/O 操作(read、write)。对于输出流,当 fwrite 写入文件的数据填满缓冲区时,才调用 write()将 stdio 缓冲区中的数据刷入内核缓冲区;对于输入流,每次读取 stdio 缓冲区大小个字节数据。默认普通磁盘上的常规文件默认常用这种缓冲模式。
size:指定缓冲区的大小。
返回值:成功返回 0,失败将返回一个非 0 值,并且会设置 errno 来指示错误原因。
1.2、setbuf()函数
#include
void setbuf(FILE *stream, char *buf);
1.3、setbuffer()函数
#include
void setbuffer(FILE *stream, char *buf, size_t size);
标准输出 printf()的行缓冲模式测试
#include
#include
#include
int main(void)
{
printf("Hello World!\n");
printf("Hello World!");
for ( ; ; )
sleep(1);
}
例:
#include
#include
#include
int main(void)
{
/* 将标准输出设置为无缓冲模式 */
if (setvbuf(stdout, NULL, _IONBF, 0)) {
perror("setvbuf error");
exit(0);
}
printf("Hello World!\n");
printf("Hello World!");
for ( ; ; )
sleep(1);
}
在使用 printf()之前,调用 setvbuf()函数将标准输出的 stdio 缓冲设置为无缓冲模式,接着编译运行:
刷新 stdio 缓冲区
无论我们采取何种缓冲模式,在任何时候都可以使用库函数 fflush()来强制刷新(将输出到 stdio 缓冲区中的数据写入到内核缓冲区,通过 write()函数)。
#include
int fflush(FILE *stream);
参数 stream 指定需要进行强制刷新的文件,如果该参数设置为 NULL,则表示刷新所有的 stdio 缓冲区。
函数调用成功返回 0,否则将返回-1,并设置 errno 以指示错误原因。
#include
#include
#include
int main(void)
{
printf("Hello World!\n");
printf("Hello World!");
fflush(stdout); //刷新标准输出 stdio 缓冲区
for ( ; ; )
sleep(1);
}
1、关闭文件时刷新 stdio 缓冲区
#include
#include
#include
int main(void)
{
printf("Hello World!\n");
printf("Hello World!");
fclose(stdout); //关闭标准输出
for ( ; ; )
sleep(1);
}
2、程序退出时刷新 stdio 缓冲区
#include
#include
#include
int main(void)
{
printf("Hello World!\n");
printf("Hello World!");
}
关于刷新 stdio 缓冲区相关内容,最后进行一个总结:
⚫ 调用 fflush()库函数可强制刷新指定文件的 stdio 缓冲区;
⚫ 调用 fclose()关闭文件时会自动刷新文件的 stdio 缓冲区;
⚫ 程序退出时会自动刷新 stdio 缓冲区(注意区分不同的情况)。
3、I/O 缓冲小节
从图中自上而下,首先应用程序调用标准 I/O 库函数将用户数据写入到 stdio 缓冲区中,stdio 缓冲区是由 stdio 库所维护的用户空间缓冲区。针对不同的缓冲模式,当满足条件时,stdio 库会调用文件 I/O(系统调用 I/O)将 stdio 缓冲区中缓存的数据写入到内核缓冲区中,内核缓冲区位于内核空间。最终由内核向磁盘设备发起读写操作,将内核缓冲区中的数据写入到磁盘(或者从磁盘设备读取数据到内核缓冲区)。应用程序调用库函数可以对 stdio 缓冲区进行相应的设置,设置缓冲区缓冲模式、缓冲区大小以及由调用者指定一块空间作为 stdio 缓冲区,并且可以强制调用 fflush()函数刷新缓冲区;而对于内核缓冲区来说,应用程序可以调用相关系统调用对内核缓冲区进行控制,譬如调用 fsync()、fdatasync()或 sync()来刷新内核缓冲区(或通过 open 指定 O_SYNC 或 O_DSYNC 标志),或者使用直接 I/O 绕过内核缓冲区(open 函数指定 O_DIRECT 标志)。
在应用程序中,在同一个文件上执行 I/O 操作时,还可以将文件 I/O(系统调用 I/O)与标准 I/O 混合使用,这个时候我们就需要将文件描述符和 FILE 指针对象之间进行转换,此时可以借助于库函数 fdopen()、fileno()来完成。
库函数 fileno()可以将标准 I/O 中使用的 FILE 指针转换为文件 I/O 中所使用的文件描述符,而 fdopen()则进行着相反的操作,其函数原型如下所示:
#include
int fileno(FILE *stream);
FILE *fdopen(int fd, const char *mode);
当混合使用文件 I/O 和标准 I/O 时,需要特别注意缓冲的问题,文件 I/O 会直接将数据写入到内核缓冲区进行高速缓存,而标准 I/O 则会将数据写入到 stdio 缓冲区,之后再调用 write()将 stdio 缓冲区中的数据写入到内核缓冲区。譬如下面这段代码:
#include
#include
#include
int main(void)
{
printf("print");
write(STDOUT_FILENO, "write\n", 6);
exit(0);
}
执行结果你会发现,先输出了"write"字符串信息,接着再输出了"print"字符串信息。