总结
fgets,sprintf,snprintf会在缓冲区数组的结尾加上一个null字节,但是使用的时候不会包含这个字节。fgets和getline都会读入回车,并且将它存入缓冲区,getchar,getc和fgetc也会读入回车,并且将它存起来。- 每个标准I/O流都有一个和它相关联的文件描述符,可以对一个流调用
fileno获得它的文件描述符。fileno不是ISO C的部分,因为文件描述符不属于ISO C。 - 标准I/O库的一个不足是效率不高。这和它复制的数据量有关。每当使用一次
fgets和fputs时,通常需要复制两次数据,一次是在用户程序的行缓冲区和标准I/O缓冲区之间,一次是在内核和标准I/O缓冲区之间。
使用fgets需要用户指定fgets使用的缓冲区,或者使用getline,如果传入的指针指向NULL,getline会负责分配缓冲区大小。
read和write需要设置缓冲区,这是系统调用级别的,大小可以任意指定,通常使用sturct stat.st_blksize的大小,用户如果直接调用read和write的话,需要自己设置缓冲区。而标准I/O库可以自己选择是否进行缓冲,如果缓冲的话,标准I/O库可以负责进行缓冲区大小选择和分配,也可以用户自己进行指定缓冲类型:行缓冲和全缓冲,用户也可以自己通过setbuf和setvbuf更改缓冲区大小和地址。而在setbuf中,如果buf是NULL的话,是关闭缓冲区,如果不为空的话,必须是BUFSIZ大小。在setvbuf中,通过mode指定缓冲区的类型,buf是NULL的话,库函数负责分配缓冲区。否则buf是多大就用多大的缓冲区。
而在内核中,还存在buffer cache和page cache,用于“延迟写”,减少和磁盘的交互。
概述
特殊符号的ASCII
'\n’是10。
EOF是-1。
标准I/O和文件I/O
文件I/O是围绕文件描述符进行的,使用open打开一个文件时,返回一个文件描述符,然后使用文件描述符进行后续I/O操作。文件I/O是UNIX相关的实现,其他系统可能有不同的实现,是不跨平台的。
标准I/O是围绕stream进行I/O操作的。当标准I/O库打开或者创建一个文件时,一个流已经和文件相关联。标准I/O库处理很多细节,比如缓冲区分配,使用优化的长度块执行I/O等,使用户不用担心选择多大的block进行I/O会更快。标准I/O库是ISO C标准定义的,不仅仅UNIX系统有实现,凡是支持ISO C标准的操作系统都应该实现,是支持跨平台的。标准I/O在UNIX上需要使用文件I/O实现,在windows等其他系统上就需要其他的实现。
stream和FILE对象
12.1 Streams
For historical reasons, the type of the C data structure that represents a stream is called FILE rather than “stream”. Since most of the library functions deal with objects of type FILE *, sometimes the term file pointer is also used to mean “stream”. This leads to unfortunate confusion over terminology in many books on C.
标准I/O的操作是围绕stream进行的,当打开一个stream时,它返回一个指向FILE类型的指针(通常叫做文件指针)。FILE是一个结构体,包含了标准I/O管理这个stream需要的所有信息,包含用于实际I/O的文件描述符,指向这个流缓冲区的指针,缓冲区的长度,当前缓冲区中的字符等。为了引用一个stream,需要将FILE指针作为参数传递给每个标准I/O函数。
stream的定向
对于ASCII字符集,一个字符用一个字节表示。对于国际字符集,一个字符用多个字节表示。标准I/O FILE stream可以用于单字节也可以用于多字节字符集。stream的orientation决定了读写的字符是单字节还是多字节,最开始创建stream时,它的orientation没有被确定,使用什么字符的I/O就会将stream的orientation定义为什么。
有两个函数可以改变stream的orientation,它们是freopen和fwide,原型如下:
freopen和fwide原型
1 |
|
freopen和fwide性质
fwide用于设置stream的orientation。如果mode为负,是单字节定向的。如果mode为正,是多字节定向的。如果mode为0,fwide确定当前stream的oritentation并返回。fwide不能改变已经定向的stream的orientation。fwide没有出错返回
标准输入,标准输出和标准错误
通常对一个进程预定义了三个stream,它们可以自动的被进程使用。它们是标准输入,标准输出和标准错误,这些stream引用的文件和文件描述符STDIN_FILENO,STDOUT_FILENO和STDERR_FILENO所引用的文件一样。
这三个stream定义在头文件<stdio.h>中,通过预定义文件指针stdin, stdou和stderr使用。
三种缓冲类型
标准I/O库提供缓冲的目的是尽可能减少read和write的调用次数,标准I/O库对每个流自动的进行缓冲管理,使得应用程序不用考虑缓冲区的管理。
标准I/O提供了三种类型的缓冲:
全缓冲
填满标准I/O的缓冲区之后,进行实际的I/O操作。对于存储在磁盘上的文件通常是由标准I/O实施全缓冲的。在一个流上第一次执行I/O操作时,相关的标准I/O函数调用malloc获得需要的缓冲区。
行缓冲
在行缓冲中,当输入和输出遇到换行符时,标准I/O库执行I/O操作。但是需要注意的是行缓冲区的长度是固定的,当行缓冲区满时即使没有遇到换行符也进行I/O操作。当涉及终端的I/O时,通常使用行缓冲。使用标准I/O的fputc允许我们一次输出一个字符,但是只有在写了一行之后才能进行实际I/O操作。
此外,任何时候只要通过标准I/O库要求从一个不带缓冲的流或者一个行缓冲的流中得到输入数据,那么就会flush所有行缓冲输出流。从行缓冲的流中得到输入数据的一个例子就是从终端按下回车,刚才输入的数据就会立刻从输出流中输出。
不带缓冲
标准I/O库不对字符进行缓冲存储。如果将字符传入不带缓冲的输出流中,字符会立即输出到输出流关联的文件或者设备。
fflush函数
标准I/O库使用flush将输出流缓冲区的内容写到和输出流相关联的文件,缓冲区可以使用标准I/O例程自动的flush,比如当缓冲区填满时,或者缓冲区不满时可以手动调用fflush函数进行flush。1
int flussh(FILE *fp);
任何时候,都可以手动强制冲洗一个流,当fp是NULL时,冲洗所有的输出流。
注意fflush和fsync的区别,fflush是将位于主存中的缓冲区的内存冲洗到内核。而内核也有一个缓冲区,叫做buffer cache或者page cache,内核接收到数据会首先将它们写入buffer cache或者page cache中,然后排入队列,晚些时候再写,这种方式叫做延迟写。fsync是将buff cache中的内容立即写入磁盘而不等待。
ISO C缓冲标准和UNIX具体实现
ISO C要求:
- 当且仅当标准输入和标准输出不指向交互设备时,它们才是全缓冲的。
- 标准错误不会是全缓冲的。
UNIX具体实现:
- 标准错误不带缓冲
- 指向终端设备的流,都是行缓冲的,否则是全缓冲的。
修改默认缓冲
可以通过setbuf和setvbuf更改流的缓冲类型。
setbuf和setvfuf原型
1 |
|
setbuf和setvfuf性质
- 这些函数需要在流被打开后调用,因为他们需要文件指针作为参数,而且应该在对流执行任何操作之前调用。
- 可以使用
setbuf函数打开和关闭缓冲机制。将buf设置为NULL,就是关闭缓冲。如果buf不为NULL,它必须指向一个长度为BUFSIZ的缓冲区,通常在这之后就是全缓冲的,如果和终端设备关联,可能会是行缓冲的。 setvbuf可以通过mode指定缓冲的类型,_IOFBF是全缓冲,_IOLBF是行缓冲,_IONBF是不缓冲。指定不缓冲,忽略buf和size参数。如果指定全缓冲或者行缓冲,buf和size可以通过buf和size指定缓冲区的位置和大小。如果指定带缓冲,而buf是NULL,系统会自动分配BUFSIZE大小的缓冲区。- 一般而言,应该由操作系统选择缓冲区的长度,并且自动分配缓冲区,这种情况下,关闭流,标准I/O库会自动释放缓冲区。
打开一个stream
可以使用fopen, freopen和fdopen函数打开一个standard I/O stream。它们的原型如下:
fopen, freopen和fdopen原型
1 |
|
fopen, freopen和fdopen性质
fopen打开路径名为pathname的一个文件fdopen使用一个已有的文件描述符,并将一个标准I/O stream和该文件描述符结合。这个函数通常用于由创建管道和网络通信通道函数返回的文件描述符,因为这些特殊文件不能使用标准I/O函数fopen打开,所以需要使用设备专用函数获得一个文件描述符,然后使用fdopen将文件描述符和一个I/O stream结合。freopen函数在一个指定的stream打开一个指定的文件,如果这个stream已经打开,先关闭这个stream;如果这个stream已经进行了定向,使用freopen清楚该定向。这个函数一般用于将一个指定的文件打开为一个预定义的stream:stdin, stdout和stderr。fopen和freopen是ISO C的部分,因为ISO C不包含文件描述符,所以只有POSIX.1有fdopen。mode有15种取值:r,w,a,rb,wb,ab,r+,r+b,rb+,w+,w+b,wb+,a+,a+b,ab+。对于标准I/O来说,使用b可以区分二进制和文本文件。但是对于UNIX来说,二进制和文本文件没有区别,有没有b无所谓。- 当用追加写时,如果有多个进程用追加写方式打开同一个文件,每个进程的数据都会正确的写入文件中。
fdopen不会截断也不会创建文件。对于fdopen来说,因为需要文件描述符,所以文件必须是打开的,当mode是w,wb时,并不会截断文件,a和ab也不能用于创建文件,因为文件描述符必须引用一个存在的文件。而如果使用a+,ab+,w+,wb+等,这个时候文件已经存在了,不会创建,也不会截断,需要写或者追加就行了,就不会有前半句说的问题了。- 使用
a和w相关的mode创建文件时,没有办法指定文件的权限位。而POSIX.1要求使用如下的权限创建文件:
S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IOTH|S_IWOTH
可以在使用fopen等函数之前,使用umask指定文件的权限位。 - 如果流引用终端设备,是行缓冲的,否则是全缓冲的。
fclose函数和性质
1 |
|
在文件被关闭之前,flush输出数据。缓冲区中的输入数据被丢弃。如果标准I/O库为这个stream自动分配了缓冲区,释放该缓冲区。
当一个进程正常终止时,所有带未写缓冲数据的标准I/O都被flush,所有打开的标准I/O都被关闭。
读写stream
对于一个打开的stream,可以使用3种不同的类型的非格式化I/O以及格式化I/O,对其进行读写操作。
3种非格式化I/O包括:
- 单字符的I/O。如果流是带缓冲的,标准I/O会负责处理缓冲。
- 单行的I/O。这里需要注意一下,单行I/O指定的buffer和标准I/O的buffer不一样。
- 直接I/O(direct I/O)。
ferror和feof, clearerr函数和属性
不管是出错还是到达文件结束,getc,fgetc和ungetc等许多函数都返回同样的值EOF,EOF是-1,可以使用ferro和feof判断到底是出错还是到达文件尾端。大多数实现中是为每个流在FILE对象中维护了出错标志和文件结束标志,可以使用clearerr清除相应的标志。函数的原型如下:1
2
3
4
5
void clearerr(FILE *stream);
int feof(FILE *stream);
int ferror(FILE *stream);
单字符I/O
getc, fgetc和getchar函数可以用于一个读一个字符。它们的原型如下:
getc, fgetc和getchar, ungetc原型
1 |
|
getc, fgetc和getchar, ungetc性质
getc和fgetc功能一样,只不过getc可以被实现为宏,而fgetc不能被实现为宏。所以:
getc的参数不应该是具有副作用的表达式,因为它可能会被计算多次。fgetc一定是函数,所以可以得到它的地址。可以当做参数传递给其他函数。fgetc的调用时间通常要比getc长,因为调用函数的时间通常比调用宏的时间长。
ungetchar函数和属性
- 从流中读取的数据可以送回流中。
- ISO C规定可以支持任何次数的回送,但是一次只能送一个字符。
- 回送的字符可以不是上次读到的字符。
- 回送的字符不能是
EOF,但是读到文件尾端时,还可以回送一个字符,因为一次成功的ungetc调用会清除EOF标志。 - 用
ungetc只能将字符写入到标准I/O库的流缓冲区中,并没有将它们写到底层设备或者文件中。
函数的原型如下:1
2
3
int ungetc(int c, FILE* fp);
输出函数putc, fputc和putchar
它们的原型如下:1
2
3
4
5
int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c); //相当于putc(c, stdout);
单行I/O
fgets和gets,getline提供了单行输入的功能,单行I/O需要指定一个缓冲区,这个缓冲区是用户自己定义的,是应用程序级别的,它和标准I/O的buffer不一样,我们可以通过setbuf和setvbuf设置标准I/O的buffer,这是标准I/O即库函数层级的,而read和write等使用的buffer又是一类buffer,这是系统调用层级的,我们也可以自己指定。
它们的原型如下:
fgets和gets,getline原型
1 |
|
fgets和gets,getline性质
gets从标准输入读,而fgets从指定的流中读gets不会读入'\n',而fgets,getline都会读入'\n';fgets需要指定缓冲的长度,遇到"\n"停止,但是不能超过n-1个字符,读入的字符送入缓冲区。缓冲区以NULL字节结尾,如果这一行包含最后一个换行符超过了n-1个字符,fgets只返回一个不完整的行,但是这一行还是以NULL结束,下一次调用继续从该行读。gets不推荐使用,因为没有指定缓冲区的长度,可能会造成缓冲区溢出,很危险。
fputs和puts原型
1 |
|
fputs和puts性质
fputs将一个以NULL字节结束的字符串写到指定的流中,尾端的NULL不输出。这并不是每次输出一行,只有NULL前面的字节中包含'\n'时,才会输出一行。puts不会输出NULL字节,但是会自动将字符串后添加一个换行符。puts并不像gets那样不安全,但是因为自动加了换行符很难受。所以尽量使用fgets和fputs。
二进制直接I/O
除了可以以字符和行为单位进行读取,还可以使用二进制stream进行I/O。
fread和fwrite原型
1 |
|
fread和fwrite性质
fread和fwrite通常用来读写一个二进制数组或者一个结构体。ptr是要读写的首地址,size是每个对象的大小,nmemb是要写的对象的个数。fread和fwrite返回读写的对象数,读出错或者到达文件结尾,返回的数可以少于nmemb。可以调用ferror或者feof判断是结束还是出错。如果写返回的数值小于nmemb,那么就是出错。fread只能用于读在同一个系统上的数据,因为不同的系统上二进制文件的格式可能不同。fread和read的区别,read是系统调用,而fread是ISO C的函数。read的buf大小是字节,而fread的size是每个对象的大小,nmemb是对象的个数。
格式化I/O
除了三种非格式化的I/O,还有标准化I/O函数。标准化I/O函数需要指定格式说明。
输出格式说明
格式说明控制其余参数如何编写,以后如何限制。每个参数按照转换说明编写,转换说明以%号开始。除了转换说明外,格式化字符串中的其他字符都按照原样输出。
一个输出格式说明由四个可选部分构成:
%[flags][fldwidth][precision][lenmodifier] convtype
flags
',将整数按千位分组字符- ‘-’,左对齐
+,显示带符号数的正负号,如果第一个字符不是正负号,在前面加上一个空格#,指定另一种形式,比如0x指定十六进制0,添加前导0而不是空格进行填充
fldwitdth
最小宽度,多余字符用空格填充
precision
整形转换后最少输出数字位数
浮点数转换后小数点后的最少位数。
字符串转换后最大字节数
精度使用一个.,然后跟着一个可选的非负十进制整数或者x。
lenmodifier
l, ll , L分别表示long, long long以及long double。
convtype
d,i,有符号十进制o,无符号八进制u,无符号十进制x,X,无符号十六禁止f,F,双精度浮点数e,E,指数形式双精度浮点苏g,Ga,A,十六进制指数形式双精度浮点数c,字符s,字符串p,指向void的指针n,%,一个%字符C,宽字符,等于lcS,宽字符串,等于ls
常见的格式化输出函数原型如下:
printf, frpintf, dprintf, snprintf和fpritnf原型
1 |
|
printf,frpintf, snprintf,dprintf和fpritnf性质
printf将格式化数据输出到标准输出fprintf将格式化数据写到指定的流。dprintf将格式化数据写到指定的文件描述符。sprintf将格式化数据送入数组buf中,sprintf在数组的尾端自动加一个null字节,但是该字符不包含在返回值中。sprintf可能会造成buf指向的缓冲区溢出,调用者有责任保证该缓冲区足够大。snprintf是为了解决缓冲区溢出的问题而引入的,它需要显式指定缓冲区的长度,超过这个长度的话,输入数据都会被丢弃,同样ssprintf在数组的尾端自动加一个null字节,但是该字符不包含在返回值中。
输入格式说明
一个输入格式转换说明由三个可选部分:
%[*][fldwidth][m][lenmodifier] convtype
- fldwidth用于说明最大宽度
- lenmodifier说明要用转换结果赋值的参数大小,
printf函数族支持的长度修饰符同样能够得到scanf函数族的支持。 - 而convtype符号和
printf中类似,但是有一些区别。比如,输入中的带符号数可以复制给无符号类型。
scanf, fscnaf, sscanf原型
1 |
|
scanf, fscnaf, sscanf性质
scanf用于分析输入字符串,并将字符序列转换成指定类型的变量。格式后的各个参数给出了变量的地址,用转换结果对这些变量赋值。- 格式说明控制如何转换参数,以便于对他们赋值,除了转换说明和空白字符外,格式字符串中的其他字符必须和输入匹配,如果有一个不匹配,就停止处理。
标准I/O效率
fgets, fgetc, getc, read这几个函数,哪个效率更高?
当他们同时读取一个300万行的98.5M的程序时,read效果最好。它们的系统CPU时间基本一样,但是用户CPU时间查了很多,以及等待I/O的时间也差了很多。为什么呢?
- 系统CPU时间相同,因为它们对内核提出的读写请求数基本相同。
- CPU时间差太多是因为,
getc和fgetc需要进行上亿次的循环(上亿个字符),而fgets需要进行百万次的循环,而read只需要几万次(缓冲区大小设置为4096时)。 fgetc和read缓冲区大小设置为1时,read要慢很多,因为read调用了两亿次系统调用,而fget调用了两亿次函数调用。系统调用的时间和各项开销要比函数调用大得多。
定位stream
有三种方法对I/O stream进行定位,分别是ftell和fseek,ftello和fseeko,fgetpos和fsetpos。它们的原型如下:
ftell, feek, ftello, fseeko和fgetpos, fsetpos原型
1 |
|
ftell, feek, ftello, fseeko和fgetpos, fsetpos性质
ftell和fseek假设文件的位置可以存放在一个长整形中,而ftello和fseeko使用off_t代替了长整形。除此之外,它们完全相同。fgetpos和fsetpos是ISO C的标准,其他是SUS,所以跨平台时,使用fgetpos和fsetpos。- 对于二进制文件,
whence可以使用SEEK_SET,SEEK_CUR,这是跨平台的,而SEEK_END不是平台的。 - 对于文本文件,
whence必须要用SEEK_SET且offset只能是0或者ftell返回的值。
实现细节
所有的standard I/O库都要使用到文件的I/O。每个I/O stream都有一个和其相关的文件描述符,可以使用fileno函数获得stream的文件描述符。
临时文件
ISO C提供了两个函数tmpnam和tmpfile帮助创建临时文件。它们的原型如下:
tmpnam和tmpfile原型
1 |
|
tmpnam和tmpfile性质
tmpnam产生一个与现有文件名不同的一个有效路径名字符串。避免使用tmpnam。tmpfile创建一个临时二进制文件(wb+),在关闭文件或者程序结束时自动删除这个文件。注意UNIX对于二进制文件不做特殊区分。tmpfile函数经常使用的标准UNIX技术是先使用tmpnam产生一个唯一的路径名,然后使用它创建一个文件,并且立刻unlink它。注意,对一个文件unlink之后,如果链接计数等于0,并不立即删除它,因为可能有进程在使用这个文件,关闭文件时才删除文件。mkdtemp和mkstemp是XSI的扩展部分。mkstemp和mkdtemp都需要传入一个字符串,它的后六位设置为XXXXXX,函数通过将这些占位符替换成不同的字符构建一个唯一的路径名。如果只指定了名字,就创建在当前目录下。mkdtemp创建的目录的权限是S_IRUSR,S_IWUSR,S_IXUSR。mkstemp创建的文件的权限是S_IRUSR,S_IWUSR,可以使用umask进行修改。mkstemp创建的文件不会被自动删除。
内存stream
Standard I/O把数据缓存在内存中,因此字符和单行的I/O更有效一些,我们也可以使用setbuf和setvbuf让标准I/O库使用自己指定的缓冲区。
在SUS4之后添加了对memory streams的支持,这些standard I/O streams没有底层文件支持,但是仍然可以使用FILE指针访问,所有的I/O都是通过在缓冲区和主存中来回交换字节实现的。这些流虽然看起来像文件流,但是某些特征更像字符串操作。
有三个函数可以用于内存流的创建,它们分别是fmemopen,open_memstream和open_wmemstream。
fmemopen函数和属性
1 |
|
fmemopen函数open memory as streamfmemopen函数允许调用者提供缓冲区用于memory stream,size指定了缓冲区大小的字节数。如果buf为空,fmemopen会分配size字节数的缓冲区,流关闭时缓冲区会被释放。type和fopen的取值一样,总共有15种取值,
open_memstream和open_wmemstream函数和属性
1 |
|
它们一个面向字节,一个面向宽字节。它们和fmemopen之间的区别:
- 创建的流只能打开;
- 不能指定自己的缓冲区,但是可以访问缓冲区地址和大小。
- 关闭流后需要自己释放缓冲区
- 对流添加字节会增加缓冲区大小。
- 缓冲区地址和长度只有在调用
fclose或者fflush后才有效。这些值只有在下一次流写入或者调用fclose前。
标准I/O的替代软件
标准I/O库的一个不足是效率不高。这和它复制的数据量有关。每当使用一次fgets和fputs时,通常需要复制两次数据,一次是在用户程序的行缓冲区和标准I/O缓冲区之间,一次是在内核和标准I/O缓冲区之间。
OK,这章我就认识到了这一个很重要的知识点。。
使用fgets需要用户指定fgets使用的缓冲区,或者使用getline,如果传入的指针指向NULL,getline会负责分配缓冲区大小。
标准I/O可以设置行缓冲和全缓冲,如果设置缓冲的话也需要一个缓冲区,通常是由系统指定的,当然也可以通过setbuf和setvbuf自己进行更改。而在setbuf中,如果buf是NULL的话,是关闭缓冲区,如果不为空的话,必须是BUFSIZ大小。在setvbuf中,通过mode指定缓冲区的类型,buf是NULL的话,库函数负责分配缓冲区。否则buf是多大就用多大的缓冲区。
直接使用系统调用read和write函数也需要设置缓冲区,这是系统调用级别的,大小可以任意指定,通常使用sturct stat.st_blksize的大小。标准I/O库用的缓冲区和read,write指的是一个(我自己的理解)。
内核中有buffer cache和page cache,调用write只是将数据复制到buffer cache和page cache,然后排入队列,实际的写磁盘操作可能在满足某个条件之后才实际写入磁盘。
1 |
|
参考文献
1.《APUE》第三版
2.https://stackoverflow.com/questions/20937616/what-is-the-difference-between-a-stream-and-a-file