Linux应用开发(标准I/O库)

一、标准IO

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 <stdio.h>
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 <stdio.h>
int fclose(FILE *stream);

参数 stream 为 FILE 类型指针,调用成功返回 0;失败将返回 EOF(也就是-1),并且会设置 errno 来指示错误原因。

二、读文件和写文件

#include <stdio.h>
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 <stdio.h>
#include <stdlib.h>
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 <stdio.h>
#include <stdlib.h>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 <stdio.h>
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 <stdio.h>
long ftell(FILE *stream);
#include <stdio.h>
#include <stdlib.h>
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 <stdio.h>
int feof(FILE *stream);
#include <stdio.h>
int ferror(FILE *stream);
#include <stdio.h>
void clearerr(FILE *stream);
#include <stdio.h>
#include <stdlib.h>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);
}

四、格式化 I/O

#include <stdio.h>
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 <stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);

五、I/O 缓冲

控制文件 I/O 内核缓冲的系统调用

1、fsync()函数

系统调用 fsync()将参数 fd 所指文件的内容数据和元数据写入磁盘,只有在对磁盘设备的写入操作完成之后,fsync()函数才会返回,其函数原型如下所示:

#include <unistd.h>
int fsync(int fd);

参数 fd 表示文件描述符,函数调用成功将返回 0,失败返回-1 并设置 errno 以指示错误原因。

使用示例

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>#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 <unistd.h>
int fdatasync(int fd);

3、sync()函数

#include <unistd.h>
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 标志)对性能的影响极大,大部分的应用程序是没有这种需求的,所以在大部分应用程序当中基本不会使用到。

六、直接 I/O:绕过内核缓冲

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的stdio缓冲

标准 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 <stdio.h>
int setvbuf(FILE *stream, char *buf, int mode, size_t size);

使用该函数需要包含头文件<stdio.h>。
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 <stdio.h>
void setbuf(FILE *stream, char *buf);

1.3、setbuffer()函数

#include <stdio.h>
void setbuffer(FILE *stream, char *buf, size_t size);

标准输出 printf()的行缓冲模式测试

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{printf("Hello World!\n");printf("Hello World!");for ( ; ; )sleep(1);
}


因为标准输出默认是行缓冲,所以只打印一行。

例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>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 <stdio.h>
int fflush(FILE *stream);

参数 stream 指定需要进行强制刷新的文件,如果该参数设置为 NULL,则表示刷新所有的 stdio 缓冲区。
函数调用成功返回 0,否则将返回-1,并设置 errno 以指示错误原因。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{printf("Hello World!\n");printf("Hello World!");fflush(stdout); //刷新标准输出 stdio 缓冲区for ( ; ; )sleep(1);
}


在一些其它的情况下,也会自动刷新 stdio 缓冲区如下:

1、关闭文件时刷新 stdio 缓冲区

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main(void)
{printf("Hello World!\n");printf("Hello World!");fclose(stdout); //关闭标准输出for ( ; ; )sleep(1);
}

2、程序退出时刷新 stdio 缓冲区

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
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 标志)。

八、文件描述符与 FILE 指针互转

在应用程序中,在同一个文件上执行 I/O 操作时,还可以将文件 I/O(系统调用 I/O)与标准 I/O 混合使用,这个时候我们就需要将文件描述符和 FILE 指针对象之间进行转换,此时可以借助于库函数 fdopen()、fileno()来完成。

库函数 fileno()可以将标准 I/O 中使用的 FILE 指针转换为文件 I/O 中所使用的文件描述符,而 fdopen()则进行着相反的操作,其函数原型如下所示:

#include <stdio.h>
int fileno(FILE *stream);
FILE *fdopen(int fd, const char *mode);

当混合使用文件 I/O 和标准 I/O 时,需要特别注意缓冲的问题,文件 I/O 会直接将数据写入到内核缓冲区进行高速缓存,而标准 I/O 则会将数据写入到 stdio 缓冲区,之后再调用 write()将 stdio 缓冲区中的数据写入到内核缓冲区。譬如下面这段代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main(void)
{printf("print");write(STDOUT_FILENO, "write\n", 6);exit(0);
}

执行结果你会发现,先输出了"write"字符串信息,接着再输出了"print"字符串信息。

本文链接:https://my.lmcjl.com/post/4017.html

展开阅读全文

4 评论

留下您的评论.