目录[-]
前言
有些细节,比如各种语言的语法,API,你看过的开源代码,如果你不经常用的话,那么很容易忘记。所以,本文忽略了一些即使看了,当时也理解了;但是由于工作关系,你不用还是会忘记的细节。本文记录了一些重要的,或者笔者感兴趣的细节,供参考。
基本原理
在计算机系统演化过程中,存在两个基本原理:
- 性价比原理:速度越块,价格越高,容量就越小。CPU缓存,内存,磁盘忠实地体现了这种关系。
- 局部性原理:该原理分成两部分:时间局部性和空间局部性。在具备良好时间局部性的程序中,被引用一次的存储器位置很可能在不远的将来会被再次引用。在具备良好空间局部性的程序中,被引用一次的存储器位置的附近位置很可能在不远的将来会被再次引用。大部分编写良好的程序都能符合这个原理。这个原理告诉我们,把一个程序经常需要的数据放到高速缓存里面,即使这个缓存很小,能够极大提高性能。
CPU
程序是由一系列指令组成的。当执行一个程序时,需要把磁盘的指令读入到内存中。然后再把内存中的数据读入到寄存器中,这样CPU就能够执行该程序了。之所以需要经过磁盘,内存和寄存器这些IO操作,是因为上文说的“性价比原理”。大量程序无法全部存放在小容量的存储器里面,但是为了充分利用CPU的性能,不让CPU发生太大延迟,才会把数据放到存取数据速度最快的介质里面。
下图是常见的计算时延周期:
流水线执行
通常,处理一个指令内部包含如下几个操作,分别是:
- 取指:从存储器读取指令字节
- 译码:从寄存器文件读入操作数
- 计算:算术/逻辑单元执行指令代表的操作。
- 访存:写入或读取存储器数据
- 写回:把结果写入到寄存器文件
- 更新pc :将PC设置成下一个指令的地址
所谓流水线执行,就是把这6个操作放在流水线上执行。那么所谓流水线,可以看看这篇经典的5级流水线介绍。
分支预测
处理器会使用精密的分支预测逻辑,来试图猜测每条跳转指令如何执行。如果预测成功,那么指令流水线就会充满着指令;否则如果预测失败,则要求处理器丢掉它为该跳转指令后所有指令所做的工作,然后再开始从正确位置处起始的指令开始执行。
在stackoverflow有个经典的问题:为什么处理有序数组快于无序数组?,就说明这种情况。
乱序执行
当CPU在执行指令时,如果发现所需要的数据不在Cache时,则需要从外部存储器中取,而这个过程通常需要几十,甚至几百个Cycle。如果CPU是顺序执行这些指令时,则后面的指令需要等待。所以如果CPU支持乱序执行的话,那么就可以先执行后面不依赖该数据的指令,进而提升CPU计算性能。
存储器结构
内存访问
数据流通过总线在处理器和内存传输。每次CPU和内存传输数据需要经过一系列步骤,我们称这些步骤为总线事务。读事务是指内存传送数据给CPU,写事务是指CPU传送数据给内存。
总线是一组并行的导线,能够携带地址、数据和控制信号。下图是一个计算机的示例配置,主要部件包括CPU,I/O桥(内含存储控制器),内存。这些组件由系统总线和存储器总线连接起来。I/O桥将系统总线的电子信号翻译成存储器的电子信号。
读事务:当执行 mov A,%eax ,表示将地址A的内容加载到寄存器%eax中。
- CPU将地址A放到系统总线上
- I/O桥将信号传递给存储器总线
- 内存感知到存储器上的地址信号,从存储器总线读地址,从内存中取出数据字,并将数据写到存储器总线
- I/O桥将存储器信号翻译成系统总线信号,然后通过系统总线传递
- CPU感知到系统总线上的数据,从总线上读数据,并将数据复制到寄存器%eax
写事务:基本过程原理类似于读事务,略。
磁盘访问
旋转磁盘物理结构
磁盘通常多个盘片组成,每个盘片有2盘面组成,每个盘面有多个磁道组成,每个磁道有多个扇区组成。扇区之间用一些间隙隔开。这些间隙不存储数据,主要用来识别扇区的格式化位。盘片中间有个可旋转的主轴,它使得盘片可以以大约5400-15000转每分钟的速度进行转动。
磁盘容量计算公式举例如下 :`512字节*300扇区*20000磁道*2盘面*5盘片(忽略了分母单位)
`
磁盘以扇区大小的块来读写数据。对扇区的访问时间有3部分组成:寻道(找到磁道)时间+旋转(旋转盘面)时间+传送时间。前2个时间耗时较大。访问第一个字节耗费很长时间,后面几乎不耗费时间。
逻辑磁盘块
现代磁盘构造复杂,为了对OS隐藏这些复杂性,现代磁盘提供了一个简单的视图,称为逻辑块。磁盘控制器(在I/O桥内部)维护着逻辑块和实际物理的磁盘扇区之间的映射关系。
OS执行I/O操作时,通过指定逻辑块号,磁盘控制器的固件执行一个快速查找,将逻辑块号翻译成(盘面,磁道,扇区的)3元组,这个3元组唯一标识了对应的物理扇区。磁盘控制器的硬件解释这个3元组,将读、写头移动到适当的盘面,等待扇区移动到读写头下,将读写头感知的位放到控制器里面的一个小缓冲区内,然后复制到内存中。
读取磁盘数据
在使用"存储器映射I/O(memory-mapped I/O)“技术的系统中,地址空间中一块地址是为与I/O设备通信保留的。每个这样的地址称为一个I/O端口。当一个I/O设备连接到总线时,它与若干个端口相关联。
以读取磁盘数据为例,假设磁盘控制器被映射到端口0xa0。随后,CPU可能通过执行3个对地址0xa0的存储指令,发起磁盘读:第一条指令发送一个命令字,告诉磁盘发起一个读操作,同时还发送了其他的参数,比如当读完时,是否中断CPU。第二条指令指明应该读的逻辑块号。第三条指令指明磁盘扇区内容的内存地址。
当磁盘控制器收到CPU读命令后,它将逻辑块翻译成扇区地址 ,然后将内容直接传送到主存,不需要CPU的干涉。这种过程称为DMA(Direct Memory Access,直接内存访问)。这样做的好处就是充分利用CPU,让CPU去干其他事情,不用等待。
当DMA完成数据传送后,扇区的内容被复制到内存中。此时磁盘控制器通过给CPU发送一个中断信号,使CPU暂停它当前正在做的工作,回到原先被中断的操作系统例程上面。
内核即把数据从内核的缓冲区复制到用户进程的缓冲区,至此完成read()系统调用。
固态硬盘
固态硬盘(SSD,Solid State Disk)是一种基于闪存的存储技术。一个SSD由若干个闪存芯片和闪存翻译层(flash translation layer)组成。闪存芯片替代旋转磁盘中的机械驱动器,而闪存翻译器是一个硬件设备,扮演与磁盘控制器相同的角色,将对逻辑块的请求翻译成堆底层物理设备的访问。
SSD顺序读和顺序写,随机读都比较快。但是随机写比较慢,比前者慢一个数量级。随机写很慢有两个原因,首先,擦除块需要相对较长的时间,1ms级的。其次,如果写操作视图修改一个包含已经有数据(也就是不全为1)的页,那么这个块中所有带有用数据的页都必须被拷贝到一个新被擦除过的块,然后才能进行对该页的写。
SSD的优点是速度快,能耗低。缺点是价格贵和使用寿命较低,易磨损。
小结
经过上述分析,我们在日常IO编程中,应尽量考虑顺序读写,而不是随机读写。可以考虑对重复度比较高的数据进行压缩。根据应用选择数据批量写。适当的可以在读写成瓶颈的地方使用SSD。
虚拟存储器
更多的进程运行需要更多地物理内存,如果缺乏内存的话,那么该程序会无法运行。另外,某个进程可能误写了另一个进程的内存。为了更加高效方便地管理存储器,现在操作系统提供了一种对内存的抽象概念,叫做虚拟存储器。
虚拟存储器是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
注意:虚拟存储器不只是“用磁盘空间来扩展物理内存”的意思。把内存扩展到磁盘只是使用虚拟存储器技术的一个结果,它的作用也可以通过覆盖或者把处于不活动状态的程序以及它们的数据全部交换到磁盘上等方式来实现。对虚拟存储器的定义是基于对地址空间的重定义的,即把地址空间定义为“连续的虚拟存储器地址”,以借此“欺骗”程序,使它们以为自己正在使用一大块的“连续”地址。
虚拟存储器提供了3种能力:
- 它将内存看成一个存储在磁盘上的地址空间的高速缓存,内存只保留活动区域,并根据需要在磁盘和内存中来回传送数据。通过这种方式,它高效地使用了内存。
- 它为每个进程提供了一致的地址空间,从而简化了存储器管理。
- 它保护了每个进程的地址空间不被其他进程破坏。
内存寻址和地址空间
计算机系统将内存看成一个由M个连续字节组成的大数组,每个字节都有一个物理地址。这个数组组成一个物理地址空间。
物理寻址就是按照最自然的方式,根据物理地址这个索引去大数组中捞取数据。
在现代操作系统中,会使用虚拟寻址这种方式,CPU会使用虚拟地址来访问内存。在虚拟地址被送到存储器之前先转换成适当的物理地址。CPU芯片中有个MMU(Memory Management Unit,存储器管理单元)来专门完成地址翻译。
虚拟存储器作用
缓存磁盘上的内容
就概念上而言,虚拟存储器被视作一个数组,一个存放在磁盘上的N个连续的字节大小的单元组成的数组。每个节都有一个唯一的虚拟地址。这个虚拟地址被用来当做数组的索引。数组上的内容可以被缓存在内存中。
VM将虚拟存储器的多个字节组成一个块,我们称之为虚拟页。类似的,物理存储器也被分成若干个物理页。
虚拟页面的集合分为3个不相交的子集:
- 未分配:不占用内存和磁盘空间
- 未缓存:不占用内存,占用磁盘
- 已缓存:占用内存和磁盘
虚拟页管理
虚拟存储器系统需要一种方法来判定一个虚拟页是否放在内存中。如果命中内存,还必须明确这个虚拟页存放在哪个物理页中。如果未命中内存,系统需要判定这个虚拟页存放在磁盘的哪个位置,然后接着在物理存储器中选择一个牺牲页,会把被牺牲的脏页写回磁盘,并且完成交换页面。
页表:存放在内存中的数据结构,存放了虚拟页到物理页的映射关系
操作系统:负责维护页表的内容,以及在磁盘和内存中传送页。
MMU:地址翻译硬件,负责在将虚拟存储器转换成物理内存时读取页表。
详细的替换算法略。
其他
多个虚拟页面可以映射到同一个物理页面,这样可以多个进程共享物理内存
TLB(Translation Lookaside Buffer,翻译后备缓冲器)是MMU硬件内部的一个小缓存,用来加速虚拟地址的到物理地址的翻译。
操作系统一般会预读额外的文件系统页以提升性能。
文件系统页可能在相当长的时间内继续有效,这样该文件在后续被进程打开时,可能根本无需访问磁盘。
TCP编程
TCP状态迁移
先来看下这张图,3次握手和4次挥手。在图中,还可以看到每次系统调用后的状态变化。
需要值得一提的是TIME_WAIT状态。在上图中我们看到执行主动关闭的那段经历了这个状态。该端点停留在这个状态的时间是MSL(Maximum segment lifetime,最长分节生命期)的2倍,有时候称之为2MSL。它允许在最大跳数内的并且小于2MSL时间内重传数据包。
TCP SOCKET API
在熟悉了TCP的3次握手和4次挥手,我们再来看看基本的socket API。
listen 函数完成两件事:
- 把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。调用listen导致套接字从CLOSED状态转换成LISTEN状态
- 该函数的backlog参数规定了内核应该为相应套接字排队的最大连接个数。内核维护两个队列,分别是
- 未完成连接队列:处于SYN_RCVD状态等待完成三次握手过程的套接字
- 已完成连接队列:处于ESTABLISHED状态的并且完成三次握手过程的套接字
另外,还需要说明的是,backlog定义有歧义,取决于系统的具体实现。也就是说,backlog的值可能是未完成连接队列大小与已完成连接队列之和;也有可能是其他值。
这里稍微说下SYN flood 攻击。是指攻击者把源IP改成一个随机数,向受害主机只发送SYN;这样受害主机据不知道把ACK/SYN发往哪里,导致合法的客户服务被拒绝。
I/O复用
本节不再去重复几种 blocking IO,nonblocking IO,IO multiplexing,signal driven IO
和asynchronous IO,详细的可以阅读UNIX网络编程卷一第6章节。这里主要提下I/O复用。
I/O复用是指内核具备一旦发现进程指定的一个或多个I/O条件就绪后它就通知进程进行相应处理的能力。I/O条件就绪是指输入数据已经准备好被读取或者描述符已经能够承接更多地输出。在具体实现时,首先构造一张有关描述符的列表,然后调用一个函数,直到这些描述符中的一个已经准备I/O时,该函数才返回。在返回时,它告诉进程哪些描述符已经准备好可以进行I/O。
I/O复用使用两个系统调用:select和recvfrom。所以,I/O复用的优势在于等待多个描述符就绪,可以提供更高的吞吐量。select系统调用可以告知内核对哪些描述符(可读,可写,异常)感兴趣以及等待多长时间(无限等待,等待某段时间,不等待)。
select vs Epoll
select存在如下几个缺点:
- 最大并发数限制,因为一个进程所打开的 FD (文件描述符)是有限制的,由 FD_SETSIZE 设置,默认值是 1024或者2048,因此 Select 模型的最大并发数就被相应限制了。需要通过重新编译内核才能修改 FD_SETSIZE。
- 效率低下:每次调用都会线性扫描全部的 FD 集合,这样效率就会呈现线性下降。
- 内存拷贝问题:内核需要采取内存拷贝,把 FD 消息通知给用户空间呢。
Epoll 存在如下几个优点:
- Epoll没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于2048, 一般来说这个数目和系统内存关系很大 ,具体数目可以 cat /proc/sys/fs/file-max 察看。
- 效率提升:Epoll 最大的优点就在于它只管你“活跃”的连接 ,而跟连接总数无关,因此在实际的网络环境中, Epoll 的效率就会远远高于 select 和 poll 。
- 没有内存拷贝问题:Epoll 在这点上使用了mmap共享内存技术,避免了内存拷贝。
源于 :http://my.oschina.net/geecoodeer/blog/202528