还有个与之类似的是buffer。这里就谈谈buffer和cache。
那么他们到底是用来干什么的呢?其实他们就是在两个相对独立的系统之间的一个中间层,用来避免这两个系统之间不必要的交互和不不必要的或者重复的同步。同步,你懂的,不同数量级系统之间的同步,你也懂的。比如内存和磁盘之间,比如应用和数据库之间。buffer针对写,cache针对读。
这篇文章先来看看Linux里面系统的那些cache和buffer(至于硬件里面的一些cache如cpu指令缓存这里就不谈了)。
首先是文件IO
对于写操作通常我们会遇到两个两个缓冲 (buffer):
一个是内核缓冲。
当我们调用write写文件时,write返回之后其实内容并没有立刻写到硬盘上,而是写到了内核的缓存中。什么时候写到磁盘?内核有一套刷缓存的机制。这样做有很明显的好处,比如我们调用1次write写1kb和调用1k次write每次写1b的数据,所花的时间是差不多的。后者所花的用户态/内核态切换时间多些,但是写磁盘的次数却是一样的。这样就大大提高了效率。
另外一个是glibc维护的用户态缓冲。
这个缓冲又是用来干什么的呢?内核和硬盘是两个相对独立的系统,内核缓冲在这两个之间避免了很多不必要的同步。那么同样,内核和用户程序也是两个相对独立的系统,每次系统调用也是要花代价的。所以上面1次write写1kb和调用1k次write每次写1b的数据的例子,前后两种方法还是有差距的,差距就在于后者需要做1k此用户态和内核态的切换。所以,glibc在用户态上又做了一个缓冲。当我们调用glibc提供的printf输出的时候,并没有直接映射到一次write系统调用,而是存在了glibc管理的缓冲中,当条件满足时(下面会说上面时候满足)再调用一次write,把用户态的缓冲写到内核态去。所以,调用1此printf到文件1kb字符和1k此print每次1个字符,所花的时间就真差不多了。
反过来,对于读,我们有缓存(cache)
用read读文件,其实就是读的内核的缓存。当内核缓存中没有的时候,会从磁盘读些内容到缓存,然后从缓存返回给用户。 注意,在某些情况下我们还可以做进一步的优化。考虑一下,我们从头到尾读文件,只读1次1kb和读1k次每次1b的时间是不是也差不多呢?这就需要内核在 read的时候事先读1kb的内容到缓存才行(这叫:read ahead)。而内核有不知道我们后续的read调用是从头到尾顺序读还是胡乱读,如果是随机读那么read ahead就会适得其反。还好posix规定了posix_fadvise()系统调用,让我们告诉内核,我们读文件的方式。用这个调用告诉内核我们是顺 序读。read的性能就上来了。爽啊。
下面列举一下glibc默认的缓冲的行为(可以通过setvbuf修改):
如果文件是stderr,则默认是没缓冲,每次printf对应一次write。
如同文件是终端,则默认是行缓冲,每当遇到换行的时候write一次
如果文件对应的是磁盘文件,则默认是全缓冲,当缓存区满或者文件关闭时会write。
当然,任何时候,你都可以调用fflush()来强制用户态的缓存写到内核缓冲中。
好,聊完了文件缓冲,下面聊聊socket的缓冲。
对!socket也是有缓冲的。同样的例子我们应用到socket上:1次send 1kb和1k次send每次1b。哪个费时费力呢?显然是后者,因为如果我们知道,IP包头,TCP包头都是要花带宽的。如果一个大大的IP包中只有1字节的数据,显然是大大的浪费了带宽。所以,John Nagle提出了Nagle算法,将这些凌乱的小包缓冲起来,集齐N个组成一个大包,再发出去。这样就大大提高了网络效率。
当然,这样做也是有负面作用的,当我们应用对网络的实时性要求比较高的时候,可能会因为这个机制而增加了网络延迟(毕竟要等集齐了才发嘛)。这时还是用setsockopt+TCP_NODELAY参数把这个优化禁用了吧。
还有吗?当然,cache和buffer无处不在。内存管理中到处有cache。就那用户空间来说,glibc中的ptmalloc2,google的TCMalloc都有cache的存在。由于这部分复杂度极高,后面有机会再分析吧。这篇文章就这样吧。
谈了一些Linux系统层面的cache和buffer。这里主要谈谈应用层面的那些cache。相比系统层面的cache集中在IO上,应用层面的cache就显得五花八门了。就从WEB说起吧。
web缓存对于服务器和客户端都是不可或缺的。
对于web服务器来说,缓存是非常重要的东西,它可以大大的增加并发,缩短相应时间,减少服务器负荷。原理很简单。因为对于一个URL来说,很短时间内它的内容变化其实是不大的,如果每次请求都要服务器算一遍就显得太浪费了。所以web缓存就是放在web服务器前面的一个代理,它接收用户请求,并向后端请求,在返回响应的时候将这些响应缓存起来,遇到请求时不经过服务器计算直接返回响应,从大大提高响应速度,尤其是在请求量很大的时候。web缓存代表作是squid和varnish。
对于客户端的浏览器来说缓存同样不可或缺。甚至在HTTP协议中都为缓存提供了支持,HTTP返回码304 Not Modified就是告诉浏览器,你要的内容没变,用你缓存中的吧。在HTTP协议之外,浏览器自己也会做许多的缓存,对于图片啊,js什么的,短时间内的请求就自作主张直接不去远程取了,会大大减少请求量,从而节省大量的时间,用户需要速度。况且浏览器和服务器本是同根生嘛,不必相互煎熬。所以有时候你得强制刷新。
与web服务器紧密相关的就是数据库了。
在数据库系统中,缓存同样无处不在。因为同样的道理,对同一个sql查询来说,在某些条件下(比如它查的表自上次查询后都没更新过)它的结果就应该和上次查询一样的。于是mysql提供了query cache。也有些框架提供了缓存功能,比如hibernate。这都是读缓存,目的在于读很多,而写比较少的时候提高读的性能,如果写很多,而读比较少的话这类缓存就没什么用了。于是,在一些情况下我们希望可以为数据库引入写缓存。典型的是主键查询和更新。于是出现了kv数据库(比如memcached)。可以提供基于主键查询的读写缓存。这对于提高数据系统的整体性能是极其重要的。
其他五花八门的cache还有那些呢?
比如dns缓存,dns缓存同样存在于客户端和dns服务器中,与web缓存的原理是一样的。将dns的解析结果缓存起来。
比如arp缓存,将arp的结果缓存起来。
甚至,连编译系统也引入了缓存比如ccache,比如visual studio中的pch(预编译头)机制。
最后,我想说的是:cache is king! cache is everywhere!