Part1:由printf()函数引发对于I/O缓冲区的分析
首先要明确一个问题:printf()是一个函数,而缓冲区是对设备而言的。所以说不存在printf()函数的缓冲区这种说法。
下面的例子说明了printf()在输出时遇到的问题:
while(1)
{
printf(“*”);
usleep(10000); //unistd.h
}
这个例子是为了不断在屏幕上输出*,但是并不能按理想状态输出,我们已知原因就在于它把要输出的内容放到缓冲区里了。要得到正确的输出有下面四种措施可以实现:1)强制刷新缓冲区fflush()2)输出里有’\n’3)缓冲区已满4)从缓冲区里取数据的时候。
然后我们需要明白什么是缓冲区(buffer)。从wikipedia的定义可知,在计算机科学里,缓冲区是用于在转移数据时,暂存数据的一块物理存储区域。缓冲区可以有硬件实现(一块固定的物理块)和软件实现(指向物理存储的虚拟数据缓冲区)。一般是用于接收方和发送方速率不一致(或可变)的时候。一般缓冲区为了满足先进先出,使用的数据结构是队列。
前面我们已经提到,缓冲区是对设备而言的概念,在外设和CPU,内存之间必然也存在着很大的读写速度差异,所以I/O设备是拥有缓冲区的。I/O设备包括三个标准输入流stdin,标准输出流stdout和标准出错流stderr。printf()作为一个输出函数,与它相关的设备自然就是stdout。一般的,stdin和stdout为行缓冲,而stderr为无缓冲。实际上在ISO C里规定:当且仅当标准输入输出不涉及交互设备时才是全缓冲,这就意味着,如果输入到文件里,遇到’\n’是不会刷新缓冲区的。
通过man stdout可以看到下面的备注信息:
现在我们整理一下思路,我们的printf()函数在输出时是将数据写到标准输出设备stdout上,而stdout是行设备,也即行缓冲区为满时才将内容发送出去然后刷新缓冲区以获得新的数据。这样一来上面说的四种措施中的两个就很容易理解了,1)强制刷新缓冲区。通过fflush(stdout)函数来强制刷新缓冲区,这是行的通的方法。3)缓冲区已满,这个通过上面的分析很容易理解,这是缓冲区刷新的一般方式。
在ascii里,'\n'为0x0a,它的意思是换行并回车,而'\r'0x0d,它的意思是回车。当stdout里有'\n'意味着要换行输出,这时就会送出缓冲区里的数据并刷新。这样措施2)也是正确的。
下面的例子证明了’\r’和‘\n’的区别:
至此,上述几个措施基本得到证明(说明:4)源自网上一个资料,源网页找不到了,该观点也暂未得到验证。在ISO C99标准里有这样的表述:Furthermore,characters are intended to be transmitted as a block to the host environmentwhen a buffer is filled, when input is requested on an unbuffered stream, orwhen input is requested on a line buffered stream that requires thetransmission of characters from the host environment.)。
这里要注意的是:
1)fflush(NULL)--flusheverything往往是一个坏主意,尤其是在多文件操作或者多线程环境里你需要对锁进行操作时。
2)在整个进程结束的时候也是要清空缓冲区的。具体讨论见Part2。
3)我们可以通过setvbuf(stdout,(char *)NULL,_IONBF,0)可以用来设置刷新缓冲区。或者是setbuf(stdout,NULL)使缓冲区全部结束。
4)在windows环境下,printf是从默认堆里获得512个字节(16位机)或4096个字节(32位机)。这里没有进行测试和深入讨论。在Part3将讨论堆和栈的区别以及分配空间malloc和new的区别。
在这里提出两个问题:a.如何测试上面的观点?b.输出的时候如果不是刚好的512个字节时怎么办?相间输出%d和%c,每组占5个字节?
Part2:补充:
a.进程结束时缓冲区发生了什么?
从名字就能看出,exit()这个系统调用是用来终止一个进程的,无论进程执行在什么位置,只要遇到exit(),进程就会停止剩下的剩余操作。而_exit()跟exit()基本上是一样的,exit定义在stdlib.h中,而_exit()定义在unistd.h里。_exit()的作用最为简单,直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构,而exit()是在其上作了一点包装。它们最大的区别是,exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区的内容写回文件,就是清理I/O缓冲。那如果我们的数据还放在缓冲区里,调用_exit()函数会使数据丢失,而使用exit()函数是关闭I/O文件之前要刷新缓冲区的。在我们之前的例子里,用的是return 0。main()函数体内,return 的效果和 exit()是一样的。
下面的例子简单证明了exit()与_exit()的区别:
可以看出,输出是正常的:
可以看出输出结果有了问题。
b.关于带缓存的I/O和不带缓存的I/O
linux对IO文件的两种操作划分。不带缓存,其实是在用户层没有缓存,不是直接对磁盘文件进行读取操作如read()和write(),它们都是系统调用。对于内核来说,要进行IO时,内核先将数据写入到内核中所设的缓冲存储器,缓冲存储器满才写到磁盘上。而带缓存的IO也叫标准IO,是ASCI C标准的IO处理,不依赖与内核,移植性强,目的就是减少read()和write()的调用次数。使用标准IO可以减少系统调用如read()和write()的调用次数。其实质是在用户层建立一个缓存区(用户缓冲区也叫流缓冲区)。它对每个IO流自动进行缓存管理(标准IO函数一般使用malloc来进行缓存分配),它提供了三种类型的缓存-全缓存(例如磁盘上的文件),行缓存(输入输出遇到新行符或者缓存满时,stdin和stdout通常是行缓存),无缓存(相当于read,write,例如stderr)。
一般的,由OS选择缓存长度并自动分配,标准I/O库在关闭流时候自动释放缓存。
标准I/O库可能效率不高,原因是需要复制的数据要在用户缓存和内核缓存之间复制,然后又从内核缓存复制到I/O缓存。例如调用fgetc和fputs时,数据要从内核和标准I/O缓存之间(调用read和write)复制,第二次是在标准I/O缓存(通常系统分配和管理),和用户程序中的行缓存(fgetc的参数就需要一个用户行缓存指针)之间。
【无缓冲的stderr其实也不是完全没缓冲,只是缓冲区的大小不为0,而为1】
Part3:堆栈区别及malloc与new的区别
a.堆栈区别
1)程序的内存分配
一个c/c++编译的程序占用的内存分为五个部分,栈区(stack 由编译器自动分配释放,存放函数的参数,局部变量的值),堆区(heap 一般由程序员分配释放,若程序员没有释放,那么OS可能会回收。它不同与数据结构里的堆,分配方式类似于链表),全局区(静态区,static 全局变量和静态变量的存储是在一起的,未初始化的放在相邻的另一块区域,程序结束后由OS释放),文字常量区(常量字符串,结束后由系统释放),程序代码区(存放函数体的二进制代码):
stack
heap
.bss 例如部分全局变量,在执行前内核将其初始化为0或者null
.text代码段是可共享的,但是是只读的。
.data需要明确赋初值的变量。
后两段出现于程序的可执行文件中,内核调用exec启动该程序时读入。
2)申请
栈空间是由OS自动分配的,如果剩余空间不足分配,则报异常。是向低地址扩展的数据结构,是一块连续的内存区域。栈顶地址和最大容量是系统预先规定好的。在函数调用时,先入栈的是函数的下一条指令的地址,然后是函数的各个参数,在大多数编译器中,参数是自右向左入栈的,然后是函数中的局部变量,静态变量不入栈。出栈顺序刚好相反。
堆空间是由程序员分配的,在c语言中的malloc函数。从记录空闲内存地址的链表遍历,寻找第一个大于所申请空间的堆结点,然后把该结点从链表里删除并分配给程序。对于大部分OS,会在这块内存的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。由于得到的堆空间并不一定刚好等于申请的大小,系统会自动地将多余的部分放入空闲链表。是向高地址扩展的数据结构,是不连续的内存区域—因为是用链表来管理空闲内存地址的,大小受限于系统中有效的虚拟内存。一般在堆的头部用一个字节存放堆的大小。
3)区别
管理方式,空间大小,是否产生碎片,增长方向,分配方式(动态栈的分配由编译器实现,alloca(),并不需要手动实现),分配效率。
b.malloc 和 new的区别相应的free与delete的区别
1)归属不同。Malloc/free 是C/C++标准库函数,而new/delete是C++的运算符。Malloc/free需要库文件支持,new/delete则不要。
2)用法不同。
malloc的函数原型为:void *malloc(size_t size),所有在使用时要强制类型转换为所需的类型,它只关心内存长度。
free的函数原型为:void free(void *ptr),指针的类型和它所指向的内存的容量事先都是知道的。如果ptr是NULL指针,那么对p进行多少次free都不会出现问题,如果ptr不是NULL指针,那么free对p连续操作两次就会导致运行错误。
在new里面内置了sizeof、类型转换和类型安全检查功能:
new是自动计算需要分配的空间,而malloc需要手动计算。
new是类型安全的,而malloc不是。New operator相当于operator new和construct。
Operator new对应于malloc,但是operator new可以重载,可以自定义内存分配策略,甚至不做内存分配,甚至分配到非内存设备上,而malloc无能为力。
new将调用constructor,而malloc不能,delete将调用destructor,而free不能。
【注】:如果用new创建对象数组,那么只使用对象的无参数构造函数。
例如
obj*object = new obj[100];
而不能写成
obj*object = new obj[100]();
在用delete释放对象数组时,不要丢了符号[],例如
delete[]object;//correct
deleteobject;//wrong
后者相当于delete object[0]
*这部分内容也没有得到测试。
//开放思考。该文档只代表个人的一些想法,难免有疏漏和各种错误,希望阅读的同学可以跟我积极讨论交流。文档只是一个知识的整理,不要拘泥于此,反而应该由此及彼,扩展出去,开放思考。
参考资料:
http://blog.youkuaiyun.com/yasin_lee/article/details/5700352:
http://en.wikipedia.org/wiki/Data_buffer
http://pic.dhe.ibm.com/infocenter/tpfhelp/current/index.jsp?topic=%2Fcom.ibm.ztpf-ztpfdf.doc_put.cur%2Fgtpc2%2Fcpp_fprintf-printf-sprintf.html:
http://stackoverflow.com/questions/1716296/why-does-printf-not-flush-after-the-call-unless-a-newline-is-in-the-format-strin
http://support.microsoft.com/kb/44725/zh-cn#appliestohttp://www.360doc.com/content/11/0521/11/5455634_118306098.shtml
http://www.360doc.com/content/12/0504/15/9400799_208611794.shtml
http://blog.youkuaiyun.com/water_cow/article/details/7538689
http://hi.baidu.com/yangyingchao/item/0004bb01adc9ebd01ef04655