本文讲述由ISO C定义的标准I/O库。这个库已经拥有非常长的历史了,它由D.R.在1975年左右编写,现在已经过去45年了。但是ISO C几乎没有对标准I/O库做出修改。不用我说,大家也知道这个库存在的问题应该是非常多的。
Linux下的不带缓冲的I/O是围绕文件描述符来展开的。标准库的则不是,标准库的操作是围绕流(stream)这个概念来进行的。例如:标准输入流,标准输出流,标准出错流。这3个流是自动被进程使用的。他们其实和文件描述符STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO引用相同的文件。
使用文件描述符的I/O是不带缓冲的(当然了,这里所说的不带缓冲指的是进程中使用这两个函数不会自动缓冲,每使用一次就会进行一次系统调用,实际上除了原始磁盘I/O之外,其它的所有磁盘I/O都会经过内核缓冲区。),而标准I/O库为了减少read和write操作,使用了缓冲。
标准I/O提供了缓冲,但是成也萧何,败也萧何啊!这个缓冲的设计也是它的败笔吧!
标准I/O提供了3种缓冲方式。
在这种情况下,在填满标准I/O缓冲区以后,才进行I/O操作。在第一次执行I/O操作的时候,标准I/O会使用malloc来获取所需要的缓冲区。那么这时候有个问题,缓冲区没满就不进行实际的I/O操作,但是你想写入磁盘,怎么办?别慌,标准I/O设计了一个冲洗(flush)操作。你可以使用函数fflush来强制冲洗一个流。冲洗意味着将缓冲区的内容写到磁盘中。
它有一个特殊情形,就是参数stream是NULL。这个时候表示强制冲洗所有输出流。
行缓冲就是当输入和输出中遇到换行符时,标准I/O执行实际I/O操作。当我们使用scanf和printf的时候,实际上就是行缓冲在起作用。行缓冲的长度是固定的,因此如果你在一行输入的内容过的,导致在你还没有换行的时候,也会发生实际的I/O操作。还有就是当你通过标准I/O库从一个不带缓冲或者是带行缓冲的流得到输入数据。那么就会强制冲洗所有行缓冲的输出流。
标准I/O对字符不进行缓冲。通常标准出错是不带缓冲的,这样就能使的出错信息及时打印出来。
规则就是如此的简单粗暴。它只说了什么时候全缓冲和不全缓冲。在Linux下。通常是这样的。
我们可以使用下面的库函数来更改缓冲方式。
这些函数的只能在打开流之后调用。所以我们可以看到这些函数的第一个参数都是FILE *。需要注意的是setbuf(),setbuffer()以及setlinebuf其实都将调用setvbuf函数。因此,我们来关注一下setvbuf()函数。
也就是说buf和size是由mode决定的。但是当buf是NULL时,标准I/O会自动为该流分配适当长度的缓冲区(就是size所指定的值)。当然只有这个被指定的模式会受到影响,下次还是会新分配缓冲的。
其余的函数说明如下:
在Linux下这三个函数可以用来打开流。仔细观察可以发现fdopen()函数需要一个文件描述符做参数。而ISO C没有涉及文件描述符,所以只能在POSIX标准之下使用这个函数。另外对于fdopen()而言,它的mode参数的含义也略有不同。这是因为文件的权限在被open或者creat的时候已经指定好了。并且fdopen()函数并不能用来创建一个文件,很明显它需要一个文件描述符,既然有了文件描述符,那么文件肯定已经存在了。好了,下面我们先看一下mode的取值。
值得注意的是Linux内核并不区分文本文件和二进制文件。因此在Linux下使用带有b的参数是没有意义的(没有作用)。
标准I/O库提供了非常多的函数来进行读写操作。下面给出一些读写相关的函数。
有个问题需要注意,那就是返回值。
fgetc(),getc()和getchar()无论是遇到文件结尾还是错误都会返回同样的值。为了区分这两种情形,必须使用ferror或者feof函数。
clearerr()函数可以用来清楚1.出错标志,2.文件结束标志。上述的fileno函数可以被实现为宏。宏和函数的区别还是比较大的。在使用某些函数的时候,需要注意它是否被实现为宏,如果是,那么意味着一下几点:
1.参数不要具备副作用。
2.不能传递宏的地址,它没有地址。
3.宏比函数快。
上述的函数之中,gets()函数由于没有指定缓冲区的大小。这曾造成过1988年的蠕虫事件。因此,当大多数人在Visual Studio2015之后的版本上书写C语言程序的时候,使用gets和scanf函数会报错。
VS不仅报错了,还让你使用scanf_s()函数来代替scanf函数。但是带来问题是比较差的可移植性。在某些类Unix操作系统上已经弃用了该接口。我们尽量不要使用不安全的函数。
前面的I/O函数都是一次读写一行或者是单个字符,这在读写大文件的时候并不适合。为此,提供了下面的函数来执行二进制I/O操作。
这两个函数仍旧存在一些问题。那就是在不同的系统上工作的时候,可能由于struct对齐方式,以及是否遵从IEEE 754标准造成程序出错。多年之前,所有的Unix操作系统都运行在PDP-11计算机上,所以没有任何问题。
上述函数在类Unix系统上没有问题,但是如果在Window下可能就行不通。ISO C提供了fgetpos()和fsetpos()函数。
格式化I/O能够漂亮的处理输入输出,但是格式转换符比较复杂,种类繁多。在此处不说明。只给出相关的函数。
在Unix中,标准I/O库最后还是需要调用不带缓冲的I/O函数。每个标准I/O都有一个与其相关联的文件描述符,可以使用fileno()函数来获得文件描述符。需要注意的是fileno()函数是POSIX标准提供的。
前面已经说过了,标准I/O的历史已经非常长了,它存在问题也比较多。很明显标准I/O的效率不高。它需要在内核缓冲区复制一次数据,然后在用户进程内存中在复制一次数据。
另外的问题可能就是不够安全,微软已经在Windows平台提供了更加安全的函数。
在Linux下替代它们的可以有sfio库,以及使用mmap()函数的ASI包。
前文说过成也萧何,败也萧何。标准I/O使用的缓冲技术正是产生很多问题和混淆的地方。