14.2 共享内存

共享内存是Linux下提供的最基本的进程间通信方法,它通过mmap或者shmget系统调用在内存中创建了一块连续的线性地址空间,而通过munmap或者shmdt系统调用可以释放这块内存。使用共享内存的好处是当多个进程使用同一块共享内存时,在任何一个进程修改了共享内存中的内容后,其他进程通过访问这段共享内存都能够得到修改后的内容。

注意 虽然mmap可以以磁盘文件的方式映射共享内存,但在Nginx封装的共享内存操作方法中是没有使用到映射文件功能的。

Nginx定义了ngx_shm_t结构体,用于描述一块共享内存,代码如下所示。


typedef struct{

//指向共享内存的起始地址

u_char*addr;

//共享内存的长度

size_t size;

//这块共享内存的名称

ngx_str_t name;

//记录日志的ngx_log_t对象

ngx_log_t*log;

//表示共享内存是否已经分配过的标志位,为1时表示已经存在

ngx_uint_t exists;

}ngx_shm_t;


操作ngx_shm_t结构体的方法有以下两个:ngx_shm_alloc用于分配新的共享内存,而ngx_shm_free用于释放已经存在的共享内存。在描述这两个方法前,先以mmap为例说明Linux是怎样向应用程序提供共享内存的,如下所示。


voidmmap(voidstart,size_t length,int prot,int flags,

int fd,off_t offset);


mmap可以将磁盘文件映射到内存中,直接操作内存时Linux内核将负责同步内存和磁盘文件中的数据,fd参数就指向需要同步的磁盘文件,而offset则代表从文件的这个偏移量处开始共享,当然Nginx没有使用这一特性。当flags参数中加入MAP_ANON或者MAP_ANONYMOUS参数时表示不使用文件映射方式,这时fd和offset参数就没有意义,也不需要传递了,此时的mmap方法和ngx_shm_alloc的功能几乎完全相同。length参数就是将要在内存中开辟的线性地址空间大小,而prot参数则是操作这段共享内存的方式(如只读或者可读可写),start参数说明希望的共享内存起始映射地址,当然,通常都会把start设为NULL空指针。

先来看看如何使用mmap实现ngx_shm_alloc方法,代码如下。


ngx_int_t ngx_shm_alloc(ngx_shm_t*shm)

{

//开辟一块shm->size大小且可以读/写的共享内存,内存首地址存放在addr中

shm->addr=(u_char*)mmap(NULL,shm->size,

PROT_READ|PROT_WRITE,

MAP_ANON|MAP_SHARED,-1,0);

if(shm->addr==MAP_FAILED){

return NGX_ERROR;

}

return NGX_OK;

}


这里不再介绍shmget方法申请共享内存的方式,它与上述代码相似。

当不再使用共享内存时,需要调用munmap或者shmdt来释放共享内存,这里还是以与mmap配对的munmap为例来说明。


int munmap(void*start,size_t length);


其中,start参数指向共享内存的首地址,而length参数表示这段共享内存的长度。下面看看ngx_shm_free方法是怎样通过munmap来释放共享内存的。


void ngx_shm_free(ngx_shm_t*shm)

{

//使用ngx_shm_t中的addr和size参数调用munmap释放共享内存即可

if(munmap((void*)shm->addr,shm->size)==-1){

ngx_log_error(NGX_LOG_ALERT,shm->log,ngx_errno,"munmap(%p,%uz)

failed",shm->addr,shm->size);

}

}


Nginx各进程间共享数据的主要方式就是使用共享内存(在使用共享内存时,Nginx一般是由master进程创建,在master进程fork出worker子进程后,所有的进程开始使用这块内存中的数据)。在开发Nginx模块时如果需要使用它,不妨用Nginx已经封装好的ngx_shm_alloc方法和ngx_shm_free方法,它们有3种实现(不映射文件使用mmap分配共享内存、以/dev/zero文件使用mmap映射共享内存、用shmget调用来分配共享内存),对于Nginx的跨平台特性考虑得很周到。下面以一个统计HTTP框架连接状况的例子来说明共享内存的用法。

作为Web服务器,Nginx具有统计整个服务器中HTTP连接状况的功能(不是某一个Nginx worker进程的状况,而是所有worker进程连接状况的总和)。例如,可以用于统计某一时刻下Nginx已经处理过的连接状况。下面定义的6个原子变量就是用于统计ngx_http_stub_status_module模块连接状况的,如下所示。


//已经建立成功过的TCP连接数

ngx_atomic_t ngx_stat_accepted0;

ngx_atomic_t*ngx_stat_accepted=&ngx_stat_accepted0;

/已经从ngx_cycle_t核心结构体的free_connections连接池中获取到ngx_connection_t对象的活跃连接数/

ngx_atomic_t ngx_stat_active0;

ngx_atomic_t*ngx_stat_active=&ngx_stat_active0;

/连接建立成功且获取到ngx_connection_t结构体后,已经分配过内存池,并且在表示初始化了读/写事件后的连接数/

ngx_atomic_t ngx_stat_handled0;

ngx_atomic_t*ngx_stat_handled=&ngx_stat_handled0;

//已经由HTTP模块处理过的连接数

ngx_atomic_t ngx_stat_requests0;

ngx_atomic_t*ngx_stat_requests=&ngx_stat_requests0;

//正在接收TCP流的连接数

ngx_atomic_t ngx_stat_reading0;

ngx_atomic_t*ngx_stat_reading=&ngx_stat_reading0;

//正在发送TCP流的连接数

ngx_atomic_t ngx_stat_writing0;

ngx_atomic_t*ngx_stat_writing=&ngx_stat_writing0;


ngx_atomic_t原子变量将会在14.3节详细介绍,本节仅关注这6个原子变量是如何使用共享内存在多个worker进程中使用这些统计变量的。


size_t size,cl;

ngx_shm_t shm;


/计算出需要使用的共享内存的大小。为什么每个统计成员需要使用128字节呢?这似乎太大了,看上去,每个ngx_atomic_t原子变量最多需要8字节而已。其实是因为Nginx充分考虑了CPU的二级缓存。在目前许多CPU架构下缓存行的大小都是128字节,而下面需要统计的变量都是访问非常频繁的成员,同时它们占用的内存又非常少,所以采用了每个成员都使用128字节存放的形式,这样速度更快/


cl=128;

size=cl

/ngx_accept_mutex/

+cl

/ngx_connection_counter/

+cl;

/ngx_temp_number/

//定义了NGX_STAT_STUB宏后才会统计上述6个原子变量

if(NGX_STAT_STUB)

size+=cl

/ngx_stat_accepted/

+cl

/ngx_stat_handled/

+cl

/ngx_stat_requests/

+cl

/ngx_stat_active/

+cl

/ngx_stat_reading/

+cl;

/ngx_stat_writing/

endif

//初始化描述共享内存的ngx_shm_t结构体

shm.size=size;

shm.name.len=sizeof("nginx_shared_zone");

shm.name.data=(u_char*)"nginx_shared_zone";

shm.log=cycle->log;

//开辟一块共享内存,共享内存的大小为shm.size

if(ngx_shm_alloc(&shm)!=NGX_OK){

return NGX_ERROR;

}

//共享内存的首地址就在shm.addr成员中

shared=shm.addr;

//原子变量类型的accept锁使用了128字节的共享内存

ngx_accept_mutex_ptr=(ngx_atomic_t*)shared;

//ngx_accept_mutex就是负载均衡锁,spin值为-1则是告诉Nginx这把锁不可以使进程进入睡眠状态,详见14.8节*/

ngx_accept_mutex.spin=(ngx_uint_t)-1;

/原子变量类型的ngx_connection_counter将统计所有建立过的连接数(包括主动发起的连接)/

ngx_connection_counter=(ngx_atomic_t)(shared+1cl);

if(NGX_STAT_STUB)

//依次初始化需要统计的6个原子变量,也就是使用共享内存作为原子变量

ngx_stat_accepted=(ngx_atomic_t)(shared+3cl);

ngx_stat_handled=(ngx_atomic_t)(shared+4cl);

ngx_stat_requests=(ngx_atomic_t)(shared+5cl);

ngx_stat_active=(ngx_atomic_t)(shared+6cl);

ngx_stat_reading=(ngx_atomic_t)(shared+7cl);

ngx_stat_writing=(ngx_atomic_t)(shared+8cl);

endif


这6个统计变量在初始化后,在处理请求的流程中由于其意义不同,所以其值会有所变化。例如,在HTTP框架中,刚开始接收客户端的HTTP请求时使用的是ngx_http_init_request方法,在这个方法中就会将ngx_stat_reading统计变量加1,表示正处于接收用户请求的连接数加1,如下所示。


(void)ngx_atomic_fetch_add(ngx_stat_reading,1);


而当读取完请求时,如在ngx_http_process_request方法中,开始处理用户请求(不再接收TCP消息),这时会把ngx_stat_reading统计变量减1,如下所示。


(void)ngx_atomic_fetch_add(ngx_stat_reading,-1);


这6个统计变量都是在关键的流程中进行维护的,每个worker进程修改的都是共享内存中的统计变量,它们对于整个Nginx服务来说是全局有效的。ngx_http_stub_status_module模块将负责在接收到相应的HTTP查询请求后,把这些统计变量以HTTP响应的方式发送给客户端。该模块也可以作为14.3节原子变量的使用案例。