标准I/O库
标准I/O库在用户内存和高速缓存之间添加了一个缓存,用于减少高速缓存与磁盘之间的I/O操作次数,以提升程序(系统)运行速度。标准I/O库负责管理这个缓存,调用标准I/O库函数读写,将使数据拷贝到缓存中,等待合适的时间或者用户发出指令(fflush
)进行实际的I/O操作,读写数据从/至高速缓存。
1. 引言
标准I/O库由ISO C标准说明。Single UNIX Specification 对 ISO C 标准进行了扩充,定义了另外一些接口。
标准I/O库处理很多细节,如缓冲区分配、以优化块长度执行I/O等。
缓冲区分配
标准I/O流自动分配缓冲区。
标准I/O流是有缓冲的IO库,目的是为了尽量减少
read
和write
的次数,通过将数据缓冲,使得每次read
和write
操作的数据更多,使得物理I/O每次写入的数据更多,避免频繁的请求物理I/O。
手动分配缓冲区,setbuf
, setvbuf
优化块长度
2. 流和FILE对象
对于unbuffered I/O
,当打开一个文件时,即返回一个文件描述符,后续的I/O操作都在此文件描述符上进行。而对于标准I/O库,它们的操作是围绕流(stream)进行的。当用标准I/O库打开或创建一个文件时,我们已使一个流与一个文件相关联。
流:输入和输出的数据分片尺寸不一致,但保证全部输出
对于ASCII字符集,一个字符用一个字节表示。对于国际字符集,一个字符可以用多个字节表示。标准的I/O文件流可用于单字节或多字节(“宽”)字符集。流的定向(stream’s orientation)决定了所读、写的字符是单字节还是多字节的。当一个流被创建时,它并没有定向。如若在未定向的流向上使用一个多字节的I/O函数,它的定向被设为宽定向,在未定向的流上使用单字节的|/O函数,流的定向被设置未字节定向的。
只有2个函数用于改变流的定向,freopen
函数用于清除一个流的定向,fwide
可用于设置流的定向。
#include <stdio.h>
#include <wchar.h>
int fwide(FILE *fp, int mode);
// 返回值:若流是宽定向的,则返回正值,若流是字节定向的,则返回负值,若流是未定向的,则返回0;
根据mode参数的不同值,fwide
函数执行不同的工作。
- 若mode < 0,设置流的定向为字节定向。
- 若mode > 0,设置流的定向为宽定向。
- 若mode == 0,不设置流的定向,返回流的定向。
注意:fwide
不改变已设置定向的流的定向。fwide
无出错返回,可以先清除errno
,在fwide
返回时检查errno
的值。
当打开一个流时,标准I/O函数fopen
返回一个指向FILE对象的指针。该对象通常是一个结构,它包含了标准I/O库为管理该流需要的所有信息,包括用于实际I/O的文件描述符
,指向用于该流缓冲区
的指针,缓冲区长度
,当前缓冲区字符数
以及出错标志
等。
FILE对象指针称为文件指针。
标准输入,标准输出与标准错误
对一个进程预定义了3个流。并且这3个流可以自动的被进程使用,它们是:标准输入,标准输出和标准错误。这些流引用的文件与文件描述符STDIN_FILENO
,STDOUT_FILENO
,STDERR_FILENO
所引用的相同,
这三个标准的I/O流通过预定义文件指针stdin
、stdout
、stderr
加以引用。这三个文件指针定义在头文件<stdio.h>
中。
缓冲
标准I/O库提供缓冲的目的是尽量减少使用read
和write
的调用次数。
read
优先从高速缓存中读取数据,如果未命中,则从磁盘中读取。write
操作将数据写入高速缓存中,并将本次写操作推入物理写队列。使用缓冲能够减少物理写操作,依据一些规则(例如缓冲区满时进行物理I/O)以实现将多次内存写操作合并成一次物理I/O操作。
它也对每个I/O流自动的进行缓冲管理,从而避免应用程序需要考虑这一点带来的麻烦。
Q: 缓冲管理是什么?缓冲区内存分配/缓冲区数据何时冲洗到磁盘上。
全缓冲
在这种情况下,当缓冲区被填满时,才进行实际的I/O操作。
Q: 当缓冲区被填满时,实际I/O操作会阻塞写缓冲区的I/O操作吗?
对于驻留在磁盘上的文件通常是由标准I/O库实施全缓冲的。在一个流上执行第一次I/O操作时,相关标准I/O函数通常调用malloc获得需使用的缓冲区。
Q: 每个I/O流管理自己的缓冲区,有没有管理所有I/O流缓冲区的机制?
需要使用是分配內存
术语冲洗(flush)
说明标准I/O缓冲区的写操作。缓冲区可由标准I/O例程自动的冲洗(例如,当一个缓冲区被填满时),或者可以调用函数fflush
冲洗一个流。值得注意的是,在UNIX环境中,flush有两种意思。在标准I/O库方面,flush(冲洗)意味着将缓冲区中的内容写到磁盘上(该缓冲区可能只是部分填满的)。在终端驱动程序方面,flush(刷清)表示丢弃已存储在缓冲区中的数据。
冲洗分为自动冲洗(由流自动管理)和手动冲洗(调用
fflush
)
行缓冲
在这种情况,流在输入/输出是遇到换行符时,标准I/O库执行实际I/O操作。这允许流一个一个字符的输出(用标准I/O函数fputc
),但只有在写了一行之后才进行实际的I/O操作。当流涉及一个终端时(如标准输入和标准输出时),通常使用行缓冲。
对于行缓冲有两个限制
- 因为标准I/O库的行缓冲的缓冲区的长度是固定的,填满了缓冲区或者缓冲区中被写入换行符号,进行实际的I/O操作。
- 任何时候,只要通过标准I/O库要求从一个(a)不带缓冲的标准I/O流 (b)行缓冲的标准I/O流(它从内核请求需要的数据)得到输入数据,那么就会冲洗所有行缓冲输出流。在(b)中带了一个括号中的说明,其理由是,所需的数据可能已在该缓冲区中,它并不要求一定从内核中读数据。很明显,从一个不带缓冲的流中输入(即(a)项)需要从内核中获得数据。
冲洗所有行缓冲输出流,所有行缓冲流被读缓冲区中数据,或写缓冲区中数据,执行实际I/O操作。
不带缓冲
标准I/O库不对字符进行缓冲存储。例如,若用标准I/O函数fputs
写了15个字符到不带缓冲的缓冲的流中,我们就期望这15个字符能立即输出,很可能使用3.8节的write
函数,将这些字符写到关联的的打开文件中。
标准错误流stderr
通常是不带缓冲的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个换行符。
ISO C 要求下列缓冲特征。
- 当且仅当标准输入和标准输出并不指向交互式设备时,它们才是全缓冲的。
- 标准错误绝不会是全缓冲的。
很多系统默认使用下列类型的缓冲: - 标准错误是不缓冲的。
- 若是指向终端设备的流,标准输入和标准输出是行缓冲的;否则是全缓冲的。
read 和 write 操作的需要等待冲洗才进行实际的I/O操作。对于写,数据首先写入流缓冲区,等待触发冲洗条件时,才进行实际的I/O操作。对于读,对于全缓冲,一次读系统调用尽量填满缓冲区,对于行缓冲,一次读系统调用,读取一行或填满缓冲区,对于不缓冲,相当于只见进行读系统调用。
对一个给定的流,可调用下列函数更改缓冲类型:
#include <stdio.h>
void setbuf(FILE *restrict fp, char *restrict buf);
int setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);
// 返回值:若成功,返回0;若出错,返回非0
可以使用setbuf
函数打开或关闭缓冲机制。为了带缓冲进行I/O,参数buf
必须指向一个长度未BUFSIZ
的缓冲区(该常量定义在<stdio.h>
)。通常在此之后流就是全缓冲的。为了关闭缓冲,将buf
设置为NULL
。
使用setvbuf
,我们可以精确地说明所需的缓冲类型。这是用mode
参数实现的:
_IOFBF 全缓冲
_IOLBF 行缓冲
_IONBF 无缓冲
如果指定一个不带缓冲的流,则忽略buf
和size
参数。如果指定全缓冲或者行缓冲,则buf
和size
可选择地指定一个缓冲区及其长度。如果该流是带缓冲的,而buf
是NULL
,则标准I/O
库将自动地为该流分配适当长度的缓冲区。适当长度指的是由常量BUFSIZ
所指定的值。
要了解,如果在一个函数内分配一个自动变量类的标准I/O缓冲区,则从该函数返回时,必须关闭该流。另外,因为某些实现将缓冲区的一部分作为用于存放它自己的管理操作信息,所以可以存放在缓冲区中的实际数据字节数少于size
。一般而言,应由系统选择缓冲区的长度,并自动分配缓冲区。在这种情况下关闭此流时,标准I/O库将自动释放缓冲区。
#include <stdio.h>
int fflush(FILE *fp);
// 若成功,返回0;若出错,返回EOF
此函数将使该流所有未写的数据都被传送至内核。作为一种特殊情形,如若fp是NULL,则此函数将导致所有输出流被冲洗。
3. 打开流
下列函数打开一个标准I/O流。
#include <stdio.h>
FILE *fopen(const char *restrict pathname, const char *restrict type);
FILE *freopne(const char *restrict pathname, const char *restirct type, FILE *restrict fp);
FILE *fdopen(int fd, const char *type);
// 若成功返回文件指针;若出错,返回NULL
这3个函数区别如下:
(1) fopen
打开路径名为pathname
的一个指定文件。
(2) freopen
函数在一个指定的流上打开一个指定的文件,若该流已经打开,则先关闭该流。若该流已经定向,则使用freopen
清除该定向。此函数一般用于将一个指定的文件打开为一个预定义的流:标准输入,标准输出,标准错误。
(3)fdopen
函数取一个已有的文件描述符(我们可能从open
, dup
, dup2
, fcntl
, pipe
, socket
, socketpair
, accept
函数得到此文件描述符),并使一个标准的I/O流与该文件描述符向结合。此函数常用于由创建管道和网络通信通道函数返回的描述符。因为这些特殊类型的文件不能用标准I/O函数fopen
打开,所以我们必须先调用设备专用函数以获得一个文件描述符,然后用fdopen
使一个标准I/O流与该描述符结合。
fopen
和freopen
是ISO C的所属部分。而ISO C并不涉及文件描述符,所以仅有POSIX.1具有fdopen
。
type
参数指定对该I/O流的读、写方式,ISO C规定type
参数可以有15种不同的值,如下表所示:
type | 说明 | open(2)标志 |
---|---|---|
r或rb | 只读打开 | O_RDONLY |
w或wb | 把文件截断至0长,或为写而创建 | O_WRONLY|O_CREAT|O_TRUNC |
a或ab | 追加:为在文件尾写而打开,或为写而创建 | O_WRONLY|O_CREAT|O_APPEND |
r+或r+b或rb+ | 为读写而打开 | O_RDWR |
w+或w+b或wb+ | 把文件长度截断为0,或为读写而打开 | O_RDWR|O_CREAT|O_TRUNC |
a+或a+b或ab+ | 为在文件尾读写而打开或创建 | O_RDWR|O_CREAT|O_APPEND |
使用字符b
作为type
的一部分,这使得标准I/O系统可以区分文本文件和二进制文件。因为UNIX内核并不对这两种文件进行区分,所以在UNIX系统环境下指定字符b
作为type
的一部分实际上并无作用。
Q:文本文件和二进制文件
对于fdopen
,type
参数的意义稍有区别。因为该描述符已被打开,所以文件描述符为写而打开,并不截断该文件(例如,如该文件描述符原来是由open
函数创建的,而且该文件已经存在,则其O_TRUNC
标志将决定是否截断该文件。fdopen
不能截断它为写而绑定的任一文件描述符所关联的文件)。另外,标准I/O的追加写方式也不能用于创建该文件(因为如果一个文件描述符引用一个文件,则该文件一定已经存在)。
FILE 对象为标准I/O流操作对象,它包含了标准I/O库为管理该流需要的所有信息,包括用于实际I/O的
文件描述符
,指向用于该流缓冲区
的指针,缓冲区长度
,当前缓冲区字符数
以及出错标志
等。对于fdopen
来说,它没有权限去修改一个不是由它打开的文件描述符fd
的文件状态标志,如果fd
的文件状态标志与fdopen
指定的读写
打开方式冲突,则fdopen
调用失败。
只有在文件被打开时才能截断该文件。
当用追加写类型打开以一个文件时,每次写都将数据写到当前的尾端处。如果有多个进程用标准I/O追加写方式打开同一个文件,那么来自每个进程的数据都将正确的写到文件中。
追加写方式为原子操作,
lseek
+write
为非原子操作,多程序写操作之间可能会出现相互写覆盖。
当以读和写类型打开一个文件时(type
中的+
号),具有下列限制。
- 如果中间没有
fflush
,fseek
,fsetpos
,rewind
,则在输出的后面不能直接跟随输入。 - 如果中间没有
fflush
,fsetpos
,rewind
,或者一个输入操作没有到达文件尾端,则在输入操作之后不能直接跟随输出。
Q: why
注意,在指定w或者a类型时创佳一个文件,我们无法说明该文件的访问权限位。POSIX.1要求实现使用如下权限位集来创建文件:
S_IRUSR
|S_IWUSR
|S_IRGRP
|S_IWGRP
|S_IROTH
|S_IWOTH
我们可以通过调整umask
的值来限制这些权限。
Q: 什么是
umask
?
除非流引用终端设备,否则按系统默认,流打开时是全缓冲的。若流引用终端设备,则该流是行缓冲的。一旦打开了流,那么在对该流执行任何操作之前,如果希望,则可以使用setbuf
或setvbuf
来设置流缓冲类型。
调用fclose
关闭一个打开的流。
#include <stdio.h>
int fclose(FILE *fp);
// 若成功,返回0;若出错,返回EOF
在该文件被关闭之前,冲洗缓冲中的输出数据。缓冲区中的任何输入数据被丢弃。如果标准I/O库已经为该流自动分配了一个缓冲区,则释放此缓冲区。
当一个进程正常终止时(直接调用exit
函数或者从main
函数返回),则所有带未写缓冲数据的标准I/O流都被冲洗,所有打开的标准I/O流都被关闭。
4. 读和写流
一旦打开了流,则可以在3种不同类型的非格式化I/O中进行选择,对其进行读写操作。
(1) 每次一个字符的I/O。一次读或写一个字符,如果流是带缓冲的,则标准I/O处理所有缓冲(读写缓冲)。
(2) 每次一行的I/O。如果想要一次读或写一行,则使用fgets
和fputs
。每行都以一个换行符终止。当调用
(3) 直接I/O。fread
和fwrite
函数支持支持这种类型的I/O。每次I/O操作读或写某种数量的对象,而每个对象具有指定的长度。这两个函数常用于从二进制文件中每次读或写一个结构。
4.1 输入函数
以下三个函数可用于一次读一个字符。
#include <stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
// 若成功,返回下一个字符;若已到达文件尾端或出错,返回EOF
函数getchar
等同与getc(stdin)
。
函数getc
和函数fgetc
区别为,getc
可被实现为宏,而fgetc
不能被实现为宏,这意味着:
(1) getc
的参数不应当是具有副作用的表达式,因为它可能会被计算多次。
(2) 因为fgetc
一定是一个函数,所以可以得到其地值。这允许将fgetc
的地址作为一个参数传递给另一个函数。
(3) 调用fgetc
所需要的时间可能比调用getc
要长,因为调用函数所需的时间通常长于调用宏。
这三个函数在返回下一个字符时,将其unsigned char
类型转换为int
类型。说明为无符号的理由是,如果最高位为1也不会使返回值为负。要求返回整形的理由是,这样就可以返回所有可能的字符在加上一个已出错或已到达文件尾端的指示值。在<stdio.h>
中的常量EOF
被要求是一个负值,其值经常是-1。这意味着不能将这三个函数的返回值存放在一个字符变量中,以后还要将这些函数的返回值与EOF
相比较。
注意:不管是出错还是到达文件尾端,三个函数返回相同的值(EOF
)。为了区分,必须调用ferror
或feof
。
#include <stdio.h>
int ferror(FILE *fp);
int feof(FILE *fp);
// 若条件为真,返回非0(真);若条件为假,返回0(假)
void clearerr(FILE *fp);
在大多数实现中,为每个流在FILE
中维护了两个标志:
- 出错标志;
- 文件结束标志;
调用clearerr
可以清除这两个标志。
从流中读取数据后,可以将ungetc
将字符再压送回流中。
#include <stdio.h>
int ungetc(int c, FILE *fp);
// 若成功,返回c;若出错,返回EOF
压送回到流中的字符以后又可以从流中读出,但读出的字符的顺序与压送回的顺序相反。应当了解。虽然ISO C允许实现支持任何次数的回送,但是,它要求实现提供一次只回送一个字符。我们不能期望一次能回送多个字符。
回送的字符,不一定必须是上一次读到的字符。不能回送EOF
。但是当已到达文件尾端时,仍可以回送一个字符。下次读将返回该字符,再读则返回EOF
。之所以能这样做的原因是,一次成功的ungetc
调用会清除该流的文件结束标志。
当正在读一个输入流,并进行某种形式的切词或者记号切分操作时,会经常用到回送字符操作。有时需要先看一看下一个字符,以决定如何处理当前字符。然后就需要方便的地将刚查看的字符回送,以便下一次调用getc
时返回该字符。如果标准I/O库不提供回送能力,就需要将字符存放到一个我们自己的变量中,并设置一个标志以便判别在下一次需要一个字符时是调用getc
,还是从变量中读取。
用
ungetc
压送回字符时,并没有将它们写到底层的文件中或设备上,只是将它们写回到标准I/O库的流缓冲中。
4.2 输出函数
对应于上述的输入函数都有一个输出函数:
#include <stdio.h>
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);
// 若成功,返回c;若失败,返回EOF
5. 每次一行I/O
6. 标准I/O的效率
7. 二进制I/O
8. 定位流
有三中方法定位标准I/O流。
定位当前读写的流起始点。
(1) ftell
和fseek
函数。它们都假定文件的位置可以放在一个长整形(2^(64-1))中。
(2) ftello
和fseeko
函数。Single UNIX Specification 引入了这两个函数,使文件偏移量可以不必一定使用长整形。它们使用off_t
数据类型取代了长整形。
(3) fgetpos
和fsetpos
函数。这两个函数是由ISO C引入的。它们使用一个抽象的数据类型f_pos_t
记录文件的位置。这种数据类型可以依据需啊摇定义为一个足够大的数,用以记录文件位置。
需要移植到非UNIX系统上运行的应用程序应当使用fgetpos
和fsetpos
。
#include <stdio.h>
long ftell(FILE *fp);
// 若成功,返回当前文件位置指示;若出错,返回-1L。
int fseek(FILE *fp, long offset, int whence);
// 若成功,返回0;若出错,返回-1。
void rewind(FILE *fp);
对于一个二进制文件,其文件位置指示器从文件开始位置开始度量,并已字节为度量单位。ftell
用于二进制皖文件时,其返回值就是这种字节的位置。为了用fseek
定位一个二进制文件,必须指定一个字节偏移量offset和解释这种偏移量的方式。whence
的值与lseek
种whence
的取值相同:SEEK_SET表示以文件头作为基准点;SEEK_CUR表示以当前文件位置指示器的位置作为偏移起始点;SEEK_END表示从文件尾端开始。
对于文本文件,它们的文件当前位置可能不以简单的字节偏移量来度量。这主要i是在非UNIX系统中,它们不同的格式存放文本文件。为了定位一个文本文件,whence
一定要是SEEK_SET,而且offset只能有两种值:0(后退到文件的起始位置),或者对该文件的ftell
返回的值。使用rewind
函数,也可将一个流设置到文件的起始位置。
除了偏移量的类型是off_t
而非long以外,ftello
函数与ftell
相同,fseeko
与fseek
相同。
#include <stdio.h>
off_t ftell(FILE *fp);
// 若成功,返回当前文件位置,否则,返回(off_t)-1;
int fseek(FILE *fp, off_t off, int whence);
// 若成功,返回0,若失败返回-1。
实现可将off_t
长度定于长于32位,
#include <stdio.h>
int fgetpos(FILE * restrict fp, fpos_t * restrict pos);
int fsetpos(FILE * restrict fp, const fpos_t *pos)
// 若成功,返回0,若失败,返回非0。
fgetpos
将文件位置指示器的当前值存入由pos
指定的对象中。在以后调用fsetpos
时,可以使用此值将流重新定位到该位置。
9. 格式化I/O
10. 实现细节
在UNIX系统中,标准I/O库的函数的实现都要调用非缓冲的I/O例程,每一个标准I/O流都有一个与之关联的文件描述符,可以对一个流调用fileno
函数以获得其文件描述符。
注意:
fileno
不是ISO C标准部分,而是POSIX.1支持的扩展。
#include <stdio.h>
int fileno(FILE *fp);
// 返回与该流相关联的文件描述符
#include <stdio.h>
int buff_size(FILE *fp) {
return fp->_IO_buf_end - fp->_IO_buf_base;
}
int is_unbuffered(FILE *fp) {
return (fp->_flags & _IO_UNBUFFERED);
}
bool is_linebuffered(FILE *fp) {
return (fp->_flags & _IO_LINE_BUF);
}
void pr_stdio(const char *name, FILE *fp) {
printf("stream = %s, ", name);
if(is_unbuffered(fp)) {
printf("unbuffered");
}
else if(is_linebuffered(fp)) {
printf("linebuffered");
}
else {
printf("fullybuffered");
}
printf(", buff size = %d\n", buff_size(fp));
}
int main(void) {
FILE *fp;
fputs("entry any character\n", stdout);
if(fgetc(stdin) == EOF) {
printf("get char error");
return -1;
}
fputs("one line to standard error\n", stderr);
pr_stdio("stdin", stdin);
pr_stdio("stdout", stdout);
pr_stdio("stderr", stderr);
if((fp = fopen("/etc/passwd", "r")) == NULL) {
printf("fopen error");
return -1;
}
if(getc(fp) == EOF) {
printf("getc error");
return -1;
}
pr_stdio("/etc/passwd", fp);
return 0;
}
注意,在打印缓冲状态信息之前,先对每个流执行I/O操作,第一个I/O操作通常就造成为该流分配缓冲区。
从中可见,该系统的默认是,当标准输入、输出连接终端时。是行缓冲的。行缓冲的长度是1024字节。注意,这里并没有将输入,输出的行长限制为1024字节,这只是缓冲区的长度。如果要将2048字节的的行写到标准输出,则要进行两次write的系统调用。当这两个流重新定向到普通文件时,它们就变成了全缓冲的,其缓冲区长度是该文件系统优先选用的I/O长度(从stat结构中得到的st_blksize值)。 当标准错误连接终端时,是无缓冲的,普通文件是全缓冲的。
11. 习题
- 使用
setvbuf
实现setbuf
。#include <stdio.h> int my_setbuf(FILE * restrict fp, char * restrict buf) { int mode; if(fp->_flags & _IO_UNBUFFERED) { mode = _IONBUF; } else if(fp->_flags & _IO_LINE_BUF) { mode = _IOLBUF; } else { mode = _IOFBUF; } setvbuf(fp, buf, mode, BUFSIZ); }
- 程序利用每次一行I/O(
fgets/fputs
)复制文件。若将程序中的MAXLINE改为4, 当复制的行超过最大值时会出现什么情况?
缓冲区以空字节结尾,填满buff,下次fgets
时从终止点继续读上次的行。fputs
写一个以空字节结尾的字符串,尾端的终止符null不写出。 - printf返回0代表什么?
传入空字符串数组。 - 对标准I/O流。如何使用
fsync
函数?
调用fflush
冲洗标准I/O流。