Skip to content

共享内存

约 6564 个字 1199 行代码 预计阅读时间 37 分钟

参考链接:

共享内存是==一种高效的进程间通信(IPC)机制,允许多个进程将同一块物理内存区域映射到各自的虚拟地址空间中==。作为速度最快的IPC形式,它通过直接读写数据,避免了内核空间与用户空间的数据复制。需配合信号量等同步机制使用,以防止数据冲突。

在 Unix/Linux 系统中,IPC_PRIVATE 是一个特殊的常量,用于进程间通信(IPC)机制中,如信号量(semaphore)、消息队列(message queue)和共享内存(shared memory)。它的主要作用是创建一个私有的 IPC 对象,确保这些对象只能被创建它们的进程及其子进程访问。

IPC_PRIVATE 的作用:

  • 创建私有 IPC 对象:当使用 IPC_PRIVATE 作为 IPC 对象的键时,系统会创建一个全新的、唯一的 IPC 对象,而不是查找已有的对象。
  • 限制访问范围:使用 IPC_PRIVATE 创建的 IPC 对象只能被创建它的进程及其子进程访问,其他进程无法通过相同的键来访问这个对象。

SystemV共享内存

ftok有什么用?

先搞懂为什么需要 ftok?

ftok 是 System V IPC 体系中一个**关键辅助函数**,核心作用是「将一个文件路径 + 一个字符标识符」转换成唯一的 key_t 类型键值,用于创建 / 获取 System V 共享内存、消息队列、信号量等 IPC 资源。

System V IPC 资源(如 shmget 创建的共享内存)是通过**唯一的 key 值**来标识的 —— 多个进程要访问同一块共享内存,必须使用同一个 key 值。

如果直接手动定义 key(比如你之前示例中的 0x1234),容易和系统中其他程序的 key 冲突;而 ftok 可以基于系统中**已存在的文件**生成唯一 key,大幅降低冲突概率,是规范的用法。

ftok 会结合:

  1. 文件的**索引节点号(inode)**(系统中每个文件的唯一标识);
  2. proj_id 的低 8 位; 生成一个唯一的 key 值。

注意:如果文件被删除后重新创建,即使路径相同,inode 会变化,ftok 生成的 key 也会变!

关键函数

关键函数如下:

/* Generates key for System V style IPC. 用户使用ftok生成独有的key作为创建shm的入参*/
key_t ftok (const char *__pathname, int __proj_id);

/**
Get shared memory segment. 用户提供key,OS创建共享内存,并返回唯一的shmid
在共享内存未被真正删除或者标记删除的情况下,不同进程调用相同的key会生成相同的shmid
*/
extern int shmget (key_t __key, size_t __size, int __shmflg) __THROW;

/**
Attach shared memory segment.为共享内存挂载进程级虚拟内存空间
该接口支持对同一个shmid重复挂载,结果是同一个进程内可以为同一个物理共享内存创建不同的虚拟内存映射,仍有共享效果
*/
extern void *shmat (int __shmid, const void *__shmaddr, int __shmflg)

/**
Detach shared memory segment.为虚拟内存地址解挂共享空间
该接口一般与shmat获取的地址配套使用,解挂不会真正删除共享内存,但是对应的虚拟内存地址就不可访问了,共享内存仍可被其他挂载的进程使用
如果未显式解挂,进程退出后也会自动解挂,解挂后shmid对应的共享内存引用也会相应自减
*/
extern int shmdt (const void *__shmaddr) __THROW;

/** 
Shared memory control operation.
可以通过cmd控制删除获取读取共享内存状态信息
对于删除功能而言,如果shm仍被进程挂载,则只是标删且该内存无法创建新的挂载,因此共享内存的生命周期是要么在接挂完成后显示调用删除,要么在解挂前显式标删,然后由最后一个接挂shmdt同时触发删除
 */
extern int shmctl (int __shmid, int __cmd, struct shmid_ds *__buf) __THROW;

核心特征

  • case1:同一个进程内,测试使用IPC_EXCL对同一个key重复shmget的影响
  • case2:父进程创建共享内存,不同子进程分别挂载和分离,父进程最重负载释放,共享内存可以共享同一份数据
  • case3:父进程创建共享内存+挂载+主动触发删除 +分离,子进程无需挂载和分离,直接继承父进程虚拟地址空间,共享内存可以共享同一份数据
  • case4:在共享内存未被真正删除或者标记删除的情况下,不同进程调用相同的key会生成相同的shmid
  • case5:对同一个shmid重复挂载实际上是对同一个物理共享内存创建多份不同的虚拟空间映射,该共享内存引用计数会增加,需要对应配套的分离解挂
  • case6:进程如果未显式shmdt分离,进程退出后也会自动分离减引用,但是删除共享内存需要显示调用
  • case7:子进程退出前显式标删再解绑,后者也能触发内存删除,父进程使用相同的key搭配IPC_EXCL能创建共享内存,但是shmid会换新
  • case8:子进程退出前显式标删但是未显式解绑,进程退出后会自动解绑并也能触发内存删除,父进程使用相同的key搭配IPC_EXCL能创建共享内存,但是shmid会换新

示例代码

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/unistd.h>
#include <string.h>

#define SHM_SIZE 4096
#define PRINT_SUCCESSFUL printf("[%s] successful...\n--------------------------------\n", __func__)

int get_nattch(int shmid) {
    struct shmid_ds ds = {};
    if(shmctl(shmid, IPC_STAT, &ds) == -1) {
        perror("shmctl:");
        return -1;
    }
    return ds.shm_nattch;
}

/**
 * case1:同一个进程内,测试使用IPC_EXCL对同一个key重复shmget的影响
 */
void system_v_shm_case1()
{
    char *pathname = "/home/ubuntu";
    int proj_id = 12345;

    // step1: 通过ftok生成ipc key,由于ftok计算时会使用inode,因此pathname必须真实存在的路径
    key_t key = ftok(pathname, proj_id);
    if (key == -1) {
        perror("ftok:");
        return;
    }

    // step2:基于用户传入key,由OS创建对应的共享内存
    // 场景1:IPC_CREAT: 如果共享内存不存在,就创建共享内存;如果存在,则获取已创建的共享内存的标识符
    // 场景2:IPC_CREAT | IPC_EXCL 如果共享内存不存在则创建;如果存在则出错返回-1
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | SHM_R | SHM_W);
    if (shmid == -1) {
        printf("unexpect first create shm failed\n");
        return;
    }

    // 对同一个key使用IPC_EXCL重复创建shm,预期失败
    shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | SHM_R | SHM_W);
    if (shmid != -1) {
        printf("unexpect repeat create shm success with IPC_EXCL\n");
        return;
    }

    // 对同一个key不使用IPC_EXCL重复创建shm,预期成功
    shmid = shmget(key, SHM_SIZE, IPC_CREAT | SHM_R | SHM_W);
    if (shmid == -1) {
        printf("unexpect repeat create shm failed without IPC_EXCL\n");
        return;
    }

    // step3:主动删除共享内存
    int ret = shmctl(shmid, IPC_RMID, NULL);
    if (ret == -1) {
        perror("shmctl:");
        return;
    }

    PRINT_SUCCESSFUL;
}

void fork_func_case2(int shmid, int i)
{
    static char str[] = "hello,world";

    // 将shm挂载到虚拟地址空间
    void *addr = shmat(shmid, NULL, 0);
    if (addr == NULL) {
        perror("shmat:");
        return;
    }

    // 偶数进程负责写内存,奇数进程负责读内存
    if (i % 2 == 0) {
        printf("i=%d, addr is %s\n", i, addr);
        memcpy(addr, str, strlen(str) + 1);
        printf("i=%d, addr is %s\n", i, addr);
    } else {
        sleep(3);
        printf("i=%d, addr is %s\n", i, addr);
    }

    // 卸载shm
    if(shmdt(addr) == -1) {
        perror("shmdt:");
        return;
    }
}

/**
 * case2:父进程创建共享内存,不同子进程分别挂载和分离,父进程最重负载释放,共享内存可以共享同一份数据
 */
void system_v_shm_case2()
{
    char *pathname = "/home/ubuntu";
    int proj_id = 12345;

    key_t key = ftok(pathname, proj_id);
    if (key == -1) {
        perror("ftok:");
        return;
    }

    // 创建shm或者获取已有shm
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | SHM_R | SHM_W);
    if (shmid == -1) {
        perror("shmget:");
        return;
    }

    for (int i = 0; i < 2; i++) {
        if (fork() == 0) {
            fork_func_case2(shmid, i);
            // !!!关键:子进程必须退出,防止进入下一轮循环
            exit(0);
        }
    }

    // 等待所有子进程结束
    while ((wait(NULL)) != -1) {
    }

    // 主动触发删除shm
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl:");
        return;
    }

    PRINT_SUCCESSFUL;
}

void fork_func_case3(char* addr, int i)
{
    static char str[] = "hello,world";

    // 偶数进程负责写内存,奇数进程负责读内存
    if (i % 2 == 0) {
        printf("i=%d, addr is %s\n", i, addr);
        memcpy(addr, str, strlen(str) + 1);
        printf("i=%d, addr is %s\n", i, addr);
    } else {
        sleep(3);
        printf("i=%d, addr is %s\n", i, addr);
    }
}

/**
 * case3:父进程创建共享内存+挂载+主动触发删除 +分离,子进程无需挂载和分离,直接继承父进程虚拟地址空间,共享内存可以共享同一份数据
 */
void system_v_shm_case3()
{
    char *pathname = "/home/ubuntu";
    int proj_id = 12345;

    key_t key = ftok(pathname, proj_id);
    if (key == -1) {
        perror("ftok:");
        return;
    }

    // 创建shm或者获取已有shm
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | SHM_R | SHM_W);
    if (shmid == -1) {
        perror("shmget:");
        return;
    }

    // 将shm挂载到虚拟地址空间
    void *addr = shmat(shmid, NULL, 0);
    if (addr == NULL) {
        perror("shmat:");
        return;
    }

    // 主动触发删除shm,删除后无法再继续挂载,该共享内存仍会存在且可被父进程和子进程继续使用,由shmdt分离时同时触发删除
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl:");
        return;
    }

    for (int i = 0; i < 2; i++) {
        if (fork() == 0) {
            fork_func_case3(addr, i);
            // !!!关键:子进程必须退出,防止进入下一轮循环
            exit(0);
        }
    }

    // 等待所有子进程结束
    while ((wait(NULL)) != -1) {
    }

    // 最后由主进程分离shm,同时会触发删除共享内存
    if(shmdt(addr) == -1) {
        perror("shmdt:");
        return;
    }

    PRINT_SUCCESSFUL;
}

/**
 * case4:在共享内存未被真正删除或者标记删除的情况下,不同进程调用相同的key会生成相同的shmid
 */
void system_v_shm_case4()
{
    char *pathname = "/home/ubuntu";
    int proj_id = 12345;

    key_t key = ftok(pathname, proj_id);
    if (key == -1) {
        perror("ftok:");
        return;
    }

    for (int i = 0; i < 4; i++) {
        if (fork() == 0) {
            int shmid = shmget(key, SHM_SIZE, IPC_CREAT | SHM_R | SHM_W);
            if (shmid == -1) {
                perror("shmget:");
                return;
            }

            void *addr = shmat(shmid, NULL, 0);
            if (addr == NULL) {
                perror("shmat:");
                return;
            }
            struct shmid_ds ds = {};
            if(shmctl(shmid, IPC_STAT, &ds) == -1) {
                perror("shmctl:");
                return;
            }

            // nattch记录当前该shm被多少个进程attch
            printf("child i=%d, pid=%d, key=%d, shmid=%d, addr=%p, nattach=%d\n", i, getpid(), key, shmid, addr, ds.shm_nattch);

            // 各个进程nattach统计后再分离
            sleep(5);
            if(shmdt(addr) == -1) {
                perror("shmdt:");
                return;
            }

            // !!!关键:子进程必须退出,防止进入下一轮循环
            exit(0);
        }
        sleep(1);
    }

    // 等待所有子进程结束
    while ((wait(NULL)) != -1) {}

    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | SHM_R | SHM_W);
    if (shmid == -1) {
        perror("shmget:");
        return;
    }

    printf("parent pid=%d, key=%d, shmid=%d\n",  getpid(), key, shmid);

    // 由父进程做最后的删除动作
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl:");
        return;
    }

    PRINT_SUCCESSFUL;
}

/**
 * case5:对同一个shmid重复挂载实际上是对同一个物理共享内存创建多份不同的虚拟空间映射,该共享内存引用计数会增加,需要对应配套的分离解挂
 */
void system_v_shm_case5()
{
    static char str[] = "hello,world";
    char *pathname = "/home/ubuntu";
    int proj_id = 12345;

    key_t key = ftok(pathname, proj_id);
    if (key == -1) {
        perror("ftok:");
        return;
    }

    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | SHM_R | SHM_W);
    if (shmid == -1) {
        perror("shmget:");
        return;
    }

    void *addr1 = shmat(shmid, NULL, 0);
    if (addr1 == NULL) {
        perror("shmat:");
        return;
    }
    memcpy(addr1, str, strlen(str) + 1);

    printf("first shmat addr=%p, nattach=%d, content=%s\n", addr1, get_nattch(shmid), addr1);

    void *addr2 = shmat(shmid, NULL, 0);
    if (addr2 == NULL) {
        perror("shmat:");
        return;
    }
    printf("second shmat addr=%p, nattach=%d, content=%s\n", addr2, get_nattch(shmid), addr2);

    if (addr1 != addr2 && strcmp(addr1, addr2) == 0) {
        printf("diff virtual addr, same physical addr\n");
    }

    if(shmdt(addr1) == -1) {
        perror("shmdt:");
        return;
    }
    printf("first shmdt addr=%p, nattach=%d\n", addr1, get_nattch(shmid));

    if(shmdt(addr2) == -1) {
        perror("shmdt:");
        return;
    }

    printf("second shmdt addr=%p, nattach=%d\n", addr2, get_nattch(shmid));

    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl:");
        return;
    }

    PRINT_SUCCESSFUL;
}

/**
 * case6:进程如果未显式shmdt分离,进程退出后也会自动分离减引用,但是删除共享内存需要显示调用
 */
void system_v_shm_case6()
{
    char *pathname = "/home/ubuntu";
    int proj_id = 12345;
    static char str[] = "hello,world";

    key_t key = ftok(pathname, proj_id);
    if (key == -1) {
        perror("ftok:");
        return;
    }

    if (fork() == 0) {
        int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | SHM_R | SHM_W);
        if (shmid == -1) {
            perror("shmget:");
            return;
        }

        void *addr = shmat(shmid, NULL, 0);
        if (addr == NULL) {
            perror("shmat:");
            return;
        }
        memcpy(addr, str, strlen(str) + 1);
        printf("child pid=%d, key=%d, shmid=%d, nattch=%d, content=%s\n", getpid(), key, shmid, get_nattch(shmid), addr);

        // !!!关键:子进程必须退出,防止进入下一轮循环
        exit(0);
    }

    // 等待子进程结束
    wait(NULL);

    // 子进程退出时,未显式标删,预期主进程则无法基于IPC_EXCL创建
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL| SHM_R | SHM_W);
    if (shmid != -1) {
        printf("unexpected shmget success\n");
        return;
    }

    // 子进程退出时,未显式标删,主进程获取已有shmid
    shmid = shmget(key, SHM_SIZE, IPC_CREAT | SHM_R | SHM_W);
    if (shmid == -1) {
        perror("shmget:");
        return;
    }
    printf("parent pid=%d, key=%d, shmid=%d, nattch=%d\n", getpid(), key, shmid, get_nattch(shmid));

    void *addr = shmat(shmid, NULL, 0);
    if (addr == NULL) {
        perror("shmat:");
        return;
    }
    // 子进程由于未主动删除共享内存,所以父进程仍然可以读取该段内存的内容
    printf("parent pid=%d, key=%d, shmid=%d, nattch=%d, content=%s\n", getpid(), key, shmid, get_nattch(shmid), addr);

    if(shmdt(addr) == -1) {
        perror("shmdt:");
        return;
    }

    // 由父进程做最后的删除动作
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl:");
        return;
    }

    PRINT_SUCCESSFUL;
}


/**
 * case7:子进程退出前显式标删再解绑,后者也能触发内存删除,父进程使用相同的key搭配IPC_EXCL能创建共享内存,但是shmid会换新
 */
void system_v_shm_case7()
{
    char *pathname = "/home/ubuntu";
    int proj_id = 12345;
    static char str[] = "hello,world";

    key_t key = ftok(pathname, proj_id);
    if (key == -1) {
        perror("ftok:");
        return;
    }

    if (fork() == 0) {
        int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | SHM_R | SHM_W);
        if (shmid == -1) {
            perror("shmget:");
            return;
        }

        void *addr = shmat(shmid, NULL, 0);
        if (addr == NULL) {
            perror("shmat:");
            return;
        }

        // 解绑前直接标删
        if (shmctl(shmid, IPC_RMID, NULL) == -1) {
            perror("shmctl:");
            return;
        }

        // 共享内存仍有效且可用
        memcpy(addr, str, strlen(str) + 1);
        printf("child pid=%d, key=%d, shmid=%d, nattch=%d, content=%s\n", getpid(), key, shmid, get_nattch(shmid), addr);

        // 标删后再解绑
        if(shmdt(addr) == -1) {
            perror("shmdt:");
            return;
        }

        // !!!关键:子进程必须退出,防止进入下一轮循环
        exit(0);
    }

    // 等待子进程结束
    wait(NULL);

    // 子进程退出前显式标删再解绑,后者也能触发内存删除,父进程使用相同的key搭配IPC_EXCL能创建共享内存,但是shmid会换新
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL| SHM_R | SHM_W);
    if (shmid == -1) {
        printf("unexpected shmget success\n");
        return;
    }
    printf("parent pid=%d, key=%d, shmid=%d, nattch=%d\n", getpid(), key, shmid, get_nattch(shmid));

    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl:");
        return;
    }

    PRINT_SUCCESSFUL;
}

/**
 * case8:子进程退出前显式标删但是未显式解绑,进程退出后会自动解绑并也能触发内存删除,父进程使用相同的key搭配IPC_EXCL能创建共享内存,但是shmid会换新
 */
void system_v_shm_case8()
{
    char *pathname = "/home/ubuntu";
    int proj_id = 12345;
    static char str[] = "hello,world";

    key_t key = ftok(pathname, proj_id);
    if (key == -1) {
        perror("ftok:");
        return;
    }

    if (fork() == 0) {
        int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | SHM_R | SHM_W);
        if (shmid == -1) {
            perror("shmget:");
            return;
        }

        void *addr = shmat(shmid, NULL, 0);
        if (addr == NULL) {
            perror("shmat:");
            return;
        }

        // 解绑前直接标删
        if (shmctl(shmid, IPC_RMID, NULL) == -1) {
            perror("shmctl:");
            return;
        }

        // 共享内存仍有效且可用
        memcpy(addr, str, strlen(str) + 1);
        printf("child pid=%d, key=%d, shmid=%d, nattch=%d, content=%s\n", getpid(), key, shmid, get_nattch(shmid), addr);

        // 标删后未主动解绑,依赖进程退出时触发解绑

        // !!!关键:子进程必须退出,防止进入下一轮循环
        exit(0);
    }

    // 等待子进程结束
    wait(NULL);

    // 子进程退出前显式标删再解绑,后者也能触发内存删除,父进程使用相同的key搭配IPC_EXCL能创建共享内存,但是shmid会换新
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL| SHM_R | SHM_W);
    if (shmid == -1) {
        printf("unexpected shmget success\n");
        return;
    }
    printf("parent pid=%d, key=%d, shmid=%d, nattch=%d\n", getpid(), key, shmid, get_nattch(shmid));

    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl:");
        return;
    }

    PRINT_SUCCESSFUL;
}

int main(int argc, char *args[])
{
    system_v_shm_case1();
    system_v_shm_case2();
    system_v_shm_case3();
    system_v_shm_case4();
    system_v_shm_case5();
    system_v_shm_case6();
    system_v_shm_case7();
    system_v_shm_case8();
}

输出:

[system_v_shm_case1] successful...
--------------------------------
i=0, addr is 
i=0, addr is hello,world
i=1, addr is hello,world
[system_v_shm_case2] successful...
--------------------------------
i=0, addr is 
i=0, addr is hello,world
i=1, addr is hello,world
[system_v_shm_case3] successful...
--------------------------------
child i=0, pid=6703, key=963123351, shmid=3, addr=0x7ffff7fbc000, nattach=1
child i=1, pid=6714, key=963123351, shmid=3, addr=0x7ffff7fbc000, nattach=2
child i=2, pid=6715, key=963123351, shmid=3, addr=0x7ffff7fbc000, nattach=3
child i=3, pid=6716, key=963123351, shmid=3, addr=0x7ffff7fbc000, nattach=4
parent pid=6658, key=963123351, shmid=3
[system_v_shm_case4] successful...
--------------------------------
first shmat addr=0x7ffff7fbc000, nattach=1, content=hello,world
second shmat addr=0x7ffff7fbb000, nattach=2, content=hello,world
diff virtual addr, same physical addr
first shmdt addr=0x7ffff7fbc000, nattach=1
second shmdt addr=0x7ffff7fbb000, nattach=0
[system_v_shm_case5] successful...
--------------------------------
child pid=6749, key=963123351, shmid=5, nattch=1, content=hello,world
parent pid=6658, key=963123351, shmid=5, nattch=0
parent pid=6658, key=963123351, shmid=5, nattch=1, content=hello,world
[system_v_shm_case6] successful...
--------------------------------
child pid=6750, key=963123351, shmid=6, nattch=1, content=hello,world
parent pid=6658, key=963123351, shmid=7, nattch=0
[system_v_shm_case7] successful...
--------------------------------
child pid=6751, key=963123351, shmid=8, nattch=1, content=hello,world
parent pid=6658, key=963123351, shmid=9, nattch=0
[system_v_shm_case8] successful...
--------------------------------

nginx中如何使用?

nginx是master进程负责共享内存的创建+挂载+显式标删(调用ngx_shm_alloc),由于在创建时已经标删,因此后续进程运行以及退出都无需关心shm的生命周期。时序上master创建和挂载共享内存后,再通过fork创建worker,因此各个worker进程无需涉及shm的各个接口,直接访问继承自master的shm->addr就可以实现对共享内存的共享操作。

另外,nginx选择使用IPC_PRIVATE创建进程私有的共享内存块,无需担心跟系统中的其他进程共享内存冲突问题。

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;
    }

    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);
    }
}

内存映射

私有映射 vs 共享映射

创建一个私有映射。区域中内容上所发生的变更对使用同一映射的其他进程是不可见的,对于文件映射来讲,所发生的变更将不会反应在底层文件上。

创建一个共享映射。区域中内容上所发生的变更对使用 MAP_SHARED 特性映射同一区域的进程是可见的,对于文件映射来讲,所发生的变更将直接反应在底层文件上。对文件的更新将无法确保立即生效。mmap 同步修改文件的底层逻辑是:

  1. MAP_SHARED 建立映射(而非 MAP_PRIVATE),进程对映射区域的修改会直接作用于内核**页缓存**(物理内存);
  2. 内核会异步(或通过手动触发)将页缓存中的修改刷回磁盘文件,完成 “内存修改→文件同步”。

一、MAP_PRIVATE的特点?

MAP_PRIVATEmmap 系统调用的**私有映射**标志,核心特点:

  • 映射区域的修改**只对当前进程可见**(写时复制,Copy-on-Write,COW);
  • 对映射内容的修改**不会同步回原文件 / 设备**;
  • 多个进程映射同一文件时,各自拥有独立的映射副本,互不干扰。

可以简单理解为:MAP_PRIVATE 给文件创建了一个 “进程私有副本”,你可以随意修改这个副本,但不会影响源文件或其他进程的映射。

二、MAP_PRIVATE的应用场景?

场景 1:只读访问文件(最基础)

这是 MAP_PRIVATE 最常用的场景 —— 仅读取文件内容,无需修改源文件,用 MAP_PRIVATE 可避免误写,且系统会优化内存使用(多个只读进程共享同一份物理内存)。

场景 2:临时修改文件内容(不影响源文件)

需要对文件内容做修改,但仅用于当前进程的计算 / 处理,不想改动源文件(比如解析配置文件时临时修改参数、处理数据文件时做临时计算)。

MAP_PRIVATE 的**写时复制**机制会在你首次修改映射区域时,为当前进程创建一份私有副本,原文件完全不受影响。

场景 3:进程间隔离的共享文件读取

多个进程同时映射同一文件,但各自需要独立的 “视图”—— 比如多进程处理同一配置文件,每个进程可能需要临时修改配置参数,但不想影响其他进程或源文件。

MAP_PRIVATE 能保证:

  • 所有进程初始时共享同一份物理内存(节省内存);
  • 任一进程修改映射内容时,会创建私有副本,其他进程仍看到原始内容。

场景 4:加载可执行文件 / 动态库(系统级应用)

操作系统加载可执行文件(ELF)或动态库(.so)时,会用 MAP_PRIVATE 映射代码段和数据段:

  • 代码段(只读):所有进程共享同一份物理内存;
  • 数据段(可写):进程首次修改时触发 COW,创建私有副本,保证进程间数据隔离。 这是操作系统高效管理进程内存的核心机制之一。

总结:

  1. 核心用途MAP_PRIVATE 用于 “只读访问文件” 或 “临时修改文件内容但不影响源文件 / 其他进程” 的场景;
  2. 底层机制:依赖 “写时复制(COW)”,只读时共享内存,写时创建私有副本,兼顾内存效率和进程隔离;
  3. 典型场景:只读读取文件、进程内临时修改文件、多进程隔离读取同一文件、系统加载可执行文件 / 动态库。

三、页缓存是什么?

页缓存(Page Cache) = 内核在物理内存里,给磁盘文件开的 “缓存页”。

所有文件读写,默认都走页缓存,不直接碰磁盘。

1、页缓存是什么?

  • 它是 Linux 内核管理的一片物理内存
  • 页(4KB) 为单位
  • 专门用来 缓存磁盘文件内容
  • 所有进程共享,不属于任何一个进程
  • 进程不能直接访问它,必须通过内核

可以理解成:**磁盘文件在内存里的 “镜像副本”。页缓存就在物理内存里,由内核全权管理。

2、页缓存的作用是什么?

  • 加速文件读写

    • 读:先从页缓存读,没有才去磁盘读

    • 写:先写页缓存,内核后台刷盘

  • 让多进程共享同一份文件数据

    • 进程 A 读文件 → 进页缓存

    • 进程 B 读同文件 → 直接用页缓存,不读磁盘

  • 避免频繁磁盘 IO

    • 页缓存就是**文件的高速中转站**。

3、页缓存与mmap的关系?

mmap文件映射做的事:直接把进程虚拟地址,映射到内核的页缓存!

所以:

  • read/write: 用户缓冲区 ←→ 页缓存 ←→ 磁盘

  • mmap: 虚拟地址 ←→ 页缓存 ←→ 磁盘

因此,mmap操作虚拟地址会直接操作页缓存,会比write/read少了一次拷贝!

4、为什么 MAP_PRIVATE 不写文件?

因为:

  • MAP_SHARED:写虚拟地址 = 直接改页缓存 → 会刷回文件
  • MAP_PRIVATE:一写就触发 COW(写时复制),内核复制一个**新的物理页**给你私有,这个页**不属于页缓存**自然不会同步到磁盘 这就是**动态库必须用 MAP_PRIVATE** 的原因:不污染原文件、不影响其他进程。

文件映射 vs 匿名映射

1、核心原理对比

要理解**文件映射**和**匿名映射**的原理,核心是抓住「是否关联磁盘文件」这个本质区别 —— 文件映射是 “虚拟地址 ↔ 磁盘文件” 的绑定,匿名映射是 “虚拟地址 ↔ 无文件的匿名物理页” 的绑定,两者共享 mmap 核心流程(虚拟地址分配 + 缺页异常),但数据来源和生命周期完全不同。

类型 核心原理 数据来源 典型用途
文件映射 虚拟地址映射到磁盘文件,缺页时从文件加载数据到页缓存,修改可同步回文件 磁盘文件 大文件读写、动态库加载
匿名映射 虚拟地址映射到 “无文件关联” 的匿名物理页,缺页时直接分配物理内存,无文件 IO 空白物理内存 进程私有内存分配(如 malloc)、共享内存

匿名映射**无文件关联**,是内核为进程分配 “纯内存” 的方式,malloc 分配大内存(通常 > 128KB)时底层就是匿名映射。

维度 文件映射 匿名映射
数据来源 磁盘文件(页缓存) 空白物理页(内核直接分配)
缺页异常处理 读取文件到页缓存 → 绑定物理地址 直接分配物理页 → 绑定(无 IO)
文件关联 关联 fd,有文件偏移 无 fd,偏移无意义
数据持久化 可通过 msync/fsync 同步到文件 进程退出后数据丢失(无持久化)
页缓存参与 必须经过页缓存 不经过页缓存(直接操作物理页)
典型场景 大文件读写、动态库加载、文件共享 malloc 大内存、父子进程共享内存

2、关键细节对比

  • 文件映射:

    • 数据锚点是磁盘文件:映射的虚拟地址最终对应 “文件内容”,哪怕进程退出,文件仍存在;

    • 页缓存是核心中间层:所有文件映射的读写都经过页缓存,避免直接磁盘 IO;

    • 生命周期与文件绑定:映射的数据可通过 msync/fsync 刷回文件,进程重启后可重新映射读取;内核会自动将发生在 MAP_SHARED 映射内容上的变更写入到底层文件中,但在默认情况下,内核不保证这种同步操作会在何时发生。msync()系统调用让应用程序能够显式地控制何时完成共享映射与映射文件之间的同步。

    • 动态库是特例:用 MAP_PRIVATE 映射,只读段共享页缓存,可写段 COW(修改不影响原文件)。

  • 匿名映射

    • 无文件依赖:映射的是 “纯物理内存”,数据只存在于内存中,进程退出后数据丢失;

    • 无页缓存参与:缺页时直接分配物理页,无需读取文件,性能比文件映射更快(无 IO);

    • 两种常见形式

      • 私有匿名映射(默认):MAP_ANONYMOUS | MAP_PRIVATE,进程私有内存(如 malloc);

      • 共享匿名映射:MAP_ANONYMOUS | MAP_SHARED,父子进程共享内存(fork 后);

    • 零填充特性:新分配的匿名物理页会被内核初始化为 0(避免泄露其他进程数据)。

    • 每次调用 mmap()创建一个私有匿名映射时都会产生一个新映射,该映射与同一(或不同)进程创建的其他匿名映射是不同的(即不会共享物理分页)。该性质决定了无法类似于SystemV的key或者mmap的文件fd创建映射与其他进程按预先预定进而实现进程间内存共享,匿名映射只能配合fork机制在父子进程以及相应的兄弟进程之间实现进程虚拟内存空间继承和共享。

3、底层核心共性

无论是文件映射还是匿名映射,都遵循 mmap 通用流程:

  1. 先分配虚拟地址:内核在进程虚拟地址空间划一块空闲区域,不立即分配物理内存;
  2. 懒加载(缺页触发):只有进程首次访问虚拟地址时,才触发缺页异常分配物理内存;
  3. 页表是核心桥梁:虚拟地址 ↔ 物理地址的映射都通过页表实现,内核仅维护页表和 vm_area_struct。

4、/dev/zero作为MAP_ANONYMOUS的替代方案

老系统 / 嵌入式系统部分不支持 MAP_ANONYMOUS,可通过映射 /dev/zero 实现相同效果。

/dev/zero 是 Linux/Unix 系统中一个特殊的字符设备文件,它的核心作用是:读取它时,会无限返回二进制 0 字节(\0);写入它时,所有数据会被直接丢弃(静默吞掉)。

mmap的底层原理

mmap 的底层本质是:内核在进程的虚拟地址空间和文件(或匿名内存)之间建立「虚拟地址 ↔ 文件偏移」的映射关系,并写入页表;当进程访问这段虚拟地址时,触发「缺页异常」,内核才真正分配物理内存、读取文件数据,完成虚拟地址到物理地址的最终映射。

阶段 1:用户调用 mmap () → 内核建立 “虚拟 - 文件” 映射(无 IO、无物理内存)

这一步是 “只建关系,不干活”:

  1. 用户调用 mmap(fd, len, prot, flags, fd, offset),触发系统调用陷入内核态;
  2. 内核先校验:文件是否可访问、虚拟地址是否冲突、权限(prot)是否合法;
  3. 内核在进程的虚拟地址空间中,找一段长度为 len 的**连续空闲虚拟地址区域**(用户指定 addr 则优先用,否则内核分配);
  4. 内核创建 vm_area_struct 结构体(内核中描述虚拟内存区域的核心结构),记录:

    • 这段虚拟地址的起始 / 结束地址;

    • 映射类型(文件 / 匿名)、对应的文件 fd 和偏移 offset

    • 访问权限(PROT_READ/PROT_WRITE 等)、映射标志(MAP_PRIVATE/MAP_SHARED);

  5. 内核更新进程的页表,但此时页表中**只记录 “虚拟地址 ↔ 文件偏移” 的关系,没有绑定物理地址**;

  6. 内核返回这段虚拟地址给用户进程,mmap 调用结束。 关键:这一步**没有读取文件、没有分配物理内存**,纯粹是 “划地址 + 记关系”,耗时极短。

阶段 2:进程首次访问虚拟地址 → 触发缺页异常(真正开始 “干活”)

进程拿到虚拟地址后,第一次读写时:

  1. CPU 访问该虚拟地址,查页表发现 “无物理地址绑定”,触发**缺页异常(Page Fault)**,CPU 暂停用户态执行,跳转到内核的缺页处理函数;
  2. 内核缺页处理函数分析异常原因:

    • 如果是 “文件映射且数据未加载”:内核从磁盘读取文件中 offset 开始的一页数据(Linux 页大小默认 4KB),写入**页缓存(Page Cache)**(这是内核管理的物理内存,所有进程共享);

    • 如果是 “匿名映射(MAP_ANONYMOUS)”:内核直接分配一块空闲物理页;

  3. 内核更新页表:将当前访问的虚拟地址,绑定到 “页缓存的物理地址”(文件映射)或 “新分配的物理地址”(匿名映射);

  4. 内核返回用户态,CPU 重新执行刚才触发异常的指令,此时能正常访问物理内存。 👉 关键:mmap 是**懒加载(Lazy Loading)** —— 只有真正访问时才分配物理内存、读取文件,避免提前占用资源。

阶段 3:MAP_PRIVATE 写操作 → 写时复制(COW)

如果是 MAP_PRIVATE(私有映射,如动态库),当进程修改映射区域时:

  1. 进程写入虚拟地址,触发缺页异常(此时物理页是共享的,且可能是只读);
  2. 内核检测到 “写操作 + MAP_PRIVATE”,执行**写时复制**:

    • 复制一份共享的物理页(页缓存),生成新的物理页;

    • 更新当前进程的页表,指向新的物理页;

    • 标记新物理页为 “可写”,原物理页仍被其他进程共享;

  3. 进程的写操作只作用于新的私有物理页,不会影响其他进程,也不会回写到磁盘文件。

阶段 4:munmap / 进程退出 → 释放映射

  1. 用户调用 munmap 或进程退出时,内核:

    • 销毁对应的 vm_area_struct 结构体;

    • 清空页表中该虚拟地址的映射关系;

    • 如果是 MAP_PRIVATE 且物理页是 COW 生成的,直接释放;

    • 如果是页缓存中的物理页,内核根据 “缓存策略” 决定是否保留(供其他进程复用)或刷回磁盘。

关键底层细节(新手易混淆)

  1. 虚拟地址不是物理地址mmap 返回的是虚拟地址,只有触发缺页异常后,才绑定物理地址;
  2. 页缓存是核心中间层mmap 读取文件时,数据先到页缓存,多个进程映射同一个文件时,共享页缓存,节省内存;
  3. MAP_SHARED 的写操作:如果是 MAP_SHARED(共享映射),写操作不会触发 COW,而是直接修改页缓存,内核会异步把修改刷回磁盘文件;
  4. 匿名映射(MAP_ANONYMOUS):没有文件参与,内核直接把虚拟地址映射到匿名物理页(如 malloc 底层会用匿名 mmap 分配大内存)。

总结

  1. mmap 底层核心是 “先建虚拟地址 - 文件的映射(页表),再通过缺页异常懒加载物理内存和文件数据”,全程围绕页表和缺页异常展开;
  2. 懒加载(缺页异常)和零拷贝(直接访问页缓存,无需类似read/write涉及内核缓冲区到用户缓冲区拷贝)是 mmap 高性能的关键;
  3. MAP_PRIVATE 依赖写时复制(COW)实现进程隔离,MAP_SHARED 则直接共享页缓存并同步到文件。

示例代码

  • case1:共享文件映射,虚拟内存修改也会修改磁盘文件本身
  • case2:共享匿名映射,经fork后子进程能继承父进程的映射关系进行实现父子进程共享匿名内存
  • case3:共享匿名映射(/dev/zero),经fork后,子进程能继承父进程的映射关系进行实现父子进程共享匿名内存
  • case4:私有文件映射,虚拟内存修改采用COW写时拷贝,不会将修改同步给文件,例如加载动态链接库的代码段只读不改,数据段各个进程写时拷贝;
  • case5:私有匿名映射,经fork后,子进程能继承父进程的映射关系,子进程修改后采用COW与父进程内存隔离
  • case6:私有匿名映射(/dev/zero),经fork后,子进程能继承父进程的映射关系,子进程修改后采用COW与父进程内存隔离
  • case7:不同进程通过同一个文件路径进行共享文件映射,修改能在进程间共享
  • case8:/dev/zero搭配MAP_SHARED无法实现进程共享,因为/dev/zero 是虚拟设备无存储,修改不会回写
映射类型 核心特征 核心使用场景
共享文件映射 关联文件 + 修改同步到文件 + 多进程共享 多进程共享文件数据、大文件高效读写、进程间通信
共享匿名映射 无文件关联 + 多进程共享内存 + 无持久化 亲缘进程(父子 / 兄弟)高效通信、共享临时数据
私有文件映射 关联文件 + 修改仅进程私有 + 不回写文件 加载动态库 / 可执行文件、只读文件访问、临时修改文件
私有匿名映射 无文件关联 + 进程私有内存 + 无持久化 进程私有内存分配(如 malloc 大内存)、临时数据存储
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include "lib.h"

static char SHM_FILEPATH[] = "/coding/example/test_shm_file";
static char SHM_INIT_CONTENT[] = "hello,world";

void write_file(char *expect_str)
{
    FILE *fp = fopen(SHM_FILEPATH, "w");
    if (fp == NULL) {
        perror("fopen fail");
        exit(EXIT_FAILURE);
    }

    if (fputs(expect_str, fp) == EOF) {
        perror("fputs fail");
        fclose(fp);
        exit(EXIT_FAILURE);
    }

    fclose(fp);
    return 0;
}

void read_file(char *expect_str)
{
    FILE *fp = fopen(SHM_FILEPATH, "r");
    if (fp == NULL) {
        perror("fopen fail");
        exit(EXIT_FAILURE);
    }

    #define MAX_LINE_LEN 1024
    char buffer[MAX_LINE_LEN];
    // fgets:读取一行到buffer,最多读MAX_LINE_LEN-1个字符(留1个存'\0')
    while (fgets(buffer, MAX_LINE_LEN, fp) != NULL) {
        printf("%s", buffer);  // 输出整行(fgets会保留换行符)
        if (expect_str != NULL && strncmp(expect_str, buffer, strlen(expect_str) != 0)) {
            printf("unexpected file content\n");
            exit(EXIT_FAILURE);
        }
    }
    printf("\n");

    fclose(fp);
    return 0;
}

/**
 * case1:共享文件映射,虚拟内存修改也会修改磁盘文件本身
 */
void mmap_shm_case1()
{
    // 文件内容初始化
    write_file(SHM_INIT_CONTENT);

    int fd = open(SHM_FILEPATH, O_RDWR);
    if (fd == -1) {  perror("open:"); return; }

    struct stat st = {};
    if (fstat(fd, &st) == -1) { perror("fstat:"); return; }

    // 基于文件fd,按MAP_SHARED创建虚拟内存映射
    char *addr = mmap(NULL, st.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) {
        perror("mmap:");
        return;
    }

    if (close(fd) == -1) { perror("close:"); return; }

    printf("before modify, size=%d, addr is %s\n", st.st_size, addr);

    static char str[] = "HELLO";
    memcpy(addr, str, strlen(str));
    printf("after modify, size=%d, addr is %s\n", st.st_size, addr);

    printf("read file is:\n");
    read_file("HELLO,world");

    // 恢复文件内容
    write_file(SHM_INIT_CONTENT);

    PRINT_SUCCESSFUL;
}

/**
 * case2:共享匿名映射,经fork后,子进程能继承父进程的映射关系进行实现父子进程共享匿名内存
 */
void mmap_shm_case2()
{
    char *addr = mmap(NULL, SHM_SIZE, PROT_READ|PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);
    if (addr == MAP_FAILED) {
        perror("mmap:");
        return;
    }
    memcpy(addr, SHM_INIT_CONTENT, strlen(SHM_INIT_CONTENT) + 1);

    printf("init, addr is %s\n", addr);

    if (fork() == 0) {
        // 子进程能继承和修改来自父进程的共享内存
        printf("child pid=%d before modify, addr is %s\n", getpid(), addr);
        if (strcmp(addr, "hello,world") != 0) {
            exit(EXIT_FAILURE);
        }

        static char str[] = "HELLO";
        memcpy(addr, str, strlen(str));
        printf("child pid=%d after modify, addr is %s\n", getpid(), addr);

        // 子进程终止
        exit(EXIT_SUCCESS);
    }

    // 父进程读取子进程的修改
    wait(NULL);
    sleep(3);
    printf("parent pid=%d, addr is %s\n", getpid(), addr);
    if (strcmp(addr, "HELLO,world") != 0) {
        exit(EXIT_FAILURE);
    }

    PRINT_SUCCESSFUL;
}

/**
 * case3:共享匿名映射(/dev/zero),经fork后,子进程能继承父进程的映射关系进行实现父子进程共享匿名内存
 */
void mmap_shm_case3()
{
    int fd = open("/dev/zero", O_RDWR);
    if (fd == -1) { perror("open"); return; }

    char *addr = mmap(NULL, SHM_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) {
        perror("mmap:");
        return;
    }

    if (close(fd) == -1) { perror("close"); return; };

    memcpy(addr, SHM_INIT_CONTENT, strlen(SHM_INIT_CONTENT) + 1);

    printf("init, addr is %s\n", addr);

    if (fork() == 0) {
        // 子进程能继承和修改来自父进程的共享内存
        printf("child pid=%d before modify, addr is %s\n", getpid(), addr);
        if (strcmp(addr, "hello,world") != 0) {
            exit(EXIT_FAILURE);
        }

        static char str[] = "HELLO";
        memcpy(addr, str, strlen(str));
        printf("child pid=%d after modify, addr is %s\n", getpid(), addr);

        // 子进程终止
        exit(EXIT_SUCCESS);
    }

    // 父进程读取子进程的修改
    wait(NULL);
    sleep(3);
    printf("parent pid=%d, addr is %s\n", getpid(), addr);
    if (strcmp(addr, "HELLO,world") != 0) {
        exit(EXIT_FAILURE);
    }

    PRINT_SUCCESSFUL;
}

/**
 * case4:私有文件映射,虚拟内存修改采用COW写时拷贝,不会将修改同步给文件
 */
void mmap_shm_case4()
{
    // 文件内容初始化
    write_file(SHM_INIT_CONTENT);

    int fd = open(SHM_FILEPATH, O_RDWR);
    if (fd == -1) {  perror("open:"); return; }

    struct stat st = {};
    if (fstat(fd, &st) == -1) { perror("fstat:"); return; }

    // 基于文件fd,按MAP_SHARED创建虚拟内存映射
    char *addr = mmap(NULL, st.st_size, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
    if (addr == MAP_FAILED) {
        perror("mmap:");
        return;
    }
    if (close(fd) == -1) { perror("close"); return; };
    printf("before modify, size=%d, addr is %s\n", st.st_size, addr);

    static char str[] = "HELLO";
    memcpy(addr, str, strlen(str));
    printf("after modify, size=%d, addr is %s\n", st.st_size, addr);

    printf("read file is:\n");
    read_file("hello,world");

    // 恢复文件内容
    write_file(SHM_INIT_CONTENT);

    PRINT_SUCCESSFUL;
}

/**
 * case5:私有匿名映射,经fork后,子进程能继承父进程的映射关系,子进程修改后采用COW与父进程内存隔离
 */
void mmap_shm_case5()
{
    char *addr = mmap(NULL, SHM_SIZE, PROT_READ|PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    if (addr == MAP_FAILED) {
        perror("mmap:");
        return;
    }
    memcpy(addr, SHM_INIT_CONTENT, strlen(SHM_INIT_CONTENT) + 1);

    printf("init, addr is %s\n", addr);

    if (fork() == 0) {
        // 子进程能继承和修改来自父进程的共享内存
        printf("child pid=%d before modify, addr is %s\n", getpid(), addr);
        if (strcmp(addr, "hello,world") != 0) {
            exit(EXIT_FAILURE);
        }

        static char str[] = "HELLO";
        memcpy(addr, str, strlen(str));
        printf("child pid=%d after modify, addr is %s\n", getpid(), addr);

        // 子进程终止
        exit(EXIT_SUCCESS);
    }

    // 父进程读取子进程的修改
    wait(NULL);
    sleep(3);
    printf("parent pid=%d, addr is %s\n", getpid(), addr);
    if (strcmp(addr, "hello,world") != 0) {
        exit(EXIT_FAILURE);
    }

    PRINT_SUCCESSFUL;
}

/**
 * case6:私有匿名映射(/dev/zero),经fork后,子进程能继承父进程的映射关系,子进程修改后采用COW与父进程内存隔离
 */
void mmap_shm_case6()
{
    int fd = open("/dev/zero", O_RDWR);
    if (fd == -1) { perror("open"); return; }

    char *addr = mmap(NULL, SHM_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
    if (addr == MAP_FAILED) {
        perror("mmap:");
        return;
    }
    if (close(fd) == -1) { perror("close"); return; };

    memcpy(addr, SHM_INIT_CONTENT, strlen(SHM_INIT_CONTENT) + 1);

    printf("init, addr is %s\n", addr);

    if (fork() == 0) {
        // 子进程能继承和修改来自父进程的共享内存
        printf("child pid=%d before modify, addr is %s\n", getpid(), addr);
        if (strcmp(addr, "hello,world") != 0) {
            exit(EXIT_FAILURE);
        }

        static char str[] = "HELLO";
        memcpy(addr, str, strlen(str));
        printf("child pid=%d after modify, addr is %s\n", getpid(), addr);

        // 子进程终止
        exit(EXIT_SUCCESS);
    }

    // 父进程读取子进程的修改
    wait(NULL);
    sleep(3);
    printf("parent pid=%d, addr is %s\n", getpid(), addr);
    if (strcmp(addr, "hello,world") != 0) {
        exit(EXIT_FAILURE);
    }

    PRINT_SUCCESSFUL;
}

/**
 * case7:不同进程通过同一个文件路径进行共享文件映射,修改能在进程间共享
 */
void mmap_shm_case7()
{
    // 文件内容初始化
    write_file(SHM_INIT_CONTENT);

    for (int i = 0; i < 3; i++) {
        if (fork() != 0) {
            continue;
        }
        int fd = open(SHM_FILEPATH, O_RDWR);
        if (fd == -1) {  perror("open:"); return; }

        struct stat st = {};
        if (fstat(fd, &st) == -1) { perror("fstat:"); return; }

        char *addr = mmap(NULL, st.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
        if (addr == MAP_FAILED) { perror("mmap:"); return;}
        if (close(fd) == -1) { perror("close:"); return; }

        sleep(i);
        printf("child i=%d, pid=%d, fd=%d, before modify addr is %s\n", i, fd, getpid(), addr);
        addr[0] = '0' + i;
        printf("child i=%d, pid=%d, fd=%d, after modify addr is %s\n", i, fd, getpid(), addr);

        // 子进程退出
        exit(EXIT_SUCCESS);
    }

    while (wait(NULL) != -1) {}

    printf("parent read file is:\n");
    read_file(NULL);

    // 恢复文件内容
    write_file(SHM_INIT_CONTENT);

    PRINT_SUCCESSFUL;
}

/**
 * case8:/dev/zero搭配MAP_SHARED无法实现进程共享,因为/dev/zero 是虚拟设备无存储,修改不会回写
 */
void mmap_shm_case8()
{
    for (int i = 0; i < 3; i++) {
        if (fork() != 0) {
            continue;
        }
        int fd = open("/dev/zero", O_RDWR);
        if (fd == -1) {  perror("open:"); return; }

        char *addr = mmap(NULL, SHM_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
        if (addr == MAP_FAILED) { perror("mmap:"); return;}
        if (close(fd) == -1) { perror("close:"); return; }

        sleep(i);
        printf("child i=%d, pid=%d, fd=%d, before modify addr is %s\n", i, fd, getpid(), addr);
        addr[0] = '0' + i;
        printf("child i=%d, pid=%d, fd=%d, after modify addr is %s\n", i, fd, getpid(), addr);

        // 子进程退出
        exit(EXIT_SUCCESS);
    }

    while (wait(NULL) != -1) {}

    PRINT_SUCCESSFUL;
}

int main(int argc, char *args[])
{
    mmap_shm_case1();
    mmap_shm_case2();
    mmap_shm_case3();
    mmap_shm_case4();
    mmap_shm_case5();
    mmap_shm_case6();
    mmap_shm_case7();
    mmap_shm_case8();
}

输出:

before modify, size=11, addr is hello,world
after modify, size=11, addr is HELLO,world
read file is:
HELLO,world
[mmap_shm_case1] successful...
--------------------------------
init, addr is hello,world
child pid=6340 before modify, addr is hello,world
child pid=6340 after modify, addr is HELLO,world
parent pid=6323, addr is HELLO,world
[mmap_shm_case2] successful...
--------------------------------
init, addr is hello,world
child pid=6350 before modify, addr is hello,world
child pid=6350 after modify, addr is HELLO,world
parent pid=6323, addr is HELLO,world
[mmap_shm_case3] successful...
--------------------------------
before modify, size=11, addr is hello,world
after modify, size=11, addr is HELLO,world
read file is:
hello,world
[mmap_shm_case4] successful...
--------------------------------
init, addr is hello,world
child pid=6400 before modify, addr is hello,world
child pid=6400 after modify, addr is HELLO,world
parent pid=6323, addr is hello,world
[mmap_shm_case5] successful...
--------------------------------
init, addr is hello,world
child pid=6405 before modify, addr is hello,world
child pid=6405 after modify, addr is HELLO,world
parent pid=6323, addr is hello,world
[mmap_shm_case6] successful...
--------------------------------
child i=0, pid=3, fd=6451, before modify addr is hello,world
child i=0, pid=3, fd=6451, after modify addr is 0ello,world
child i=1, pid=3, fd=6452, before modify addr is 0ello,world
child i=1, pid=3, fd=6452, after modify addr is 1ello,world
child i=2, pid=3, fd=6453, before modify addr is 1ello,world
child i=2, pid=3, fd=6453, after modify addr is 2ello,world
parent read file is:
2ello,world
[mmap_shm_case7] successful...
--------------------------------
child i=0, pid=3, fd=6458, before modify addr is 
child i=0, pid=3, fd=6458, after modify addr is 0
child i=1, pid=3, fd=6459, before modify addr is 
child i=1, pid=3, fd=6459, after modify addr is 1
child i=2, pid=3, fd=6460, before modify addr is 
child i=2, pid=3, fd=6460, after modify addr is 2
[mmap_shm_case8] successful...
--------------------------------

nginx中如何使用?

1、nginx提供了共享匿名映射,也是有master父子创建和管理映射,子进程直接继承映射,实现master以及不同worker之间的内存共享

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);
    }
}

2、nginx也提供了基于/dev/zero方式的共享映射,使用方式同共享匿名映射

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);
    }
}

POSIX共享内存

POSIX 共享内存对象用来在无关进程间共享一块内存区域而无需创建一个底层的磁盘文件。为创建 POSIX 共享内存对象需要使用 shm_open()调用来替换通常在 mmap()调用之前调用的 open()。shm_open()调用会在基于内存的文件系统中创建一个文件,并且可以使用传统的文件描述符系统调用在这个虚拟文件上执行各种操作。特别地,必须要使用 ftruncate()来设置共享内存对象的大小,因为其初始长度为零。

解决什么问题?

System V共享内存和共享文件映射,两种技术都可以实现不同进程共享内存区域,两者不足在于:

  • System V 共享内存模型使用的是键和标识符,这与标准的 UNIX I/O 模型使用文件名和描述符的做法是不一致的。这种差异意味着使用 System V 共享内存段需要一整套全新的系统调用和命令。
  • 使用一个共享文件映射来进行 IPC 要求创建一个磁盘文件,即使无需对共享区域进行持久存储也需要这样做。除了因需要创建文件所带来的不便之外,这种技术还会带来一些文件 I/O 开销。另外,使用共享匿名映射,由于无约定的全局标识,无法做到让无继承关系的进程实现进程间共享内存。

POSIX共享内存能够让无关进程共享一个映射区域而无需创建一个相应的映射文件。

要使用 POSIX 共享内存对象需要完成下列任务。

  1. 使用shm_open()函数打开一个与指定的名字对应的对象(需要遵循POSIX 共享内存对象的命名规则。)shm_open()函数与 open()系统调用类似,它会创建一个新共享对象或打开一个既有对象。作为函数结果,shm_open()会返回一个引用该对象的文件描述符。
  2. 将上一步中获得的文件描述符传入 mmap()调用并在其 flags 参数中指定 MAP_SHARED。这会将共享内存对象映射进进程的虚拟地址空间。与 mmap()的其他用法一样,一旦映射了对象之后就能够关闭该文件描述符而不会影响到这个映射。然而,有可能需要将这个文件描述符保持在打开状态以便后续的fstat()和ftruncate()调用使用这个文件描述符。

POSIX 共享内存上 shm_open()和 mmap()的关系类似于 System V 共享内存上 shmget()和shmat()的关系。使用 POSIX 共享内存对象需要两步式过程(shm_open()加上 mmap())而没有使用单个函数来执行两项任务是因为历史原因。在 POSIX 委员会增加这个特性时,mmap()调用已经存在了。实际上,这里所需要做的事情是使用shm_open()调用替换open()调用,其中的差别是使用 shm_open()无需在一个基于磁盘的文件系统上创建一个文件。由于共享内存对象的引用是通过文件描述符来完成的,因此可以直接使用 UNIX 系统中已经定义好的各种文件描述符系统调用(如 ftruncate())而无需增加新的用途特殊的系统调用(System V 共享内存就需要这样做)。

另外,一个新共享内存对象被创建时其初始长度会被设置为 0。这意味着在创建完一个新共享内存对象之后通常在调用 mmap()之前需要调用 ftruncate()来设置对象的大小。

示例代码

#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include "lib.h"

static char posix_shm_name[] = "/shm_demo";
static char SHM_INIT_CONTENT[] = "hello,world";

/**
 * case1:不同进程通过shm_open创建共享内存对象并获取对应fd进行共享映射,修改能在进程间共享
 */
void posix_shm_case1()
{
    for (int i = 0; i < 3; i++) {
        if (fork() != 0) {
            continue;
        }
        int flag = O_RDWR;
        if (i == 0) {
            // shm_open 不会自动创建文件,必须显式指定 O_CREAT 才能新建
            flag |= O_CREAT;
        }
        int fd = shm_open(posix_shm_name, flag, 0);
        if (fd == -1) {  perror("shm_open:"); return; }

        if (i == 0) {
            if (ftruncate(fd, SHM_SIZE) == -1) {
                perror("ftruncate:"); return;
            }
            write(fd, SHM_INIT_CONTENT, strlen(SHM_INIT_CONTENT) + 1);
        }

        struct stat st = {};
        if (fstat(fd, &st) == -1) { perror("fstat:"); return; }

        char *addr = mmap(NULL, st.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
        if (addr == MAP_FAILED) { perror("mmap:"); return;}
        if (close(fd) == -1) { perror("close:"); return; }

        sleep(i);
        printf("child i=%d, pid=%d, fd=%d, before modify addr is %s\n", i, fd, getpid(), addr);
        addr[0] = '0' + i;
        printf("child i=%d, pid=%d, fd=%d, after modify addr is %s\n", i, fd, getpid(), addr);

        // 子进程退出
        exit(EXIT_SUCCESS);
    }

    while (wait(NULL) != -1) {}

    PRINT_SUCCESSFUL;
}

int main(int argc, char *args[])
{
    posix_shm_case1();
}

输出:

child i=0, pid=3, fd=6185, before modify addr is hello,world
child i=0, pid=3, fd=6185, after modify addr is 0ello,world
child i=1, pid=3, fd=6186, before modify addr is 0ello,world
child i=1, pid=3, fd=6186, after modify addr is 1ello,world
child i=2, pid=3, fd=6187, before modify addr is 1ello,world
child i=2, pid=3, fd=6187, after modify addr is 2ello,world
[posix_shm_case1] successful...