Linux-应用编程学习笔记(二、文件I/O、标准I/O)

一、文件I/O基础

文件 I/O 指的是对文件的输入/输出操作,就是对文件的读写操作。Linux 下一切皆文件。

1.1 文件描述符

在 open函数执行成功的情况下, 会返回一个非负整数, 该返回值就是一个文件描述符(file descriptor) , 这说明文件描述符是一个非负整数;

对于 Linux 内核而言,所有打开的文件都会通过文件描述符进行索引。

在 Linux 系统中,一个进程可以打开的文件数是有限制的。默认1024个(0~1023)系统占用:系统标准输入(0)、 标准输出(1)以及标准错误(2)。

通过 ulimit 命令来查看进程可打开的最大文件数:

ulimit -n

Tips:每一个硬件设备都会对应于linux系统下的某一个文件(设备文件),应用程序对设备文件进行读写操作来使用或控制硬件设备。 

1.1.1 复制文件描述符

复制得到的文件描述符和旧的文件描述符拥有相同的权限,但是号是不一样的。可以多次复制。

“复制”的含义实则是复制文件表。

  • dup 函数用于复制文件描述符,可以实现两个写入的接续进行。
#include <unistd.h>
int dup(int oldfd);

函数参数和返回值含义如下:
oldfd: 需要被复制的文件描述符。
返回值: 成功时将返回一个新的文件描述符,由操作系统分配,分配置原则遵循文件描述符分配原则;
如果复制失败将返回-1,并且会设置 errno 值。
  • dup2也可以复制文件描述符,并且可以手动指定文件描述符。
#include <unistd.h>
int dup2(int oldfd, int newfd);

函数参数和返回值含义如下:
oldfd: 需要被复制的文件描述符。
newfd: 指定一个文件描述符(需要指定一个当前进程没有使用到的文件描述符)。
返回值: 成功时将返回一个新的文件描述符,也就是手动指定的文件描述符 newfd;如果复制失败将返
回-1,并且会设置 errno 值。

1.2 open打开文件(文件权限)

可以通过man命令查看系统调用的信息。

man 2 open

/*
man 命令后面跟着两个参数:
数字 2 表示系统调用, Linux 命令(对应数字 1)以及标准 C 库函数(对应数字 3)所对应的帮助信息。
最后一个参数 open 表示需要查看的系统调用函数名。
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

int creat(const char *pathname, mode_t mode);

int openat(int dirfd, const char *pathname, int flags);
int openat(int dirfd, const char *pathname, int flags, mode_t mode);

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

pathname: 
字符串类型,用于标识需要打开或创建的文件,可以包含路径(绝对路径或相对路径) 信息,譬如:"./src_file"(当前目录下的 src_file 文件)、 "/home/dengtao/hello.c"等;如果 pathname 是一个符号链接,会对其进行解引用。

flags: 
调用 open 函数时需要提供的标志, 包括文件访问模式标志以及其它文件相关标志,这些标志使用宏定义进行描述,都是常量, open 函数提供了非常多的标志,我们传入 flags 参数时既可以单独使用某一个标志,也可以通过位或运算(|) 将多个标志进行组合。 这些标志介绍如下:

  • O_RDONLY:只读。    
  • O_WRONLY:只写。    
  • O_RDWR:可读可写。    
  • O_CREAT:不存在则创建。    
  • O_DIRECTORY:pathname指向的不是目录,调用open失败。    
  • O_EXCL:与O_CREAT一起使用,判断是否已经存在。    
  • O_NOFOLLOW:pathname是一个符号链接,将不对其进行解引用,直接返回错误。
  • O_TRUNC :将文件原本的内容全部丢弃,文件大小变为 0。
  • O_APPEND :调用 open 函数打开文件,当每次使用 write()函数对文件进行写操作时,都会自动把文件当前位置偏移量移动到文件末尾, 从文件末尾开始写入数据,也就是意味着每次写入数据都是从文件末尾开始。使用了 O_APPEND 标志,即使是通过 lseek 函数也是无法修改写文件时对应的位置偏移量。
  • O_DSYNC:每个write()之后,自动调用fdatasync()进行数据同步
  • O_SYNC:每个write()之后,自动调用fsync()进行数据同步

mode: 
此参数用于指定新建文件的访问权限,只有当 flags 参数中包含 O_CREAT 或 O_TMPFILE 标志
时才有效(O_TMPFILE 标志用于创建一个临时文件)。 权限对于文件来说是一个很重要的属性,那么在 Linux系统中,我们可以通过 touch 命令新建一个文件,此时文件会有一个默认的权限,如果需要修改文件权限,可通过 chmod 命令对文件权限进行修改,譬如在 Linux 系统下我们可以使用"ls -l"命令来查看到文件所对应的权限。mode 参数的类型是 mode_t(u32)

S---这 3 个 bit 位用于表示文件的特殊权限,文件特殊权限一般用的比较少;

U---这 3 个 bit 位用于表示文件所属用户的权限,即文件或目录的所属者;

G---这 3 个 bit 位用于表示同组用户(group)的权限,即与文件所有者有相同组 ID 的所有用户;

O---这 3 个 bit 位用于表示其他用户的权限;

3 个 bit 位中,按照 rwx 顺序来分配权限位。例如:7(八进制),可读可写可执行,5(八进制),可读可执行。

open 函数文件权限宏:(通过位或可以组合)

宏定义说明
S_IRUSR允许文件所属者读文件
S_IWUSR允许文件所属者写文件
S_IXUSR允许文件所属者执行文件
S_IRWXU允许文件所属者读、写、执行文件
S_IRGRP允许同组用户读文件
S_IWGRP允许同组用户写文件
S_IXGRP允许同组用户执行文件
S_IRWXG允许同组用户读、写、执行文件
S_IROTH允许其他用户读文件
S_IWOTH允许其他用户写文件
S_IXOTH允许其他用户执行文件
S_IRWXO允许其他用户读、写、执行文件
S_ISUID
S_ISGID
S_ISVTX
set-user-ID(特殊权限)
set-group-ID(特殊权限)
sticky(特殊权限)

返回值

成功将返回文件描述符,文件描述符是一个非负整数;失败将返回-1。

1.3 write写文件

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

函数参数和返回值含义如下:
fd: open之后返回的文件描述符。 关于文件描述符,需要将进行写操作的文件所对应的文件描述符传递给 write 函数。
buf: 指定写入数据对应的缓冲区。
count: 指定写入的字节数
返回值: 如果成功将返回写入的字节数(0 表示未写入任何字节),如果此数字小于 count 参数,这不是错误,譬如磁盘空间已满,可能会发生这种情况;如果写入出错,则返回-1。

一般是默认写文件写到文件起始位置。如果当前位置偏移量为 1000 个字节处,调用 write()写入或 read()读取 500 个字节之后,当前位置偏移量将会移动到 1500 个字节处。

1.4 read读文件

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

函数参数和返回值含义如下:
fd: 文件描述符。与 write 函数的 fd 参数意义相同。

buf: 指定用于存储读取数据的缓冲区。

count: 指定需要读取的字节数。

返回值: 如果读取成功将返回读取到的字节数,实际读取到的字节数(没那么多可以读的字节)可能会小于 count 参数指定的字节数,也有可能会为 0。

1.5 close关闭文件

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

fd: 文件描述符,需要关闭的文件所对应的文件描述符。
返回值: 如果成功返回 0,如果失败则返回-1。

进程终止的时候,内核会自动关闭进程打开的所有文件。显式关闭不再需要的文件描述符是良好的编程习惯,文件描述符是有限资源。

1.6 lseek

系统会自动记录读写位置偏移量。文件第一个字节数据的位置偏移量为 0。
当打开文件时,会将读写偏移量设置为指向文件开始位置处,以后每次调用 read()、 write()将自动对其进行调整,以指向已读或已写数据后的下一字节,因此,连续的调用 read()和 write()函数将使得读写按顺序递增,对文件进行操作。

#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);

fd: 文件描述符。
offset: 偏移量,以字节为单位。
whence: 用于定义参数 offset 偏移量对应的参考值, 该参数为下列其中一种(宏定义) :

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

返回值: 成功将返回从文件头部开始算起的位置偏移量(字节为单位), 也就是当前的读写位置; 发生错误将返回-1。

二、文件I/O深入

2.1 Linux系统如何管理文件

2.1.1 静态文件和inode

静态文件:文件存放在磁盘文件系统中,并且以一种固定的形式进行存放。

文件储存在硬盘上, 硬盘的最小存储单位叫做“扇区” (Sector), 每个扇区储存 512 字节(相当于 0.5KB),操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个“” (block)。这种由多个扇区组成的“块” ,是文件存取的最小单位。 “块” 的大小,最常见的是 4KB,即连续八个 sector 组成一个 block。

磁盘两个区域:数据区(用于存储文件中的数据),inode区(用于存放 inode table(inode 表))。

通过"ls -i"命令或stat 命令查看文件的 inode 编号。

打开一个文件,系统内部会将这个过程分为三步:
1) 系统找到这个文件名所对应的 inode 编号;
2) 通过 inode 编号从 inode table 中找到对应的 inode 结构体;
3) 根据 inode 结构体中记录的信息,确定文件数据所在的 block,并读出数据。


2.1.2 文件打开时的状态

当我们调用 open 函数去打开文件的时候,内核会申请一段内存(一段缓冲区) ,并且将静态文件的数据内容从磁盘这些存储设备中读取到内存中进行管理、 缓存(也把内存中的这份文件数据叫做动态文件、内核缓冲区)。

读写动态文件后,动态文件与静态文件不同步,内核会将动态文件更新至磁盘设备中。

在 Linux 系统中, 内核会为每个进程设置一个专门的数据结构用于管理该进程,譬如用于记录进程的状态信息、运行特征等,我们把这个称为进程控制块(Process control block,缩写PCB) 。

PCB 数据结构体中有一个指针指向了文件描述符表(File descriptors), 文件描述符表中的每一个元素索引到对应的文件表(File table),文件表也是一个数据结构体,其中记录了很多文件相关的信息,譬如文件状态标志、 引用计数、 当前文件的读写偏移量以及 i-node 指针(指向该文件对应的 inode)等, 进程打开的所有文件对应的文件描述符都记录在文件描述符表中,每一个文件描述符都会指向一个对应的文件表。

2.2 返回错误处理与errno

errno 本质上是一个 int 类型的变量,用于存储错误编号, 但是需要注意的是,并不是执行所有的系统调用或 C 库函数出错时,操作系统都会设置 errno。

通过man命令可以查询返回值。

#include <errno.h>

2.2.1 strerror函数(C库函数)

将对应的 errno 转换成适合我们查看的字符串信息

#include <string.h>
char *strerror(int errnum);

函数参数和返回值如下:
errnum: 错误编号 errno。
返回值: 对应错误编号的字符串描述信息。

2.2.2 perror函数(C库函数)

调用此函数不需要传入 errno,函数内部会自己去获取 errno 变量的值, 调用此函数会直接将错误提示字符串打印出来,而不是返回字符串,除此之外还可以在输出的错误提示字符串之前加入自己的打印信息。perror 函数会在附加信息后面自动加入冒号和空格以区分

#include <stdio.h>
void perror(const char *s);

函数参数和返回值含义如下:
s: 在错误提示字符串信息之前,可加入自己的打印信息,也可不加,不加则传入空字符串即可。
返回值: void 无返回值。

2.3终止进程

终止进程的方法:

  • main 函数中运行 return;
  • 调用 Linux 系统调用_exit()或_Exit();
  • 调用 C 标准库函数 exit()。

2.3.1 _exit、_Exit

调用_exit()函数会清除其使用的内存空间,并销毁其在内核中的各种数据结构, 关闭进程的所有文件描述符, 并结束进程、将控制权交给操作系统。_Exit与_exit一样。

_exit()和_Exit()是系统调用

#include <unistd.h>
void _exit(int status);

status:0 表示正常结束、若为其它值则表示程序执行过程中检测到有错误发生。

2.3.2 exit

exit()是一个标准 C 库函数

#include <stdlib.h>
void exit(int status);

2.4 空洞文件

使用lseek函数将文件偏移量(6000字节)超过文件长度(4096字节),write开始在6000写,在4096~6000之间没有写入任何数据,就形成了文件空洞,那么该文件就是空洞文件。文件空洞不占用物理空间。直到在某个时刻对空洞部分进行写入数据时才会为它分配对应的空间,但是空洞文件形成时,逻辑上该文件的大小是包含了空洞部分的大小的,这点需要注意。


空洞文件的用处:对于很大的文件,可以使用多线程进行分段读写。

使用 ls 命令查看到的大小是文件的逻辑大小,包括了空洞部分大小和真实数据部分大小; du 命令查看到的大小是文件实际占用存储块的大小


2.5 多次打开同一文件

  • 一个进程内多次 open 打开同一个文件,那么会得到多个不同的文件描述符 fd,同理在关闭文件的时候也需要调用 close 依次关闭各个文件描述符。
  • 一个进程内多次 open 打开同一个文件,在内存中并不会存在多份动态文件。
  • 一个进程内多次 open 打开同一个文件,在不同文件描述符所对应的读写位置偏移量是相互独立的。
  • 分别写入同一文件,现象是:第二次写入的覆盖第一次写入的。想要接着写就用O_APPEND。

2.6 文件共享

同一个文件(譬如磁盘上的同一个文件,对应同一个 inode) 被多个独立的读写体同时进行 IO 操作(一个读写体操作文件尚未调用 close 关闭的情况下,另一个读写体去操作文件)。

常见的三种文件共享的实现方式
(1)同一个进程中多次调用 open 函数打开同一个文件。

多次调用open,得到不同的文件描述符,对应多个不同地文件表,文件表索引到同一inode。

(2)不同进程中分别使用 open 函数打开同一个文件。

(3)同一个进程中通过 dup(dup2)函数对文件描述符进行复制。

2.7 竞争冒险

操作共享资源的两个进程(或线程),其操作之后的所得到的结果往往是不可预期的, 因为每个进程(或线程)去操作文件的顺序是不可预期的,即这些进程获得 CPU 使用权的先后顺序是不可预期的,完全由操作系统调配, 这就是所谓的竞争状态。

2.8 原子操作

所谓原子操作, 是由多步操作组成的一个操作,原子操作要么一步也不执行,一旦执行, 必须要执行完所有步骤,不可能只执行所有步骤中的一个子集。

  • O_APPEND实现原子操作

移动当前写位置偏移量到文件末尾、写入数据”这两个操作步骤就组成了一个原子操作。

  • pread()和pwrite()
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

函数参数和返回值含义如下:
fd、 buf、 count 参数与 read 或 write 函数意义相同。
offset: 表示当前需要进行读或写的位置偏移量。
返回值: 返回值与 read、 write 函数返回值意义一样。

调用 pread 函数时,无法中断其定位和读操作(也就是原子操作);不更新文件表中的当前位置偏移量

  • 创建一个文件(O_EXCL 标志)

文件创建的竞争状态:

进程 A 和进程 B 都会创建出同一个文件,同一个文件被创建两次这是不允许的。

将“判断文件是否存在、创建文件”这两个步骤合成为一个原子操作

2.9 fcntl和ioctl

#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ )

函数参数和返回值含义如下:
fd: 文件描述符。
cmd: 对fd进行的操作命令。cmd 操作命令大致可以分为以下 5 种功能:
1.复制文件描述符(cmd=F_DUPFD 或 cmd=F_DUPFD_CLOEXEC);
2.获取/设置文件描述符标志(cmd=F_GETFD 或 cmd=F_SETFD);
3.获取/设置文件状态标志(cmd=F_GETFL 或 cmd=F_SETFL);
4.获取/设置异步 IO 所有权(cmd=F_GETOWN 或 cmd=F_SETOWN);
5.获取/设置记录锁(cmd=F_GETLK 或 cmd=F_SETLK);
fcntl 函数是一个可变参函数,第三个参数需要根据不同的 cmd 来传入对应的实参,配合 cmd 来使
用。
返回值: 执行失败情况下,返回-1,并且会设置 errno;执行成功的情况下,其返回值与 cmd(操作命
令)有关,譬如 cmd=F_DUPFD(复制文件描述符)将返回一个新的文件描述符、 cmd=F_GETFD(获取文
件描述符标志)将返回文件描述符标志、 cmd=F_GETFL(获取文件状态标志)将返回文件状态标志等。

一般用于操作特殊文件或硬件外设。

#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);

函数参数和返回值含义如下:
fd: 文件描述符。
request: 此参数与具体要操作的对象有关,没有统一值, 表示向文件描述符请求相应的操作;
...: 此函数是一个可变参函数, 第三个参数需要根据 request 参数来决定,配合 request 来使用。
返回值: 成功返回 0,失败返回-1。

2.10 截断文件

截断:如果文件目前的大小大于参数 length 所指定的大小,则多余的数据将被丢失,类似于多余的部分被“砍”掉了;如果文件目前的大小小于参数 length 所指定的大小,则将其进行扩展, 对扩展部分进行读取将得到空字节"\0"。

#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);

ftruncate()使用文件描述符 fd 来指定目标文件。
truncate()则直接使用文件路径 path 来指定目标文件。

使用 ftruncate()函数进行文件截断操作之前,必须调用 open()函数打开该文件得到文件描述符,并且必须要具有可写权限,也就是调用 open()打开文件时需要指定 O_WRONLY 或 O_RDWR。
调用这两个函数并不会导致文件读写位置偏移量发生改变,所以截断之后一般需要重新设置文件当前的读写位置偏移量,以免由于之前所指向的位置已经不存在而发生错误(譬如文件长度变短了,文件当前所指向的读写位置已不存在)。
 

三、标准I/O库

3.1 标准I/O库简介

<stdio.h>

标准 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。

3.2 FILE指针

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

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


3.3 标准输入、标准输出和标准错误

#include <unistd.h>

/* Standard file descriptors. */
#define STDIN_FILENO 0 /* Standard input. */
#define STDOUT_FILENO1 /* Standard output. */
#define STDERR_FILENO2 /* Standard error output. */
#include <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

3.4 fopen、fclose

#include <stdio.h>
FILE *fopen(const char *path, const char *mode);

函数参数和返回值含义如下:
path: 参数 path 指向文件路径,可以是绝对路径、也可以是相对路径。
mode: 参数 mode 指定了对该文件的读写权限,是一个字符串,稍后介绍。
返回值: 调用成功返回一个指向 FILE 类型对象的指针(FILE *),该指针与打开或创建的文件相关联,
后续的标准 I/O 操作将围绕 FILE 指针进行。 如果失败则返回 NULL,并设置 errno 以指示错误原因。
  • r = O_RDONLY
  • r+ = O_RDWR
  • w = O_WRONLY| O_CREAT|O_TRUNC
  • w+ =  O_RDWR| O_CREAT|O_TRUNC
  • a = O_WRONLY| O_CREAT|O_APPEND
  • a+ = O_RDWR| O_CREAT|O_APEND

新建文件的权限:默认值S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH (0666)

#include <stdio.h>
int fclose(FILE *stream);

3.5 fread、fwrite

#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 指针。
返回值: 调用成功时返回读取到的数据项的数目(数据项数目并不等于实际读取的字节数,除非参数
size 等于 1);如果发生错误或到达文件末尾,则 fread()返回的值将小于参数 nmemb, fread()不能区分文件结尾和错误。


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.6 fseek定位

#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
//用于设置文件读写位置偏移量。


函数参数和返回值含义如下:
stream: FILE 指针。
offset: 与 lseek()函数的 offset 参数意义相同。
whence: 与 lseek()函数的 whence 参数意义相同。
返回值: 成功返回 0;发生错误将返回-1,并且会设置 errno 以指示错误原因; 与 lseek()函数的返回值
意义不同,这里要注意!




#include <stdio.h>
long ftell(FILE *stream);
//用于获取文件当前的读写位置偏移量。

3.7 检测或复位状态

1.feof函数

用于测试参数 stream 所指文件的 end-of-file 标志。

#include <stdio.h>
int feof(FILE *stream);

2.ferror函数

用于测试参数 stream 所指文件的错误标志。

#include <stdio.h>
int ferror(FILE *stream);

3.clearerr函数

用于清除 end-of-file 标志和错误标志。

#include <stdio.h>
void clearerr(FILE *stream);

3.8 格式化I/O

1.格式化输出

#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, ...);
//与sprintf一样,为了防止缓冲区溢出,加入了参数size,多出的将丢弃。

printf举例:

%[flags][width][.precision][length]type

flags: 标志,可包含 0 个或多个标志;
width: 输出最小宽度,表示转换后输出字符串的最小宽度;
precision: 精度,前面有一个点号" . ";
length: 长度修饰符;
type: 转换类型,指定待转换数据的类型。

type:

  • d/i:  int
  • o:    unsigned int,无符号八进制
  • u:    unsigned int,无符号十进制
  • x/X: unsigned int,无符号十六进制,大小写
  • f/F: double,浮点数,默认保留小数后六位
  • e/E: double,科学计数法表示浮点数
  • g/G: double,根据数值的长度,选择以最短的方式输出
  • s:   char *,字符串
  • p:   void *,十六进制表示的指针
  • c:   char,字符型,可以把输入的数字转换到 ASCII 码字符输出

flags:

  • #:带前缀
  • 0:在输出的数字前补0
  • -:左对齐
  • ‘ ’(空格):正数前面加空格,负数前面加负号
  • +:输出正数的正号

width:

最小的输出宽度,用十进制数来表示输出的最小位数,若实际的输出位数大于指定的输出的最小位数,则以实际的位数进行输出,若实际的位数小于指定输出的最小位数,则可按照指定的 flags 标志补 0 或补空格。

precision精度:显示小数点后的位数。

length:指明待转换数据的长度。

2.格式化输入

#include <stdio.h>
int scanf(const char *format, ...);
//将用户输入(标准输入)的数据进行格式化转换并进行存储

int fscanf(FILE *stream, const char *format, ...);
//从指定文件中读取数据,作为格式转换的输入数据

int sscanf(const char *str, const char *format, ...);
//从参数 str 所指向的字符串缓冲区中读取数据,作为格式转换的输入数据

format:

%[*][width][length]type
%[m][width][length]type

width: 最大字符宽度;
length: 长度修饰符,与格式化输出函数的 format 参数中的 length 字段意义相同。
type: 指定输入数据的类型。与printf一致。

3.9 I/O缓冲

1.文件I/O的内核缓冲

read()和 write()系统调用在进行文件读写操作的时候并不会直接访问磁盘设备,而是仅仅在用户空间缓冲区和内核缓冲区(kernel buffer cache)之间复制数据。

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

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

#include <unistd.h>

int fsync(int fd);
//将参数 fd 所指文件的内容数据和元数据写入磁盘,写完之后返回

int fdatasync(int fd);
//与fsync类似,只写入内容数据,不写元数据(记录文件属性相关的数据信息,比如文件大小、时间戳、权限等)

void sync(void);
//刷新所有文件 I/O 内核缓冲区

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

fd = open(filepath, O_WRONLY | O_DSYNC);
//在每个 write()调用之后调用 fdatasync()函数进行数据同步

fd = open(filepath, O_WRONLY | O_SYNC);
//在每个 write()调用之后调用 fsync()函数进行数据同步

对性能的影响
在程序中频繁调用 fsync()、 fdatasync()、 sync()(或者调用 open 时指定 O_DSYNC 或 O_SYNC 标志)对性能的影响极大,大部分不会使用。
 

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

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

fd = open(filepath, O_WRONLY | O_DIRECT);

直接 I/O 的对齐限制

⚫ 应用程序中用于存放数据的缓冲区,其内存起始地址必须以块大小的整数倍进行对齐;
⚫ 写文件时,文件的位置偏移量必须是块大小的整数倍;
⚫ 写入到文件的数据大小必须是块大小的整数倍。

如何确定磁盘分区的块大小呢?可以使用 tune2fs 命令进行查看
tune2fs -l /dev/sda1 | grep "Block size"

4.stdio缓冲

对 stdio 缓冲进行设置

#include <stdio.h>

int setvbuf(FILE *stream, char *buf, int mode, size_t size);
stream: FILE 指针,用于指定对应的文件。
buf:指向 size 大小的内存区域将作为该文件的 stdio 缓冲区
mode: 参数 mode 用于指定缓冲区的缓冲类型:_IONBF(不缓冲),_IOLBF(行缓冲),_IOFBF(全缓冲)。
size: 指定缓冲区的大小。
返回值: 成功返回 0,失败将返回一个非 0 值,并且会设置 errno 来指示错误原因。


void setbuf(FILE *stream, char *buf);
buf:NULL无缓冲,或者BUFSIZ字节的缓冲区


void setbuffer(FILE *stream, char *buf, size_t size);
//允许调用者指定 buf 缓冲区的大小

刷新stdio缓冲区

#include <stdio.h>
int fflush(FILE *stream);
//stream 指定需要进行强制刷新的文件,如果该参数设置为 NULL,则表示刷新所有的 stdio 缓冲区


调用 fflush()库函数可强制刷新指定文件的 stdio 缓冲区;
调用 fclose()关闭文件时会自动刷新文件的 stdio 缓冲区;
程序退出时会自动刷新 stdio 缓冲区(注意区分不同的情况:return会刷新,_exit和_Exit不会刷新) 。

5.I/O缓冲小节

3.10 文件描述符和FILE指针互转

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

  • 16
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

chinalihuanyu

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

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

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

打赏作者

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

抵扣说明:

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

余额充值