做为程序员大家天天都在不停的写各种各种程序,想着如何如何改变世界(曾经我是这么想的,但是现在我还是这么想的)。但我在工作中发现好多同事对计算机本身这个东西并不了解,尤其是cpu、内存、网络、磁盘等等。不知道他们是如何运转的。这也不能怪我们,现在的高级语言对他们封装的太好了,好到我们都不知道他们是否还存在了。但是我编写的程序需要运行在她们上面,一不小心我们就会被她们捉弄了(相信大家都有这样的体会,网络怎么慢了、磁盘IO怎么慢了、线程怎么被夯住了等等),解决这些问题真的很辣手啊。在开发出高性能高可靠的程序的时候你要非常了解你的宿主服务器,我们在日常工作中都是在不停的压榨我们的宿主服务器(把宝宝都压榨的骨瘦如柴,我都看不下去了)。我分享一下我自己对计算机的一些认识。
一、从cup到内存的距离
CPU是机器的心脏,你的所有的程序最终都是由它来执行和运算的。主内存(RAM)是你的数据(包括代码行)存放的地方。本文将忽略硬件驱动和网络之类的东西,我们的程序的目标是尽可能多的在内存中运行。CPU和主内存之间有好几层缓存,因为即使直接访问主内存也是非常慢的。如果你正在多次对一块数据做相同的运算,那么在执行运算的时候把它加载到离CPU很近的地方就有意义了。
cpu-memory越靠近CPU的缓存越快也越小。所以L1缓存很小但很快(译注:L1表示一级缓存),并且紧靠着在使用它的CPU内核。L2大一些,也慢一些,并且仍然只能被一个单独的 CPU 核使用。L3在现代多核机器中更普遍,仍然更大,更慢,并且被单个插槽上的所有 CPU 核共享。最后,你拥有一块主存,由全部插槽上的所有 CPU 核共享。
cpu到各个缓存的时间:
cpu到各个缓存的时间二、cup如何调戏内存
当CPU执行运算的时候,它先去L1查找所需的数据,再去L2,然后是L3,最后如果这些缓存中都没有,所需的数据就要去主内存拿。走得越远,运算耗费的时间就越长。所以如果你在做一些很频繁的事,你要确保数据在L1缓存中。当这份数据运行完整后CPU会把数据回写到主内存中去。
数据在缓存中不是以独立的项来存储的,如不是一个单独的变量,也不是一个单独的指针。缓存是由缓存行组成的,通常是64字节(译注:这篇文章发表时常用处理器的缓存行是64字节的,比较旧的处理器缓存行是32字节),并且它有效地引用主内存中的一块地址。一个Java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量。非常奇妙的是如果你访问一个long数组,当数组中的一个值被加载到缓存中,它会额外加载另外7个。
问题:如果同一份数据同时被两个cup加载并且都缓存到各自的L1缓存中时,如何保证这两份数据的一致性?
cpu有个缓存一致性协议,这类协议有MSI、MESI、MOSI及Dragon Protocol,基本的思想是cpu通过各种机制保证:当一份在多cpu共享的数据,其中一个cpu有修改时并写回主内存后,别的cpu缓存会无效,使用时会强制从主内存加载。
缓存一致性协议三、mmap技术
当我们在操作一个文件时,linux是这样的:常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。而使用mmap操作文件中,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作。而之后访问数据时发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。
mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。
如果大家有平凡的对文件的读写操作建议使用mmap,现在各个高级语言已经支持如java的MappedByteBuffer等。现在好多中间件的文件操作都是使用mmap技术。
四、Zero-Copy技术
许多web应用都会向用户提供大量的静态内容,这意味着有很多data从硬盘读出之后,会原封不动的通过socket传输给用户。这种操作看起来可能不会怎么消耗CPU,但是实际上它是低效的:kernal把数据从disk读出来,然后把它传输给user级的application,然后application再次把同样的内容再传回给处于kernal级的socket。这种场景下,application实际上只是作为一种低效的中间介质,用来把disk file的data传给socket。
data每次穿过user-kernel boundary,都会被copy,这会消耗cpu,并且占用RAM的带宽。幸运的是,你可以用一种叫做Zero-Copy的技术来去掉这些无谓的copy。应用程序用zero copy来请求kernel直接把disk的data传输给socket,而不是通过应用程序传输。Zero copy大大提高了应用程序的性能,并且减少了kernel和user模式的上下文切换。现在各个高级语言已经完美支持Zero-Copy技术,javaapi的java.nio.channel.FileChannel的transferTo()方法。
网友评论