本文转载于:http://www.360doc.com/content/11/0521/11/5455634_118306098.shtmlphp
首先,先稍微了解系统调用的概念:
系统调用,英文名systemcall,每一个操做系统都在内核里有一些内建的函数库,这些函数能够用来完成一些系统系统调用把应用程序的请求传给内核,调用相应的的内核函数完成所需的处理,将处理结果返回给应用程序,若是没有系统调用和内核函数,用户将不能编写大型应用程序,及别的功能,这些函数集合起来就叫作程序接口或应用编程接口(Application Programming Interface,API),咱们要在这个系统上编写各类应用程序,就是经过这个API接口来调用系统内核里面的函数。若是没有系统调用,那么应用程序就失去内核的支持。
如今,再聊不带缓存的I/O操做:
linix对IO文件的操做分为不带缓存的IO操做和标准IO操做(即带缓存),刚开始,要明确如下几点:
1:不带缓存,不是直接对磁盘文件进行读取操做,像read()和write()函数,它们都属于系统调用,只不过在用户层没有缓存,因此叫作无缓存IO,但对于内核来讲,仍是进行了缓存,只是用户层看不到罢了。若是这一点看不懂,请看第二点;
2:带不带缓存是相对来讲的,若是你要写入数据到文件上时(就是写入磁盘上),内核先将数据写入到内核中所设的缓冲储存器,假如这个缓冲储存器的长度是100个字节,你调用系统函:html
ssize_twrite (int fd,const void * buf,size_t count);程序员
写操做时,设每次写入长度count=10个字节,那么你几要调用10次这个函数才能把这个缓冲区写满,此时数据仍是在缓冲区,并无写入到磁盘,缓冲区满时才进行实际上的IO操做,把数据写入到磁盘上,因此上面说的“不带缓存不是就没有缓存直写进磁盘”就是这个意思。编程
那么,既然不带缓存的操做实际在内核是有缓存器的,那带缓存的IO操做又是怎么回事呢?缓存
带缓存IO也叫标准IO,符合ANSI C 的标准IO处理,不依赖系统内核,因此移植性强,咱们使用标准IO操做不少时候是为了减小对read()和write()的系统调用次数,带缓存IO其实就是在用户层再创建一个缓存区,这个缓存区的分配和优化长度等细节都是标准IO库代你处理好了,不用去操心,仍是用上面那个例子说明这个操做过程:函数
上面说要写数据到文件上,内核缓存(注意这个不是用户层缓存区)区长度是100字节,咱们调用不带缓存的IO函数write()就要调用10次,这样系统效率低,如今咱们在用户层创建另外一个缓存区(用户层缓存区或者叫流缓存),假设流缓存的长度是50字节,咱们用标准C库函数的fwrite()将数据写入到这个流缓存区里面,流缓存区满50字节后在进入内核缓存区,此时再调用系统函数write()将数据写入到文件(实质是磁盘)上,看到这里,你用该明白一点,标准IO操做fwrite()最后仍是要掉用无缓存IO操做write,这里进行了两次调用fwrite()写100字节也就是进行两次系统调用write()。优化
若是看到这里尚未一点眉目的话,那就比较麻烦了,但愿下面两条总结可以帮上忙:url
无缓存IO操做数据流向路径:数据——内核缓存区——磁盘spa
标准IO操做数据流向路径:数据——流缓存区——内核缓存区——磁盘操作系统
下面是一个网友的看法,以供参考:
不带缓存的I/O对文件描述符操做,下面带缓存的I/O是针对流的。
标准I/O库就是带缓存的I/O,它由ANSI C标准说明。固然,标准I/O最终都会调用上面的I/O例程。标准I/O库代替用户处理不少细节,好比缓存分配、以优化长度执行I/O等。
标准I/O提供缓存的目的就是减小调用read和write的次数,它对每一个I/O流自动进行缓存管理(标准I/O函数一般调用malloc来分配缓存)。它提供了三种类型的缓存:
1)全缓存。当填满标准I/O缓存后才执行I/O操做。磁盘上的文件一般是全缓存的。
2)行缓存。当输入输出遇到新行符或缓存满时,才由标准I/O库执行实际I/O操做。stdin、stdout一般是行缓存的。
3)无缓存。至关于read、write了。stderr一般是无缓存的,由于它必须尽快输出。
通常而言,由系统选择缓存的长度,并自动分配。标准I/O库在关闭流的时候自动释放缓存。
在标准I / O库中,一个效率不高的不足之处是须要复制的数据量。当使用每次一行函数fgets和fputs时,一般须要复制两次数据:一次是在内核和标准I / O缓存之间(当调用read和write时),第二次是在标准I / O缓存(一般系统分配和管理)和用户程序中的行缓存(fgets的参数就须要一个用户行缓存指针)之间。
无论上面讲的到底懂没懂,记住一点:
使用标准I / O例程的一个优势是无需考虑缓存及最佳I / O长度的选择,而且它并不比直接调用read、write慢多少。
带缓存的文件操做是标准C库的实现,第一次调用带缓存的文件操做函数时标准库会自动分配内存而且读出一段固定大小的内容存储在缓存中。因此之后每次的读写操做并非针对硬盘上的文件直接进行的,而是针对内存中的缓存的。什么时候从硬盘中读取文件或者向硬盘中写入文件有标准库的机制控制。不带缓存的文件操做一般都是系统提供的系统调用,更加低级,直接从硬盘中读取和写入文件,因为 IO瓶颈的缘由,速度并不如意,并且原子操做须要程序员本身保证,但使用得当的话效率并不差。另外标准库中的带缓存文件IO是调用系统提供的不带缓存IO实现的。
这里为了说明标准I/O的工做原理,借用了glibc中标准I/O实现的细节,因此代码可能是不可移植的.
1. buffered I/O, 即标准I/O
首先,要明确,unbufferedI/O只是相对于buffered I/O,即标准I/O来讲的.
而不是说unbuffered I/O读写磁盘时不用缓冲.实际上,内核是存在高速缓冲区来进行
真正的磁盘读写的,不过这里要讨论的buffer跟内核中的缓冲区无关.
buffered I/O的目的是什么呢?
很简单,buffered I/O的目的就是为了提升效率.
请明确一个关系,那就是,
buffered I/O库函数(fread, fwrite等,用户空间)<----call---> unbuffered I/O系统调用(read,write等,内核空间)<------->读写磁盘
buffered I/O库函数都是调用相关的unbuffered I/O系统调用来实现的,他们并不直接读写磁盘.
那么,效率的提升从何而来呢?
注意到,buffered I/O中都是库函数,而unbuffered I/O中为系统调用,使用库函数的效率是高于使用系统调用的.
buffered I/O就是经过尽量的少使用系统调用来提升效率的.
它的基本方法是,在用户进程空间维护一块缓冲区,第一次读(库函数)的时候用read(系统调用)多从内核读出一些数据,
下次在要读(库函数)数据的时候,先从该缓冲区读,而不用进行再次read(系统调用)了.
一样,写的时候,先将数据写入(库函数)一个缓冲区,屡次之后,在集中进行一次write(系统调用),写入内核空间.
buffered I/O中的fgets, puts, fread, fwrite等和unbufferedI/O中的read,write等就是调用和被调用的关系
下面是一个利用buffered I/O读取数据的例子:
#include <stdlib.h> |
buffered I/O中的"buffer"究竟是指什么呢?
这个buffer在什么地方呢?
FILE是什么呢?它的空间是怎么分配的呢?
要弄清楚这些问题,就要看看FILE是如何定义和运做的了.
(特别说明,在平时写程序时,不用也不要关心FILE是如何定义和运做的,最好不要直接操做
它,这里使用它,只是为了说明buffered IO)
下面的这个是glibc给出的FILE的定义,它是实现相关的,别的平台定义方式不一样.
struct _IO_FILE { |
上面的定义中有三组重要的字段:
1.
char* _IO_read_ptr;
char* _IO_read_end;
char* _IO_read_base;
2.
char* _IO_write_base;
char* _IO_write_ptr;
char* _IO_write_end;
3.
char* _IO_buf_base;
char* _IO_buf_end;
其中,
_IO_read_base 指向"读缓冲区"
_IO_read_end 指向"读缓冲区"的末尾
_IO_read_end - _IO_read_base "读缓冲区"的长度
_IO_write_base 指向"写缓冲区"
_IO_write_end 指向"写缓冲区"的末尾
_IO_write_end - _IO_write_base "写缓冲区"的长度
_IO_buf_base 指向"缓冲区"
_IO_buf_end 指向"缓冲区"的末尾
_IO_buf_end - _IO_buf_base "缓冲区"的长度
上面的定义貌似给出了3个缓冲区,实际上上面的_IO_read_base,
_IO_write_base, _IO_buf_base都指向了同一个缓冲区.
这个缓冲区跟上面程序中的char buf[5];没有任何关系.
他们在第一次buffered I/O操做时由库函数自动申请空间,最后由相应库函数负责释放.
(再次声明,这里只是glibc的实现,别的实现可能会不一样,后面就再也不强调了)
请看下面的程序(这里给的是stdin,行缓冲的例子):
#include <stdlib.h> |
运行的结果以下:
before reading
read buffer base (nil)
read buffer length 0
write buffer base (nil)
write buffer length 0
buf buffer base (nil)
buf buffer length 0
123
after reading
read buffer base 0xb77c5000
read buffer length 4
write buffer base 0xb77c5000
write buffer length 0
buf buffer base 0xb77c5000
buf buffer length 1024
能够看到,在读操做以前,myfile的缓冲区是没有被分配的,在一次读以后,myfile的缓冲区才被分配.
这个缓冲区既不是内核中的缓冲区,也不是用户分配的缓冲区,而是有用户进程空间中的由buffered I/O系统负责维护的缓冲区.
用setbuf设置stdin缓冲区大小后(printf("bufbuffer length %d\n", myfile->_IO_buf_end - myfile->_IO_buf_base);)默认是8192,可是只能读setbuf设置的大小,也就是:printf("readbuffer length %d\n", myfile->_IO_read_end - myfile->_IO_read_base);若是越界程序将执行错误
(固然,用户能够能够维护该缓冲区,这里不作讨论了)
上面的例子只是说明了buffered I/O缓冲区的存在,下面从全缓冲,行缓冲和无缓冲3个方面看一下buffered I/O
是如何工做的.
1.1. 全缓冲
下面是APUE上的原话:
全缓冲"在填满标准I/O缓冲区后才进行实际的I/O操做.对于驻留在磁盘上的文件一般是由标准I/O库实施全缓冲的"
书中这里"实际的I/O操做"实际上容易引发误导,这里并非读写磁盘,而应该是进行read或write的系统调用
下面两个例子会说明这个问题
#include <stdlib.h> |
上面提到的bbb.txt文件的内容是由不少行的"123456789"组成
运行结果:
before reading, myfile->_IO_read_ptr:0
123456789
123456789
123456789
123456789
123456789
123456789
123456789
123456789
123456789
123456789
after reading, myfile->_IO_read_ptr:4
上例中,fgets(buf, 5, myfile);仅仅读4个字符,可是,缓冲区已被写满,(这个缓冲区默认是4096)
可是_IO_read_ptr却向前移动了5位,下次再次调用读操做时,
只要要读的位数不超过myfile->_IO_read_end -myfile->_IO_read_ptr
那么就不须要再次调用系统调用read,只要将数据从myfile的缓冲区拷贝到
buf便可(从myfile->_IO_read_ptr开始拷贝)
全缓冲读的时候,
_IO_read_base始终指向缓冲区的开始
_IO_read_end始终指向已从内核读入缓冲区的字符的下一个
(对全缓冲来讲,buffered I/O读每次都试图都将缓冲区读满)
_IO_read_ptr始终指向缓冲区中已被用户读走的字符的下一个
(_IO_read_end < (_IO_buf_base-_IO_buf_end)) && (_IO_read_ptr ==_IO_read_end)时则已经到达文件末尾
其中_IO_buf_base-_IO_buf_end是缓冲区的长度
通常大致的工做情景为:
第一次fgets(或其余的)时,标准I/O会调用read将缓冲区充满,下一次fgets不调用read而是直接从该缓冲区中拷贝数据,直到
缓冲区的中剩余的数据不够时,再次调用read.在这个过程当中,_IO_read_ptr就是用来记录缓冲区中哪些数据是已读的,
哪些数据是未读的.
#include <stdlib.h> |
上面这个是关于全缓冲写的例子.
全缓冲时,只有当标准I/O自动flush(好比当缓冲区已满时)或者手工调用fflush时,
标准I/O才会调用一次write系统调用.
例子中,fwrite(buf+i, 1, 512, myfile);这一句只是将buf+i接下来的512个字节
写入缓冲区,因为缓冲区未满,标准I/O并未调用write.
此时,myfile->_IO_write_ptr= myfile->_IO_write_base;会致使标准I/O认为
没有数据写入缓冲区,因此永远不会调用write,这样aaa.txt文件得不到写入.
没有注释掉时的运行结果:
0xb77f9000 write buffer base
0xb77f9000 buf buffer base
0xb77f9000 read buffer base
0xb77f9000 write buffer ptr
0xb77f9000 write buffer base
0xb77f9000 buf buffer base
0xb77f9000 read buffer base
0xb77f9000 write buffer ptr
0xb77f9000 write buffer base
0xb77f9000 buf buffer base
0xb77f9000 read buffer base
0xb77f9000 write buffer ptr
0xb77f9000 write buffer base
0xb77f9000 buf buffer base
0xb77f9000 read buffer base
0xb77f9000 write buffer ptr
注释掉myfile->_IO_write_ptr = myfile->_IO_write_base;先后,看看效果
注释掉之后的运行结果:
0xb774e000 write buffer base
0xb774e000 buf buffer base
0xb774e000 read buffer base
0xb774e200 write buffer ptr
0xb774e000 write buffer base
0xb774e000 buf buffer base
0xb774e000 read buffer base
0xb774e400 write buffer ptr
0xb774e000 write buffer base
0xb774e000 buf buffer base
0xb774e000 read buffer base
0xb774e600 write buffer ptr
0xb774e000 write buffer base
0xb774e000 buf buffer base
0xb774e000 read buffer base
0xb774e800 write buffer ptr
上面write buffer ptr在每次往缓冲中写512个字节后地址就增加512.
全缓冲写的时候:
_IO_write_base始终指向缓冲区的开始
_IO_write_end全缓冲的时候,始终指向缓冲区的最后一个字符的下一个
(对全缓冲来讲,buffered I/O写老是试图在缓冲区写满以后,再系统调用write)
_IO_write_ptr始终指向缓冲区中已被用户写入的字符的下一个
flush的时候,将_IO_write_base和_IO_write_ptr之间的字符经过系统调用write写入内核
1.2. 行缓冲
下面是APUE上的原话:
行缓冲"当输入输出中遇到换行符时,标准I/O库执行I/O操做. "
书中这里"执行IO操做"也容易引发误导,这里不是读写磁盘,而应该是进行read或write的系统调用
下面两个例子会说明这个问题
第一个例子能够用来讲明下面这篇帖子的问题
http://bbs.chinaunix.net/viewthread.php?tid=954547
#include <stdlib.h> //而非仅仅上面须要的个字符 |
上例中, fgets(buf, 5, stdin);仅仅须要4个字符,可是,输入行中的其余数据也被写入缓冲区,
可是_IO_read_ptr向前移动了5位,下次再次调用fgets操做时,就不须要再次调用系统调用read,只要将数据从stdin的缓冲区拷贝到buf2便可(从stdin->_IO_read_ptr开始拷贝),stdin->_IO_read_ptr = stdin->_IO_read_end;会致使标准I/O会认为缓冲区已空,再次fgets则须要再次调用read.比较一下将该句注释掉先后的效果
行缓冲读的时候,
_IO_read_base始终指向缓冲区的开始
_IO_read_end始终指向已从内核读入缓冲区的字符的下一个
_IO_read_ptr始终指向缓冲区中已被用户读走的字符的下一个
(_IO_read_end < (_IO_buf_base-_IO_buf_end)) && (_IO_read_ptr ==_IO_read_end)时则已经到达文件末尾
其中_IO_buf_base-_IO_buf_end是缓冲区的长度
#include <stdlib.h> |
这个例子将将FILE结构中指针的变化写入的文件ccc.txt
运行后能够有兴趣的话,能够看看.
上面这个是关于行缓冲写的例子.
stdout->_IO_write_ptr = stdout->_IO_write_base;会使得标准I/O认为
缓冲区是空的,从而没有任何输出.
能够将上面程序中的注释分别去掉,看看运行结果
行缓冲时,下面3个条件之一会致使缓冲区当即被flush
1. 缓冲区已满
2. 遇到一个换行符;好比将上面例子中buf[4]改成'\n'时
3. 再次要求从内核中获得数据时;好比上面的程序加上getchar()会致使立刻输出
行缓冲写的时候:
_IO_write_base始终指向缓冲区的开始
_IO_write_end始终指向缓冲区的开始
_IO_write_ptr始终指向缓冲区中已被用户写入的字符的下一个
flush的时候,将_IO_write_base和_IO_write_ptr之间的字符经过系统调用write写入内核
1.3. 无缓冲
无缓冲时,标准I/O不对字符进行缓冲存储.典型表明是stderr
这里的无缓冲,并非指缓冲区大小为0,其实,仍是有缓冲的,大小为1
#include <stdlib.h> |
对无缓冲的流的每次读写操做都会引发系统调用
1.4 feof的问题
CU上已经有无数的帖子在探讨feof了,这里从缓冲区的角度去考察一下.
对于一个空文件,为何要先读一下,才能用feof判断出该文件到告终尾了呢?
#include <stdlib.h> |
运行上面的程序,输入多于4个,少于13个字符,而且以连按两次ctrl+d为结束(不要按回车)
从上面的例子,能够看出,每当知足
(_IO_read_end < (_IO_buf_base-_IO_buf_end)) && (_IO_read_ptr ==_IO_read_end)
时,标准I/O则认为已经到达文件末尾,feof(stdin)才会被设置
其中_IO_buf_base-_IO_buf_end是缓冲区的长度
也就是说,标准I/O是经过它的缓冲区来判断流是否要结束了的.
这就解释了为何即便是一个空文件,标准I/O也须要读一次,才能使用feof判断释放为空
1.5. 其余说明
不少新手有一个误解,就是fgets, fputs表明行缓冲,fread, fwrite表明全缓冲 fgetc, fputc表明无缓冲
等等.
其实不是这样的,是什么样的缓冲跟使用那个函数没有关系,
而跟你读写什么类型的文件有关系.上面的例子中屡次在全缓冲中使用fgets, fputs,而在行缓冲中使用fread, fwrite下面的是引至APUE的实际上ISO C要求:1.当且仅当标准输入和标准输出并不涉及交互式设备时,他们才是全缓冲的2.标准输出决不是全缓冲的.不少系统默认使用下列类型的标准:1.标准输出是不带缓冲的.2.如如果涉及终端设备的其余流,则他们是行缓冲的;不然是全缓冲的.