Skip to content

共享内存

约 1155 个字 609 行代码 预计阅读时间 11 分钟

参考链接:
- 进程间通信---SystemV共享内存 - IPC_PRIVATE 的含义和作用

共享内存是==一种高效的进程间通信(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 是 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();
}

五、nginx中如何使用System V共享内存? 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);
    }
}

POSIX标准