Nginx由于极高的性能受到大家的追捧,而Nginx的高性能与它优秀的内存管理方式是分不开的,今天就来聊一聊Nginx中的内存对齐和内存分页。

先说下Nginx中的内存对齐,Nginx中的内存对齐机制是它高性能的关键因素之一,先说点基础的东西,什么是内存对齐呢? 内存对齐是操作系统为了快速访问内存而采取的一种策略。那么为什么要内存对齐呢?因为处理器读写数据,并不是以字节为单位,而是以块(2,4,8,16字节)为单位进行的,而且由于操作系统的原因,块的起始地址必须整除块大小。如果不进行对齐,那么本来只需要一次进行的访问,可能需要好几次才能完成,并且还要进行额外的数据分离和合并,导致效率低下。更严重地,有的CPU因为不允许访问unaligned address,就报错,或者打开调试器或者dump core,比如sun sparc solaris绝对不会容忍你访问unaligned address,都会以一个core结束你的程序的执行。所以一般编译器都会在编译时做相应的优化以保证程序运行时所有数据地址都是在'aligned address'上的,这就是内存对齐的由来。

      为了更好理解上面的意思,这里给出一个示例。在32位系统中,假如一个int变量在内存中的地址是0x00ff42c3,因为int是占用4个字节,所以它的尾地址应该是0x00ff42c6,这个时候CPU为了读取这个int变量的值,就需要先后读取两个4字节的块,分别是0x00ff42c0~0x00ff42c3和0x00ff42c4~0x00ff42c7,然后通过移位等一系列的操作来得到,在这个计算的过程中还有可能引起一些总线数据错误的。但是如果编译器对变量地址进行了对齐,比如放在0x00ff42c0,CPU就只需要一次就可以读取到,这样的话就加快读取效率。

   综合,内存对齐的原因有2点:
      (1) 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

      (2) 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要至少要两次内存访问;而对齐的内存访问仅需要一次访问

再说一下linux下如何进行内存对齐的:

内存对齐可以用一句话来概括:"数据项只能存储在地址是数据项大小的整数倍的内存位置上"。

每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16 来改变这一系数,其中的n 就是你要指定的“对齐系数”。

规则1:

数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0 的地方,以后每个数据成员的对齐按照#pragma pack 指定的数值和这个数据成员自身长度中,比较小的那个进行。

规则2:

结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。

规则3:

结合1、2 颗推断:当#pragma pack 的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。

Nginx是怎么做内存对齐的呢?:

//Nginx中的内存对齐
#ifndef NGX_ALIGNMENT
#define NGX_ALIGNMENT   sizeof(unsigned long)    /* platform word *///4byte
#endif
//把d对齐到a的整数倍
// 两个参数d和a,d代表未对齐内存地址,a代表对齐单位,必须为2的幂。假设a是8,那么用二进制表示就是1000,a-1就是0111.
// d + a-1之后在第四位可能进位一次(如果d的前三位不为000,则会进位。反之,则不会),
// ~(a-1)是1111...1000,&之后的结过就自然在4位上对齐了。注意二进制中第四位的单位是8,也就是以8为单位对齐。
// 例如,d=17,a=8,则结果为24.所以该表达式的结果就是对齐了的内存地址
#define ngx_align(d, a)     (((d) + (a - 1)) & ~(a - 1))
//把指针p的地址对齐到a的整数倍
#define ngx_align_ptr(p, a)                                                   \
    (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))

说完内存对齐,再来说下Nginx中的内存分页机制,nginx中的内存分页实现很简单,在ngx_create_pool(size_t size, ngx_log_t *log)中

//限定内存池的大小不超过NGX_MAX_ALLOC_FROM_POOL
    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
//NGX_MAX_ALLOC_FROM_POOL是一个内存池分配的最大容量,值为ngx_pagesize - 1,ngx_pagesize是一块内存页的大小,在x86下通常为4096
#define NGX_MAX_ALLOC_FROM_POOL  (ngx_pagesize - 1)



这么做有什么好处呢?

在存储器管理中,连续分配方式会形成许多“碎片”,虽然可通过“紧凑”方法将许多碎片拼接成可用的大块空间,但须为之付出很大开销。如果允许将一个进程直接分散地装入到许多不相邻的分区中,则无须再进行“紧凑”。基于这一思想而产生了离散分配方式。如果离散分配的基本单位是页,则称为分页存储管理方式。

分页管理器把地址空间划分成4K大小的页面(非Intel X86体系与之不同),当进程访问某个页面时,操作系统首先在Cache中查找页面,如果该页面不在内存中,则产生一个缺页中断(Page Fault),进程就会被阻塞,直至要访问的页面从外存调入内存中。


综上,内存分页管理使得进程的地址空间可以为整个物理内存地址空间(如4G),一个页面经过映射后在实际物理空间中是连续存储的。根据程序局部性原理,如果程序的指令在一段时间内访问的内存都在同一页面内,则会提高cache命中率,也就提高了访存的效率。

总结一下,内存分页管理使得程序向系统申请一个页面的内存时,该页内存地址在物理内存地址空间中是连续分布的,这提高了cache命中率。如果申请的内存大于一个内存页,则会降低程序指令访存cache命中率。所以,在nginx内存池小块内存管理单元中,其有效内存的最大值为一个页面大小。