1. 程式人生 > >Nginx原始碼分析與實踐---程序間通訊機制(共享記憶體)

Nginx原始碼分析與實踐---程序間通訊機制(共享記憶體)

Nginx有一個master程序和多個worker程序,那麼master程序與worker程序間或worker程序之間是如何通訊的呢,又什麼時候需要程序間通訊呢?

我們知道linux下的程序間通訊方式主要有:管道、FIFO、套接字、訊息佇列、共享記憶體、訊號。那麼nginx的程序間通訊方式採用的是什麼呢?

nginx的3種程序間通訊方式為:共享記憶體套接字訊號


共享記憶體

1.什麼時候需要使用共享記憶體呢?

舉個栗子:nginx程序間共享的資料就需要使用共享記憶體,比如伺服器中HTTP的連線數:成功建立連線的TCP數、正在傳送TCP流的連線數、正在接收TCP流的連線數等等。對於這些共享的資料,就應該使用共享記憶體。每個worker程序修改的都是共享記憶體中的資料,修改是對所有程序都有效的。

2.nginx的共享記憶體是怎麼實現的呢?

我們知道,在linux下可以通過mmap分配共享記憶體,用munmap釋放共享記憶體。nginx共享記憶體的實現其實就是把這兩個方法進行封裝。下面看原始碼:

.../os/unix/ngx_shmem.h

/*
 * Copyright (C) Igor Sysoev
 * Copyright (C) Nginx, Inc.
 */


#ifndef _NGX_SHMEM_H_INCLUDED_
#define _NGX_SHMEM_H_INCLUDED_


#include <ngx_config.h>
#include <ngx_core.h>


typedef struct {
    u_char      *addr;    /* 共享記憶體的起始地址 */
    size_t       size;    /* 共享記憶體的長度 */
    ngx_str_t    name;    /* 共享記憶體的名字  */
    ngx_log_t   *log;     /* 記錄日誌的物件 */
    ngx_uint_t   exists;   /* unsigned  exists:1;  */ /*共享記憶體是否已經分配過,1:已經分配 */
} ngx_shm_t;


ngx_int_t ngx_shm_alloc(ngx_shm_t *shm);    /* 分配新的共享記憶體 */
void ngx_shm_free(ngx_shm_t *shm);    /*釋放已經存在的共享記憶體 */


#endif /* _NGX_SHMEM_H_INCLUDED_ */
分析:

共享記憶體通過ngx_shm_t這個結構體進行管理,每個成員的意義都已註釋。共享記憶體的方法就兩個:ngx_shm_alloc、ngx_shm_free。一個分配、一個釋放。這兩個方法其實就是對mmap和munmap進行封裝。所以瞭解這兩個方法之前得先看下linux的mmap和munmap函式:

void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);

返回值:成功:被對映區的起始地址;出錯:MAP_FAILED

addr: 指定的fd描述符應被對映到程序地址空間的起始地址,一般為NULL,意思就是讓核心自己去選擇起始地址

len: 對映到程序地址空間的位元組數

prot:對這塊共享記憶體中的資料,我們可以處理的方式,如下:


flags:變動共享記憶體區中的資料這一行為是共享的還是私有的,即對所有程序可見,還是隻對該程序可見。如下:


fd:被對映的檔案描述符

offset:被對映區域在檔案中的起始位置。

具體的見下圖,一目瞭然。


需要注意的是:nginx的共享記憶體不是對映檔案中的內容。當flags引數中MAP_ANON或MAP_ANONYMOUS,表示不從檔案中對映,只從記憶體中開闢一塊連續的線性地址空間出來作為共享記憶體。因此,這種情況下fd和offset引數就沒意義,分別置-1和0即可。

為從某一程序的地址空間中刪除一個對映關係,呼叫munmap。

int munmap(void *addr, size_t len);

成功:0;出錯:-1

好了,下面終於可以看ngx_shm_alloc和ngx_shm_free的原始碼了。

.../os/unix/ngx_shmem.c

#if (NGX_HAVE_MAP_ANON)

ngx_int_t
ngx_shm_alloc(ngx_shm_t *shm)
{
    /* MAP_ANON:不使用檔案對映方式,因此fd,offset無用,相當於在記憶體開闢一塊空間用於共享,由master建立 */
    shm->addr = (u_char *) mmap(NULL, shm->size,
                                PROT_READ|PROT_WRITE,
                                MAP_ANON|MAP_SHARED, -1, 0);

    if (shm->addr == MAP_FAILED) {
        ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,
                      "mmap(MAP_ANON|MAP_SHARED, %uz) failed", shm->size);
        return NGX_ERROR;
    }

    return NGX_OK;
}


void
ngx_shm_free(ngx_shm_t *shm)
{
    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);
    }
}

#elif (NGX_HAVE_MAP_DEVZERO)
可以看到ngx_shm_alloc和ngx_shm_free的確是對mmap和munmap分別進行了封裝。而且使用了MAP_ANON,是在記憶體中開闢了一塊空間用於共享記憶體,而不是將硬碟中的檔案對映。
3.nginx是如何使用共享記憶體的呢?

首先,我們得知道:預設情況下,通過fork派生的子程序並不與其父程序共享記憶體區。但master與worker程序是父子程序啊,這該怎麼辦呢?如何讓master程序與worker程序共享記憶體區呢?

解決方法就在於mmap的flags引數。master程序在呼叫fork之前先指定flags為MAP_SHARED來呼叫mmap,此時,POSIX是保證父程序中的記憶體對映關係是存留到子程序中的,父程序對共享記憶體所做的修改子程序能看到,反過來一樣。所以流程是:master程序在記憶體中以MAP_SHARED方式開闢一塊共享記憶體,並對映到自己程序地址空間中的共享記憶體區,然後master呼叫fork,派生子程序,子程序在自己的地址空間內也會繼承這塊共享記憶體區。這樣問題便解決了。