Linux 学习笔记:标准I/O 库

一、标准 I/O 库简介

标准I/O 库 是标准C 库中用于文件I/O 操作(例如读文件、写文件等)相关的一系列库函数的集合。

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 库在不同的操作系统之间其接口定义几乎都一样的。
  4. 性能、效率:标准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_FILENOSTDIN_FILENO1STDIN_FILENO2

或者

标准输入标准输出标准错误
stdinstdoutstderr

二、 文件操作基本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缓冲区

  1. 调用fflush 库函数可强制刷新指定文件的stdio 缓冲区。
  2. 调用fclose 关闭文件会自动刷新文件的stdio 缓冲区。
  3. 程序退出时会自动刷新stdio缓冲区。这里分两种情况:
    1. 刷新stdio 缓冲区: 使用exit() ,return 或不显式调用相关退出函数时
    2. 不刷新stdio缓冲区:使用_exit 或者 _Exit()终止程序。

4. I/O缓冲 小结

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gdut_llkkyy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值