第五章:标准IO库
第三四章的内容讲述的是Unix标准(POSIX)。
C语言也有标准--ANSI C,它也规定了一打文件相关的接口,也就是本章要讲述的标准IO。
标准IO封装了系统级的IO,有很好的移植性。
标准IO处理了IO缓存问题,用户不必关心IO效率(IO efficiency,前面有介绍)。
然而缓存为标准IO带来方便的同时,也引入了新的复杂性。
缓存的3种类型:
全缓存
主要用于磁盘文件操作,只有当缓存填满后才进行实际IO(写入磁盘)。
也可以手动调用fflush来执行实际IO。
行缓存
多用于标准输入输出,在输入输出时遇到换行符时执行IO操作(显示在屏幕上?)。
当缓存填满都没遇到换行符,系统也会调用IO操作(否则会溢出)。
//TODO
如果标准IO向无缓存或行缓存(需向内核请求数据)流请求输入,所有行缓存流都会flush。
之所以要在括号内说明行缓存,是因为数据可能已经在缓存中,而不要向内核请求。
所有无缓存流的数据都要向内核请求才能得到。
不缓存
相当于直接调用被封装的底层系统IO,只要有数据就会立马执行IO操作。
STD_ERR就是一个很好的例子(错误信息能立马显示在屏幕上)。
ANSI C规定标准输入输出不涉及到交互设备才是全缓存,而标准出错总是不带缓存。
利用下面的接口,我们可以设置对于标准IO的缓存:
#include
void setbuf(FILE *restrict fp, char *restrict buf);
int setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);
上面的函数应在打开IO流后和对IO流执行任何操作前。
setbuf可以打开关闭缓存,如果buf参数为NULL,则将流转换成无缓存。
如果buf不为NULL(长度为BUFSIZ),则转换成带缓存(是全缓存还是行缓存则要有系统和设备类型而定)。
setvbuf的mode参数可为:_IOFBF _IOLBF _IONBF
如果buf为NULL且mode不为_IONBF,则系统自动选择合适的缓存大小。
如果使用一个在函数内部的自动变量作为缓存,则在函数返回前,必须关闭流(变量生命周期)。
int fflush(FILE *fp);
flush IO流,当fp=null时,flush所有的流。
FILE *fopen(const char *restrict pathname, const char *restrict type);
FILE *freopen(const char *restrict pathname, const char *restrict type, FILE *restrict fp);
FILE *fdopen(int filedes, const char *type);
freopen在特定的流上打开特定的文件,如果fp已打开,则先关闭fp(很像dup2)。
fdopen用来打开特定的filedes,由Unix系统底层的IO得到的文件描述符打开标准IO流(对网络、管道非常有用)。
type参数:
r/rb w/wb a/ab 读/写/追加数据(只读 或 只写)
r+/r+b/rb+ w+/w+b/wb+ a+/a+b/ab+ 更新文件(可读 且 可写,a则为追加)。
b代表用二进制方式打开,指定是否自动转换换行符(b在Unix环境中没有作用)。
+代表混合模式,只要有+号出现,返回的流必定可写也可读。
r和r+要求文件必须存在,其他模式都会自动创建文件(POSIX要求这种情况下创建的文件的权限为666)。
w和w+会对已存在的文件进行truncate。
带a就只能对文件追加数据。
fdopen函数中有2个例外:不会对任何文件进行truncate,也不创建任何文件。
当以混合模式打开流(+)有一定的限制
//TODO
输出后,如未执行fflush,fseek,fsetpos,rewind函数,则不能直接输入。
输入后,如未执行fseek,fsetpos,rewind或者输入遇到EOF,则不能直接输出。
int fclose(FILE *fp);
关闭流,将输出buffer的内容写入文件,丢弃输入buffer的内容,释放系统申请的buffer。
进程正常结束时,系统会刷新并关闭该进程打开的所有标准IO流。
读写流
有三种打开方式返回流,而对返回的流,也有三种方式对其进行读写。
一次读写一个字符
一次读写一行
direct io,一次读写n个object。
int getc(FILE *fp);
int fgetc(FILE *fp);
getchar一般都被定义为getc(stdin),而getc和fgetc则前者为宏定义,后者为函数。
所以getc的参数不能是带有副作用的表达式(i++之类的,展开宏后,i++会被执行多次)。
fgetc有地址,而getc没有 。getc编码效率高,开销小(无函数调用)。
三个函数都返回int,返回值可能是[ [0-255] [-1(eof)] ]。本来应该返回unsigned char才对,但是由于-1的存在,只有选择返回int。
出错和到达文件结尾都返回的都是EOF,所以要进一步判断就得使用:
int ferror(FILE *fp);
int feof(FILE *fp);
FILE结构内部维护了两个标志:error和eof,两者都可以使用clearerr清除。
void clearerr(FILE *fp);
int ungetc(int c, FILE *fp);
我们可以使用ungetc将c回送到流中。
eof不能被回送,但当读取出最后一个字符(此时EOF标志已经被打开),仍可以回送字符。这是因为ungetc会清除eof标志。
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);
参考对照输入函数系列
char *fgets(char *restrict buf, int n, FILE *restrict fp);
char *gets(char *buf);
fgets从fp中读取一行,但该行字符数不能超出n-1(第n个字符必须是null)。
如果n-1个字符都无换行符,则返回n-1个字符+null,下次接着读该行。
gets由于不能指定buf长度,可能导致溢出,不推荐使用函数。
int fputs(const char *restrict str, FILE *restrict fp);
int puts(const char *str);
fputs输出字符串str,但不保证str是一行数据(包含换行符)。
puts虽然没有溢出的危险,但是它自动为输出添加换行符。
两者都不输出null。
标准IO的效率
system call / posix / libc
-----------------------------------------
4 User Program
-----------------------------------------
3 fgetc (ANSI C/Glibc)
----------------------------------------- 无/行/全缓存,1:1/n:1/m:1
2 read (Posix/Linux?)
----------------------------------------- 用户程序设置缓存n:1
1 sys_read (Linux kernel System Call )
----------------------------------------- 内核缓存(习题1答案)
hardware device
-----------------------------------------
上图是Unix/Linux IO系统的层次:
Linux内核管理底层硬件设备,并提供内核调用sys_read。在Linux内核和硬件设备间,有内核缓存。
由于Linux是与POSIX标准兼容的,所以Linux也提供了read函数。
而我们本章讲述的标准IO由ANSI C兼容的GLibc实现。
作者通过表5-4总结出一些很有意思的结论:
1,user time由用户程序决定,也就是程序在用户空间内执行的时间而定(包括用户代码和C库函数)。
2,除开缓存大小为1的read版本,其它版本的system time都一样,是因为其他版本执行的系统调用的次数都差不多(标准IO自动处理了缓存)。
3,使用宏比使用函数代码体积偏大,而运行速度稍快。
4,出人意料的是,fgets版本居然比getc还要快。原来fgets并没有使用getc实现,而是使用了汇编版本的memccpy。
5,比较fgetc和缓存大小为1的read版本后,可以发现用户、系统时间都差别巨大。
其中的主要原因是system call(context switching是一个原因?)比函数调用消耗更大。
二进制IO函数
size_t fread(void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
size_t fwrite(const void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
fread用来从fp流中读nobj个大小为size的对象到ptr中。
fwrite用来从ptr地址开始,将nobj个大小为size的对象写入fp流中。
fread和fwrite都返回写入的对象数目。
由于这两组函数都牵涉到结构体和内存读写,不同机器这方面实现都会有差异,导致在机器间的移植性不高。
1,编译器和硬件导致对齐方式不同,可能使同一结构体在不同机器的大小不一,结构体成员的位移不一。
2,字节顺序(big/little ending)会导致多字节的基本数据类型(int ,float...)排列不一。
定位流
long ftell(FILE *fp);
int fseek(FILE *fp, long offset, int whence);
void rewind(FILE *fp);
fseek设置当前位移,whence可为SEEK_SET , SEEK_CUR, SEEK_END(并不为ANSI C强制要求)。
offset的单位为byte,ftell返回当前文件位移。
rewind重置流位移。
off_t ftello(FILE *fp);
int fseeko(FILE *fp, off_t offset, int whence);
与ftell和fseek的唯一区别是offset被定义为off_t类型,而不是long。
int fgetpos(FILE *restrict fp, fpos_t *restrict pos);
int fsetpos(FILE *fp, const fpos_t *pos);
ISO C引入的函数,能将文件位移保存到pos参数。
格式化输入输出
int printf(const char *restrict format, ...);
int fprintf(FILE *restrict fp, const char *restrict format, ...);
返回输出的字符个数,出错返回负值。
int sprintf(char *restrict buf, const char *restrict format, ...);
int snprintf(char *restrict buf, size_t n, const char *restrict format, ...);
打印到buf指向的内存,返回存储到buf的字符个数。出错返回负值。
#include
#include
int vprintf(const char *restrict format, va_list arg);
int vfprintf(FILE *restrict fp, const char *restrict format, va_list arg);
int vsprintf(char *restrict buf, const char *restrict format, va_list arg);
int vsnprintf(char *restrict buf, size_t n, const char *restrict format, va_list arg);
同上,但是...被替换成arg(va_list在stdarg.h中定义)。
int scanf(const char *restrict format, ...);
int fscanf(FILE *restrict fp, const char *restrict format, ...);
int sscanf(const char *restrict buf, const char *restrict format, ...);
参考Unix手册
实现细节
int fileno(FILE *fp);
标准IO对第三章的IO函数的封装,每个FILE结构内也有一个对应的文件描述符。
而对文件描述符调用fileno,则可以返回fp对应的描述符。
当stdin,stdout,stderr连到终端时(即未重定向),一般前两者都是行缓存,stderr则是无缓存。
当stdin,stdout,stderr重定向到磁盘文件时,前两者是全缓存,stderr还是无缓存。
临时文件
char *tmpnam(char *ptr);
FILE *tmpfile(void);
tmpnam返回一个独一无二的文件名。当ptr为NULL时,文件名保存在静态变量中,再调用tmpnam则会覆盖原文件名。
tmpfile创建一个二进制文件,并用权限wb+打开,返回流。
tmpfile创建并打开文件,然后马上unlink该文件。一旦打开进程结束,该文件一定会被删除(参见前面内容)。
另外XSI还定义了两个临时文件相关的函数
char *tempnam(const char *directory, const char *prefix);
返回一个文件路径,使其以prefix作为文件名前缀,并在directory目录下。
如果定义了环境变量TMPDIR,优先使用。
若未定义TMPDIR,且directory参数不为NULL,则使用directory。
若未定义TMPDIR,且directory为NULL,则使用内定义的P_tmpdir字符串。
如果以上条件都不满足,则使用一本地目录,一般实现为/tmp。
#include
int mkstemp(char *template);
注意头文件为stdlib.h,mkstemp与tmpfile类似,但是是返回文件描述符。
mkstemp创建的临时文件的名字有template确定,template的最后6个字符必须是"XXXXXX"。
X会被替换成其他字符,进而生成独一无二的文件名(新的文件名会保存在template参数中)。
mkstemp不会unlink创建的文件,需要用户手动删除。
使用tmpnam和tempnam都是有缺陷的。由于生成文件名和创建文件不是一个原子操作,有可能被打断,
而导致多个进程同时创建临时文件时,有可能生成相同的文件名(虽然几率小)。所以还是推荐使用tmpfile和mkstemp。
标准IO外的其他选择
标准IO对Unix IO例程进行了封装,Unix IO例程对系统调用进行了封装。
由于封装层数多,而且两层间传递数据都要拷贝缓存,导致标准IO速度不够快。
标准IO外的其他选择有:fio,sfio,mmap。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/7618884/viewspace-664129/
,如需转载,请注明出处,否则将追究法律责任。