Linux C编程:标准I/O库

1 标准 I/O 库简介

标准 I/O 库是标准 C 库中用于文件 I/O 操作(譬如读文件、写文件等)相关的一系列库函数的集合,通常标准 I/O 库函数相关的函数定义都在头文件 <stdio.h>中。

1.1 标准 I/O 和文件 I/O 的区别

  1. 虽然标准 I/O 和文件 I/O 都是 C 语言函数,但是标准 I/O 是标准 C 库函数,而文件 I/O 则是 Linux 系统调用;
  2. 标准 I/O 是由文件 I/O 封装而来,标准 I/O 内部实际上是调用文件 I/O 来完成实际操作的;
  3. 可移植性:标准 I/O 相比于文件 I/O 具有更好的可移植性,通常对于不同的操作系统,其内核向应 用层提供的系统调用往往都是不同,譬如系统调用的定义、功能、参数列表、返回值等往往都是不 一样的;而对于标准 I/O 来说,由于很多操作系统都实现了标准 I/O 库,标准 I/O 库在不同的操作 系统之间其接口定义几乎是一样的,所以标准 I/O 在不同操作系统之间相比于文件 I/O 具有更好的 可移植性。 
  4. 性能、效率:标准 I/O 库在用户空间维护了自己的 stdio 缓冲区,所以标准 I/O 是带有缓存的,而 文件 I/O 在用户空间是不带有缓存的,所以在性能、效率上,标准 I/O 要优于文件 I/O。

2 FILE指针

文件 I/O 函数都是围绕文件描述符进行的,而标准 I/O 库函数是围绕 FILE 指针进行的;当使用标准 I/O 库函数打开或创建一个 文件时,会返回一个指向 FILE 类型对象的指针(FILE *),使用该 FILE 指针与被打开或创建的文件相关联,然后该 FILE 指针就用于后续的标准 I/O 操作

 FILE 是一个结构体数据类型,它包含了标准 I/O 库函数为管理文件所需要的所有信息,包括用于实际 I/O 的文件描述符指向文件缓冲区的指针缓冲区的长度当前缓冲区中的字节数以及出错标志等。

3 标准I/O库函数

3.1 打开文件:fopen()

#include <stdio.h>

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

函数参数和返回值含义如下:

  • path:参数 path 指向文件路径,可以是绝对路径、也可以是相对路径。
  • mode:参数 mode 指定了对该文件的读写权限,是一个字符串。
  • 返回值:调用成功返回一个指向 FILE 类型对象的指针(FILE *),该指针与打开或创建的文件相关联,如果失败则返回 NULL,并设置 errno 以指示错误原因。

参数 mode 字符串类型,可取值为如下值之一:

标注 I/O fopen()函数的 mode 参数
mode说明对应于 open()函数的 flags 参数取值
以只读方式打开文件O_RDONLY
r+以可读、可写方式打开文件O_RDWR
以只写方式打开文件,如果参数 path 指定的文件 存在,将文件长度截断为 0;如果指定文件不存在 则创建该文件O_WRONLY | O_CREAT | O_TRUNC
w+以可读、可写方式打开文件,如果参数 path 指定 的文件存在,将文件长度截断为 0;如果指定文件 不存在则创建该文件O_RDWR | O_CREAT | O_TRUNC
a以只写方式打开文件,打开以进行追加内容(在 文件末尾写入),如果文件不存在则创建该文 件O_WRONLY | O_CREAT | O_APPEND
a+以可读、可写方式打开文件,以追加方式写入 (在文件末尾写入),如果文件不存在则创建该 文件O_RDWR | O_CREAT | O_APPEND

3.2 读文件:fread()

#include <stdio.h>

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
  •  ptr:fread()将读取到的数据存放在参数 ptr 指向的缓冲区中;
  • size:fread()从文件读取 nmemb 个数据项,每一个数据项的大小为 size 个字节,所以总共读取的数据大小为 nmemb * size 个字节。
  • nmemb:参数 nmemb 指定了读取数据项的个数。
  • stream:FILE 指针。
  • 返回值:调用成功时返回读取到的数据项(nmemb)的个数(数据项数目并不等于实际读取的字节数,除非参数 size 等于 1);如果发生错误或到达文件末尾,则 fread()返回的值将小于参数 nmemb,那么到底发生了错误 还是到达了文件末尾,fread()不能区分文件结尾和错误,究竟是哪一种情况,此时可以使用 ferror()或 feof() 函数来判断

3.2.1 检查或复位状态

 调用 fread()读取数据时,如果返回值小于参数 nmemb 所指定的值,表示发生了错误或者已经到了文件 末尾(文件结束 end-of-file),但 fread()无法具体确定是哪一种情况,在这种情况下,可以通过判断错误标志或 end-of-file 标志来确定具体的情况。

3.2.1.1 feof()函数

库函数 feof()用于测试参数 stream 所指文件的 end-of-file 标志,当文件的读写位置移动到了文件末尾时,end-of-file 标志将会被设置。

#include <stdio.h>

int feof(FILE *stream);

 如果 end-of-file 标志被设置了,则调用 feof()函数将返回一个非零值,如果 end-of-file 标志没有被设置,则返回 0。

if (feof(file)) {
    /* 到达文件末尾 */
    }
    else {
    /* 未到达文件末尾 */
    }
3.2.1.2 ferror()函数

库函数 ferror()用于测试参数 stream 所指文件的错误标志,当对文件的 I/O 操作发生错误时,错误标志将会被设置。

#include <stdio.h>

int ferror(FILE *stream);

 如果错误标志被设置了,则调用 ferror()函数将返回一个非零值,如果错误标志没有被设置,则返回 0。

if (ferror(file)) {
    /* 发生错误 */
    }
    else {
    /* 未发生错误 */
    }

3.2.2 清除 end-of-file 标志和错误标志:clearerr()

库函数 clearerr()用于清除 end-of-file 标志和错误标志,当调用 feof()或 ferror()校验这些标志后,通常需要清除这些标志,避免下次校验时使用到的是上一次设置的值,此时可以手动调用 clearerr()函数清除标志。

#include <stdio.h>

void clearerr(FILE *stream);
  • stream:FILE 指针。 
  • 返回值:此函数没有返回值,调用将总是会成功!

3.3 写文件:fwrite()

#include <stdio.h>

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
  •  ptr:将参数 ptr 指向的缓冲区中的数据写入到文件中。
  • size:参数 size 指定了每个数据项的字节大小,与 fread()函数的 size 参数意义相同。
  • nmemb:参数 nmemb 指定了写入的数据项个数,与 fread()函数的 nmemb 参数意义相同。 stream:FILE 指针。
  • 返回值:调用成功时返回写入的数据项的数目(数据项数目并不等于实际写入的字节数,除非参数 size 等于 1);如果发生错误,则 fwrite()返回的值将小于参数 nmemb(或者等于 0)。

3.4 关闭文件:fclose()

#include <stdio.h>

int fclose(FILE *stream);

参数 stream :为 FILE 类型指针,即成功调用fopen()函数的返回值

 返回值:调用成功返回 0;失败将返回 EOF(也就是-1),并且会设置 errno 来 指示错误原因。

3.5 读写位置偏移量

3.5.1 设置读写位置偏移量:fseek

 库函数 fseek()的作用类似于系统调用 lseek(),用于设置文件读写位置偏移量,其函数原型如下所示:

#include <stdio.h>

int fseek(FILE *stream, long offset, int whence);

函数参数和返回值含义如下:

stream:FILE 指针。

offset:偏移量,以字节为单位,与 lseek()函数的 offset 参数意义相同。

whence:与 lseek()函数的 whence 参数意义相同,用于定义参数 offset 偏移量对应的参考值,该参数为下列其中一种(宏定义):

  • SEEK_SET:读写偏移量将指向 offset 字节位置处(从文件头部开始算);
  • SEEK_CUR:读写偏移量将指向当前位置偏移量 + offset 字节位置处,offset 可以为正、也可以为 负,如果是正数表示往后偏移,如果是负数则表示往前偏移;
  • SEEK_END:读写偏移量将指向文件末尾 + offset 字节位置处,同样 offset 可以为正、也可以为负, 如果是正数表示往后偏移、如果是负数则表示往前偏移。

返回值:成功返回 0;发生错误将返回-1,并且会设置 errno 以指示错误原因;

3.5.2 获取文件当前的读写位置偏移量:ftell()

库函数 ftell()可用于获取文件当前的读写位置偏移量,其函数原型如下所示:

#include <stdio.h>

long ftell(FILE *stream);
  • 参数 stream :指向对应的文件,
  • 返回值:函数调用成功将返回当前读写位置偏移量;调用失败将返回-1,并会设置 errno 以指示错误原因。

3.6  文件描述符与 FILE 指针互转:fdopen()、 fileno()

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

#include <stdio.h>

int fileno(FILE *stream); //将标准 I/O 中使用的 FILE 指针转换为文件 I/O 中所使用的文件描述符
FILE *fdopen(int fd, const char *mode); //将文件 I/O 中所使用的文件描述符转换为标准 I/O 中使用的 FILE 指针
  •  对于 fileno()函数来说,根据传入的 FILE 指针得到整数文件描述符,通过返回值得到文件描述符,如果 转换错误将返回-1,并且会设置 errno 来指示错误原因。
  • 若fopen()函数中的 mode 参数与文件描述符 fd 的访问模式不一致,则会导致调用 fdopen()失败。

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

4 格式化I/O

  1. 将格式化数据写入到标准输出,所以通常称为格式化输出:printf()、fprintf()、 dprintf()、sprintf()、snprintf()
  2. 从标准输入中获取格式化数据,格式化输入包括:scanf()、 fscanf()、sscanf()这三个库函数

4.1 格式化输出

C 库函数提供了 5 个格式化输出函数,包括:printf()、fprintf()、dprintf()、sprintf()、snprintf(),它们可以以调用者指定的格式进行转换输出,其函数定义如下所示:

#include <stdio.h>

int printf(const char *format, ...);  //将格式化数据写入到标准输出
int fprintf(FILE *stream, const char *format, ...);  //将格式化数据写入到 FILE 指针指定的文件中
int dprintf(int fd, const char *format, ...);  //将格式化数据写入到文件描述符 fd 指定的文件中
int sprintf(char *buf, const char *format, ...); //将格式化的数据存储在用户指定的缓冲区 buf 中
int snprintf(char *buf, size_t size, const char *format, ...); //将格式化的数据存储在用户指定的缓冲区 buf 中
  • 参数 format:这是一个字符串,称为格式 控制字符串,用于指定后续的参数如何进行格式转换
  • 每个函数除了固定参数之外,还可携带 0 个或多个可变参数。

4.2 格式化输入

C 库函数提供了 3 个格式化输入函数,包括:scanf()、fscanf()、sscanf(),其函数定义如下所示:

#include <stdio.h>

int scanf(const char *format, ...); //将用户输入(标准输入)的数据进行格式化转换
int fscanf(FILE *stream, const char *format, ...); //从 FILE 指针指定文件中读取数据,并将数据进行格式化转换
int sscanf(const char *str, const char *format, ...); //从参数 str 所指向的字符串中读取数据,并将数据进行格式化转换
  • 参数 format:同样也称为格式 控制字符串,用于指定输入数据如何进行格式转换
  • 每个函数除了固定参数之外,还可携带 0 个或多个可变参数。

5  I/O 缓冲

出于速度和效率的考虑,系统 I/O 调用(即文件 I/O,open、read、write 等)和标准 C 语言库 I/O 函数 (即标准 I/O 函数)在操作磁盘文件时会对数据进行缓冲

5.1 文件 I/O 的内核缓冲

read()和 write()系统调用在进行文件读写操作的时候并不会直接访问磁盘设备,而是仅仅在用户空间缓冲区和内核缓冲区(kernel buffer cache)之间复制数据,在后面的某个时刻,内核会将其缓冲区中的数据写入(刷新)到磁盘设备中,所以由此可知,系统调用与磁盘操作并不是同步的。

5.1.1 刷新文件 I/O 的内核缓冲区

强制将文件 I/O 内核缓冲区中缓存的数据写入(刷新)到磁盘设备中,对于某些应用程序来说,可能是很有必要的.

Linux 中提供了一些系统调用可用于控制文件 I/O 内核缓冲,包括系统调用 sync()、syncfs()、fsync()以 及 fdatasync()。

㈠、fsync()函数

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

元数据:文件大小、时间戳、权限等等信息

#include <unistd.h>

int fsync(int fd);
  • 参数 fd:表示文件描述符,
  • 返回值:函数调用成功将返回 0,失败返回-1 并设置 errno 以指示错误原因。
 ㈡、fdatasync()函数

系统调用 fdatasync()与 fsync()类似,不同之处在于 fdatasync()仅将参数 fd 所指文件的内容数据写入磁 盘,并不包括文件的元数据;同样,只有在对磁盘设备的写入操作完成之后,fdatasync()函数才会返回,其 函数原型如下所示:

#include <unistd.h>

int fdatasync(int fd);
㈢、sync()函数

系统调用 sync()会将所有文件 I/O 内核缓冲区中的文件内容数据元数据全部更新到磁盘设备中,该函 数没有参数、也无返回值,意味着它不是对某一个指定的文件进行数据更新,而是刷新所有文件 I/O 内核缓 冲区。其函数原型如下所示:

#include <unistd.h>

void sync(void);

5.1.2 控制文件 I/O 内核缓冲的标志

调用 open()函数时指定一些标志也可以影响到文件 I/O 内核缓冲,譬如 O_DSYNC 标志和 O_SYNC 标志。

㈠、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);

5.1.3 对性能的影响

在程序中频繁调用 fsync()、fdatasync()、sync()(或者调用 open 时指定 O_DSYNC 或 O_SYNC 标志) 对性能的影响极大,大部分的应用程序是没有这种需求的,所以在大部分应用程序当中基本不会使用到。

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

Linux 允许应用程序在执行文件 I/O 操作时绕过内核缓冲区,从用户空间 直接将数据传递到文件或磁盘设备,把这种操作也称为直接 I/O(direct I/O)或裸 I/O(raw I/O)。

直接 I/O 只在一些特定的需求场合,譬如磁盘速率测试工具、数据库系统等。

可针对某一文件或块设备执行直接 I/O,要做到这一点,需要在调用 open()函数打开文件时,指定 O_DIRECT 标志,譬如:

fd = open(filepath, O_WRONLY | O_DIRECT);

5.3 标准 I/O 的 stdio 缓冲

  • 文件 I/O 内核缓冲,这是由内核维护的缓冲区
  • 标准 I/O 所维护的 stdio 缓冲是用户空间的缓冲区

当应用程序中通过标准 I/O 操作磁盘文件时,为了减少调用系统调用的次数,标准 I/O 函数会将 用户写入或读取文件的数据缓存在 stdio 缓冲区,然后再一次性将 stdio 缓冲区中缓存的数据通过调用系统 调用 I/O(文件 I/O)写入到文件 I/O 内核缓冲区或者拷贝到应用程序的 buf 中。当操作磁盘文件时,在用户空间缓存大块数据以减少调用系统调用的次数,使得效率、性能得到优化。

5.3.1 对 stdio 缓冲进行设置
㈠、setvbuf()函数

调用 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 缓冲区;如果 buf 等 于 NULL,那么 stdio 库会自动分配一块空间作为该文件的 stdio 缓冲区(除非参数 mode 配置为非缓冲模 式)。

mode:参数 mode 用于指定缓冲区的缓冲类型

  • _IONBF:不对 I/O 进行缓冲(无缓冲)。意味着每个标准 I/O 函数将立即调用 write()或者 read(), 并且忽略 buf 和 size 参数,可以分别指定两个参数为 NULL 和 0。标准错误 stderr 默认属于这一种 类型,从而保证错误信息能够立即输出。
  • _IOLBF:采用行缓冲 I/O。在这种情况下,当在输入或输出中遇到换行符"\n"时,标准 I/O 才会执行文件 I/O 操作。对于输出流,在输出一个换行符前将数据缓存(除非缓冲区已经被填满),当输出换行符时,再将这一行数据通过文件 I/O write()函数刷入到内核缓冲区中;对于输入流,每次读取一行数据。对于终端设备默认采用的就是行缓冲模式,譬如标准输入和标准输出。
  • _IOFBF:采用全缓冲 I/O。在这种情况下,在填满 stdio 缓冲区后才进行文件 I/O 操作(read、write)。对于输出流,当 fwrite 写入文件的数据填满缓冲区时,才调用 write()将 stdio 缓冲区中的数据刷入内核缓冲区;对于输入流,每次读取 stdio 缓冲区大小个字节数据。默认普通磁盘上的常规文件默认常用这种缓冲模式。

size:指定缓冲区的大小。

返回值:成功返回 0,失败将返回一个非 0 值,并且会设置 errno 来指示错误原因。

㈡、setbuf()函数

 setbuf()函数构建于 setvbuf()之上,执行类似的任务,其函数原型如下所示:

#include <stdio.h>

void setbuf(FILE *stream, char *buf);
㈢、setbuffer()函数

setbuffer()函数类似于 setbuf(),但允许调用者指定 buf 缓冲区的大小,其函数原型如下所示:

#include <stdio.h>

void setbuffer(FILE *stream, char *buf, size_t size);

5.3.2 刷新 stdio 缓冲区:fflush()

无论我们采取何种缓冲模式,在任何时候都可以使用库函数 fflush()来强制刷新(将输出到 stdio 缓冲区中的数据写入到内核缓冲区,通过 write()函数)stdio 缓冲区。

#include <stdio.h>

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

 关于刷新 stdio 缓冲区相关内容总结:

  1.  调用 fflush()库函数可强制刷新指定文件的 stdio 缓冲区;
  2. 调用 fclose()关闭文件时会自动刷新文件的 stdio 缓冲区;
  3. 程序退出时会自动刷新 stdio 缓冲区(注意区分不同的情况):
  • 使用 exit()、 return 或不显式调用相关函数或执行 return 语句来结束程序会自动刷新 stdio 缓冲区
  • 如果使用_exit 或_Exit()终止程序则不会刷新

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值