那些“cache”和“buffer”(一)

来源:互联网 发布:c语言n!=0是什么意思 编辑:程序博客网 时间:2024/06/07 00:07

江湖人称:cache is king

还有个与之类似的是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的存在。由于这部分复杂度极高,后面有机会再分析吧。这篇文章就这样吧。

所以说:cache is king!


原始出处


原创粉丝点击