在学习使用C语言的时候,我们总会遇到各种各样的输入输出函数。fopen,open,read,write,scanf,printf等等。那么到底什么时候应该用什么,我么该如何选择呢,我之前总是有疑问。在认真学习了《csapp》《apue》之后,总算是有点眉目了。下面做一个记录。
首先明确一下什么是输入输出,输入输出是在主存和外部设备之间拷贝数据的过程。输入输出是相对主存而言的。在编写C语言程序的时候,我们可能会遇到两类I/O函数,一类是系统I/O,一类是标准I/O。
系统I/O:
open,read, write,lseek,stat,close等
标准I/O:
fopen, fdopen, fread, fwrite, fscanf, fprintf, sscanf, sprintf,fgets,fputs
fflush, fseek, fclose等
标准I/O是系统I/O带缓冲的替代品,主要用来克服系统I/O的性能不足,是磁盘和终端设备I/O之选。
大多数C程序员在他们的职业生涯中只使用标准I/O,而从不涉及低级I/O函数。
标准I/O有一些限制:
限制一:
跟在输出函数之后的输入函数,如果中间没有插入对fflush/fseek/fsetpos或者rewind的调用,一个输入函数不能跟随在一个输出函数之后。fflush函数清空输出流的缓冲区,实际就是写入文件,这样的话,输入的数据就不会和因为输出而填在输入缓冲区的数据弄混了。后三个函数通过使用lseek函数来重置当前的文件位置。
限制二:
跟在输入函数之后的输出函数,如果中间没有插入fseek/fsetpos或者rewind的调用,一个输出函数不能跟随在一个输入函数之后,除非该输入函数遇上了一个EOF。因为这样的话,输出函数会连带缓冲区中的数据一起输出到文件中。
fseek,fsetpos和rewind都是改变文件表的当前位置指针,很好理解。
在这里的关键是fflush函数的理解,为什么对于限制二不能用fflush。
根据Linux man手册,
fflush有以下原型
int fflush(FILE *stream)
对于输出流,fflush将用户缓冲区的数据写到输出对象,或者通过流的写函数来更新流。对于可定位文件相对应的输入流(磁盘文件,但不能是管道或者终端设备),fflush抛弃
从对应文件中取出到缓冲区中但是还没有被应用所用的数据。
如果stream参数是NULL, fflush flush所有打开的输出流。
fflush并不会改变文件表的当前位置。
在对网络使用标准I/O的时候会遇到问题。因为对套接字使用lseek函数是非法的。
对流I/O的第一个限制能够通过采用在每个输入操作前刷新缓冲区来满足(实验验证ubuntu16.04,无论是否使用fflush效果是一样的,相信在fread实现中有内置的fflush),然而要满足第二个限制
唯一的办法是,对同一个打开的套接字描述符打开两个流,一个用来读一个用来写。但是fclose(fpin)和fclose(fout),试图关闭同一个底层的套接字描述符,所以第二个close会失败,对于顺序程序来说不是问题,但是对于线程化的程序来说关闭一个已经关闭的描述符会导致灾难(为什么会导致灾难)。
因此《csapp》建议使用健壮的rio函数,如果需要格式化的输出,使用sprintf函数在存储器中格式化一个字符串,然后用rio_writen把它发送到套接字接口(直接不用缓冲区,相比原始接口仅仅重启中断的系统调用)。如果需要格式化输入,使用rio_readlineb来读一个完整的文本行,然后用sscanf从文本行中提取不同的字段。实际上就是说输出就不要用buffer了。