共享内存是 Linux 下提供的最基本的进程间通信方法,它通过 mmap 或者 shmget 系统调用在内存中创建了一块连续的线性地址空间,而通过 munmap 或者 shmdt 系统调用可以释放这块内存。使用共享内存的好处是当多个进程使用同一块共享内存时,在任何一个进程修改了共享内存中的内容后,其他进程通过访问这段共享内存都能够得到修改后的内容。
Nginx 各进程间共享数据的主要方式就是使用共享内存(在使用共享内存时,Nginx 一般在 master 进程创建,在 master 进程 fork 出子进程后,所有的进程开始使用这块内存中的数据)。Nginx 为了兼容性,提供了三种共享内存的实现方式。
mmap 文档
shmget 文档

Nginx 共享内存特点

  • 实际上 mmapshmget 都支持多个无亲缘关系的进程间通信,mmap 通过映射到文件来实现,shmget 则是通过映射 key 的方式,可以通过 ipcs -m 来查看当前分配的共享内存情况。但是对于 Nginx 来说没有必要,因为 workermaster 是具有亲缘关系的进程,因此使用匿名共享内存的方式更为合适
  • 使用匿名共享内存的好处是,如果程序中没有主动释放这部分内存或因为异常情况导致没有释放内存,当进程结束后内存将由操作系统回收
  • shmgetmmap 有个不同之处就是通过 key 映射的内存如果不释放则不会由操作系统回收,需要通过 ipcs 命令手动删除

Nginx 共享内存结构

  • addr 指向共享内存的起始地址
  • 共享内存的大小为 size 字节
  • 共享内存的名称用 name 表示
  • 记录日志的 log 成员
  • exists 是表示共享内存是否已经分配过的标志,为 1 时表示已经存在
typedef struct {
    u_char      *addr;
    size_t       size;
    ngx_str_t    name;
    ngx_log_t   *log;
    ngx_uint_t   exists;   /* unsigned  exists:1;  */
} ngx_shm_t;

Nginx 共享内存的创建和删除

mmap

  1. 分配使用 mmap 系统调用,设置 MAP_ANON 创建匿名内存,这个标志表示不使用文件映射方式,这时 fdoffset 参数就没有意义,mmap 分配的匿名内存只能在具有亲缘关系的进程之间共享,且如果内存不释放,会由操作系统回收
  2. 释放内存使用 munmap 系统调用
  3. 如果不支持 MAP_ANON 标志,则通过通过类似磁盘映射的方式,如果是一般的文件需要在文件打开的时候操作共享内存,如果关闭了文件描述符再操作则会引发 Bus error ,但是如果映射到 /dev/zero 则与匿名内存行为相同,它是一个特殊的设备文件,不会存储任何实际的数据,而是在需要时生成零值。当将其映射到内存中时,实际上并不是从文件中读取数据,而是在内存中分配了一块新的区域,并将其初始化为零值。
  4. 由于 /dev/zero 不是一个普通文件,而是是一个用于生成零值的特殊设备文件,所以即使关闭了与之关联的文件描述符,内存映射区域仍然存在,并且可以继续访问和使用
    #if (NGX_HAVE_MAP_ANON)
    
    ngx_int_t
    ngx_shm_alloc(ngx_shm_t *shm)
    {
        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_int_t
    ngx_shm_alloc(ngx_shm_t *shm)
    {
        ngx_fd_t  fd;
    
        fd = open("/dev/zero", O_RDWR);
    
        if (fd == -1) {
            ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,
                          "open(\"/dev/zero\") failed");
            return NGX_ERROR;
        }
    
        shm->addr = (u_char *) mmap(NULL, shm->size, PROT_READ|PROT_WRITE,
                                    MAP_SHARED, fd, 0);
    
        if (shm->addr == MAP_FAILED) {
            ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,
                          "mmap(/dev/zero, MAP_SHARED, %uz) failed", shm->size);
        }
    
        if (close(fd) == -1) {
            ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,
                          "close(\"/dev/zero\") failed");
        }
    
        return (shm->addr == MAP_FAILED) ? NGX_ERROR : 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);
        }
    }

shmget

  1. Nginx 中使用 shmgetIPC_PRIVATE 选项来创建匿名内存,使用这种方式创建,可以提前调用 shctl 来删除得到的 id,同 mmap 关闭文件描述符类似。在调用 shmctl 函数时使用 IPC_RMID 命令会从内核中删除共享内存段的数据结构,但并不会影响当前已经连接到该共享内存段的进程。换句话说,它不会导致对共享内存段的映射立即无效。这是因为共享内存的机制允许多个进程将同一块内存映射到它们的地址空间中。因此,尽管调用了 shmctl 函数删除了共享内存段的标识符,但在当前进程中,仍然可以继续访问和使用共享内存段,直到它被分离或进程终止
  2. 释放共享内存使用 shmdt 系统调用,注意调用 shmdt 只是分离了共享内存,只有当所有使用它的进程都 detach 了这块内存才会真正释放,类似引用计数
    #elif (NGX_HAVE_SYSVSHM)
    
    #include <sys/ipc.h>
    #include <sys/shm.h>
    
    
    ngx_int_t
    ngx_shm_alloc(ngx_shm_t *shm)
    {
        int  id;
    
        id = shmget(IPC_PRIVATE, shm->size, (SHM_R|SHM_W|IPC_CREAT));
    
        if (id == -1) {
            ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,
                          "shmget(%uz) failed", shm->size);
            return NGX_ERROR;
        }
    
        ngx_log_debug1(NGX_LOG_DEBUG_CORE, shm->log, 0, "shmget id: %d", id);
    
        shm->addr = shmat(id, NULL, 0);
    
        if (shm->addr == (void *) -1) {
            ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno, "shmat() failed");
        }
    
        if (shmctl(id, IPC_RMID, NULL) == -1) {
            ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,
                          "shmctl(IPC_RMID) failed");
        }
    
        return (shm->addr == (void *) -1) ? NGX_ERROR : NGX_OK;
    }
    
    
    void
    ngx_shm_free(ngx_shm_t *shm)
    {
        if (shmdt(shm->addr) == -1) {
            ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,
                          "shmdt(%p) failed", shm->addr);
        }
    }
    
    #endif
    

Nginx 共享内存使用

  • 在 Nginx 中共享内存主要用于 accept_mutex 和统计连接相关的信息,如总连接数
  • 在 master 进程初始化时,会为这些变量分配响应的内存,供 worker 进程工作时使用
  • 以下代码段来自 ngx_event_module_init 事件模块初始化函数,当使用 ngx_http_stub_status_module 模块时,还支持统计更多状态的连接数
        /* cl should be equal to or greater than cache line size */
    
        cl = 128;
    
        size = cl            /* ngx_accept_mutex */
               + cl          /* ngx_connection_counter */
               + cl;         /* ngx_temp_number */
    
    #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 */
               + cl;         /* ngx_stat_waiting */
    
    #endif
    
        shm.size = size;
        ngx_str_set(&shm.name, "nginx_shared_zone");
        shm.log = cycle->log;
    
        if (ngx_shm_alloc(&shm) != NGX_OK) {
            return NGX_ERROR;
        }
    
        shared = shm.addr;
    
        ngx_accept_mutex_ptr = (ngx_atomic_t *) shared;
        ngx_accept_mutex.spin = (ngx_uint_t) -1;
    
        if (ngx_shmtx_create(&ngx_accept_mutex, (ngx_shmtx_sh_t *) shared,
                             cycle->lock_file.data)
            != NGX_OK)
        {
            return NGX_ERROR;
        }
    
        ngx_connection_counter = (ngx_atomic_t *) (shared + 1 * cl);
    
        (void) ngx_atomic_cmp_set(ngx_connection_counter, 0, 1);
    
        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                       "counter: %p, %uA",
                       ngx_connection_counter, *ngx_connection_counter);
    
        ngx_temp_number = (ngx_atomic_t *) (shared + 2 * cl);
    
        tp = ngx_timeofday();
    
        ngx_random_number = (tp->msec << 16) + ngx_pid;
    
    #if (NGX_STAT_STUB)
    
        ngx_stat_accepted = (ngx_atomic_t *) (shared + 3 * cl);
        ngx_stat_handled = (ngx_atomic_t *) (shared + 4 * cl);
        ngx_stat_requests = (ngx_atomic_t *) (shared + 5 * cl);
        ngx_stat_active = (ngx_atomic_t *) (shared + 6 * cl);
        ngx_stat_reading = (ngx_atomic_t *) (shared + 7 * cl);
        ngx_stat_writing = (ngx_atomic_t *) (shared + 8 * cl);
        ngx_stat_waiting = (ngx_atomic_t *) (shared + 9 * cl);
    
    #endif