MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言共享内存分配与释放

2021-07-105.2k 阅读

共享内存概述

在Linux系统中,进程间通信(IPC,Inter - Process Communication)是一个重要的话题。共享内存作为一种高效的进程间通信机制,允许不同的进程访问同一块物理内存区域,从而实现数据的快速共享。与其他IPC机制(如管道、消息队列等)相比,共享内存不需要在内核和用户空间之间频繁地复制数据,大大提高了通信效率。

共享内存的原理

共享内存的核心原理基于操作系统的虚拟内存管理机制。每个进程在运行时都有自己独立的虚拟地址空间,这些虚拟地址空间映射到实际的物理内存。当创建共享内存时,系统会在物理内存中分配一块区域,并为这块区域创建一个共享的虚拟内存映射。多个进程可以将这块共享的虚拟内存映射到各自的虚拟地址空间中,这样它们就可以通过访问自己虚拟地址空间中的对应区域来共享数据。

共享内存的优势

  1. 高效性:由于共享内存直接在物理内存上进行数据操作,避免了数据在用户空间和内核空间之间的频繁拷贝,因此在进程间传递大量数据时,共享内存的效率远高于其他IPC机制。
  2. 灵活性:共享内存可以存储任何类型的数据,无论是简单的变量还是复杂的数据结构,只要进程能够正确地解析和操作这些数据。

共享内存的劣势

  1. 同步问题:因为多个进程可以同时访问共享内存,所以需要额外的同步机制(如信号量、互斥锁等)来确保数据的一致性和完整性。如果没有正确的同步,可能会导致数据竞争和不一致的问题。
  2. 内存管理复杂:共享内存的分配和释放需要程序员手动处理,并且要确保在所有使用共享内存的进程都不再使用时才能正确释放,否则可能会导致内存泄漏。

共享内存相关系统调用

在Linux C语言编程中,主要使用以下几个系统调用来实现共享内存的分配、映射、访问和释放。

shmget函数

函数原型

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

参数说明

  • key:是一个键值,用于标识共享内存对象。可以使用ftok函数生成一个唯一的键值,也可以使用IPC_PRIVATE创建一个只在当前进程及其子进程间共享的共享内存对象。
  • size:指定共享内存的大小,单位是字节。
  • shmflg:是一组标志位,用于指定共享内存的访问权限和创建方式等。例如,IPC_CREAT表示如果共享内存不存在则创建它,IPC_EXCLIPC_CREAT一起使用时表示如果共享内存已存在则返回错误。常见的访问权限标志有0666(表示可读可写)等。

返回值: 成功时返回共享内存标识符(一个非负整数),失败时返回 -1,并设置errno以指示错误原因。

shmat函数

函数原型

#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);

参数说明

  • shmid:是shmget函数返回的共享内存标识符。
  • shmaddr:指定共享内存映射到进程虚拟地址空间的地址。通常设为NULL,表示让系统自动选择一个合适的地址。
  • shmflg:标志位,用于指定共享内存的访问模式。例如,SHM_RDONLY表示以只读方式映射,0表示以读写方式映射。

返回值: 成功时返回指向共享内存区域的指针,失败时返回(void *)-1,并设置errno

shmdt函数

函数原型

#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);

参数说明

  • shmaddr:是shmat函数返回的指向共享内存区域的指针。

返回值: 成功时返回0,失败时返回 -1,并设置errno

shmctl函数

函数原型

#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数说明

  • shmid:共享内存标识符。
  • cmd:指定对共享内存执行的操作。常见的操作有IPC_STAT(获取共享内存的状态信息)、IPC_SET(设置共享内存的状态信息)、IPC_RMID(删除共享内存对象)。
  • buf:是一个指向struct shmid_ds结构体的指针,用于传递或接收共享内存的状态信息。当cmdIPC_STAT时,系统将共享内存的状态信息填充到buf指向的结构体中;当cmdIPC_SET时,系统将buf中的信息设置到共享内存对象中。

返回值: 成功时返回0,失败时返回 -1,并设置errno

共享内存分配示例代码

下面通过一个简单的示例代码来演示如何在Linux C语言中分配共享内存。

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

#define SHM_SIZE 1024

int main() {
    key_t key;
    int shmid;
    char *shmptr;

    // 生成一个唯一的键值
    key = ftok(".", 'a');
    if (key == -1) {
        perror("ftok");
        exit(1);
    }

    // 创建共享内存对象
    shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    // 将共享内存映射到进程的虚拟地址空间
    shmptr = (char *)shmat(shmid, NULL, 0);
    if (shmptr == (void *)-1) {
        perror("shmat");
        exit(1);
    }

    // 使用共享内存,这里简单地写入一些数据
    sprintf(shmptr, "Hello, shared memory!");

    printf("Data written to shared memory: %s\n", shmptr);

    // 分离共享内存
    if (shmdt(shmptr) == -1) {
        perror("shmdt");
        exit(1);
    }

    // 删除共享内存对象
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl");
        exit(1);
    }

    return 0;
}

在上述代码中:

  1. 首先使用ftok函数生成一个唯一的键值keyftok函数的第一个参数是一个存在的文件路径,第二个参数是一个字符,通过这两个参数生成一个键值。
  2. 然后使用shmget函数创建一个大小为SHM_SIZE(这里设为1024字节)的共享内存对象,IPC_CREAT标志表示如果共享内存不存在则创建它,权限设置为0666(可读可写)。
  3. 使用shmat函数将共享内存映射到进程的虚拟地址空间,得到一个指向共享内存区域的指针shmptr
  4. 接着通过shmptr指针在共享内存中写入数据,并打印出写入的数据。
  5. 使用shmdt函数分离共享内存,即取消共享内存与进程虚拟地址空间的映射。
  6. 最后使用shmctl函数并指定cmdIPC_RMID来删除共享内存对象。

多进程共享内存示例

前面的示例只是在单个进程中使用共享内存,下面展示如何在多个进程(父子进程)之间共享内存。

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

#define SHM_SIZE 1024

int main() {
    key_t key;
    int shmid;
    char *shmptr;
    pid_t pid;

    // 生成一个唯一的键值
    key = ftok(".", 'a');
    if (key == -1) {
        perror("ftok");
        exit(1);
    }

    // 创建共享内存对象
    shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    // 将共享内存映射到进程的虚拟地址空间
    shmptr = (char *)shmat(shmid, NULL, 0);
    if (shmptr == (void *)-1) {
        perror("shmat");
        exit(1);
    }

    // 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(1);
    } else if (pid == 0) {
        // 子进程
        sprintf(shmptr, "Data from child process");
        printf("Child process has written: %s\n", shmptr);
    } else {
        // 父进程
        wait(NULL);
        printf("Parent process reads: %s\n", shmptr);
    }

    // 分离共享内存
    if (shmdt(shmptr) == -1) {
        perror("shmdt");
        exit(1);
    }

    // 删除共享内存对象
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl");
        exit(1);
    }

    return 0;
}

在这个示例中:

  1. 同样先生成键值、创建共享内存对象并映射到进程虚拟地址空间。
  2. 使用fork函数创建一个子进程。
  3. 子进程在共享内存中写入数据,并打印写入的内容。
  4. 父进程通过wait函数等待子进程结束,然后读取共享内存中的数据并打印。
  5. 最后父子进程都分离共享内存并删除共享内存对象。

共享内存释放的注意事项

  1. 确保所有进程都已分离:在删除共享内存对象之前,必须确保所有使用该共享内存的进程都已经调用shmdt函数将其从虚拟地址空间中分离。否则,可能会导致未定义行为,并且共享内存对象无法被正确释放,从而造成内存泄漏。
  2. 同步删除操作:如果有多个进程可能会删除共享内存对象,需要使用同步机制(如信号量)来确保只有一个进程执行删除操作。否则,可能会出现重复删除或在其他进程仍在使用时就删除的情况。
  3. 异常处理:在调用shmdtshmctl(用于删除共享内存)函数时,要正确处理可能出现的错误。例如,shmdt函数可能因为共享内存已被其他进程锁定等原因而失败,shmctl函数可能因为权限不足等原因而失败。在错误发生时,应根据errno的值进行适当的处理,如记录错误日志、进行重试或采取其他补救措施。

共享内存与同步机制结合使用

如前文所述,共享内存本身没有提供同步机制,为了确保多个进程对共享内存的访问是安全和一致的,需要结合其他同步机制,如信号量。

信号量概述

信号量是一个整型变量,它通过一个计数器来控制对共享资源的访问。当一个进程想要访问共享资源时,它需要先获取信号量(将计数器减1)。如果计数器的值大于0,说明有可用的资源,进程可以获取信号量并访问共享资源;如果计数器的值为0,说明资源已被占用,进程需要等待,直到信号量的值大于0。当进程使用完共享资源后,它需要释放信号量(将计数器加1)。

使用信号量与共享内存示例

下面的示例展示了如何结合共享内存和信号量来实现多进程安全地访问共享内存。

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <unistd.h>

#define SHM_SIZE 1024
#define SEM_KEY 1234

// 信号量操作函数
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

void semaphore_p(int semid) {
    struct sembuf sem_op;
    sem_op.sem_num = 0;
    sem_op.sem_op = -1;
    sem_op.sem_flg = 0;
    if (semop(semid, &sem_op, 1) == -1) {
        perror("semaphore_p");
        exit(1);
    }
}

void semaphore_v(int semid) {
    struct sembuf sem_op;
    sem_op.sem_num = 0;
    sem_op.sem_op = 1;
    sem_op.sem_flg = 0;
    if (semop(semid, &sem_op, 1) == -1) {
        perror("semaphore_v");
        exit(1);
    }
}

int main() {
    key_t key;
    int shmid, semid;
    char *shmptr;
    pid_t pid;

    // 生成共享内存键值
    key = ftok(".", 'a');
    if (key == -1) {
        perror("ftok for shm");
        exit(1);
    }

    // 创建共享内存对象
    shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    // 将共享内存映射到进程的虚拟地址空间
    shmptr = (char *)shmat(shmid, NULL, 0);
    if (shmptr == (void *)-1) {
        perror("shmat");
        exit(1);
    }

    // 创建信号量
    semid = semget(SEM_KEY, 1, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("semget");
        exit(1);
    }

    // 初始化信号量
    union semun sem_set;
    sem_set.val = 1;
    if (semctl(semid, 0, SETVAL, sem_set) == -1) {
        perror("semctl SETVAL");
        exit(1);
    }

    // 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(1);
    } else if (pid == 0) {
        // 子进程
        semaphore_p(semid);
        sprintf(shmptr, "Data from child process");
        printf("Child process has written: %s\n", shmptr);
        semaphore_v(semid);
    } else {
        // 父进程
        wait(NULL);
        semaphore_p(semid);
        printf("Parent process reads: %s\n", shmptr);
        semaphore_v(semid);
    }

    // 分离共享内存
    if (shmdt(shmptr) == -1) {
        perror("shmdt");
        exit(1);
    }

    // 删除共享内存对象
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl");
        exit(1);
    }

    // 删除信号量
    if (semctl(semid, 0, IPC_RMID) == -1) {
        perror("semctl IPC_RMID");
        exit(1);
    }

    return 0;
}

在这个示例中:

  1. 除了共享内存相关的操作外,还使用semget函数创建了一个信号量,键值为SEM_KEY
  2. 使用semctl函数并结合union semun结构体对信号量进行初始化,将其值设为1,表示有一个可用的资源。
  3. 在子进程和父进程访问共享内存之前,先调用semaphore_p函数获取信号量(将信号量的值减1),确保在同一时间只有一个进程可以访问共享内存。
  4. 在访问完共享内存后,调用semaphore_v函数释放信号量(将信号量的值加1)。
  5. 最后在程序结束时,不仅删除了共享内存对象,还删除了信号量。

共享内存的实际应用场景

  1. 高性能计算:在并行计算中,多个计算节点可能需要共享大量的数据,如矩阵、数据集等。共享内存可以提供高效的数据共享方式,减少数据传输的开销,提高计算效率。
  2. 数据库系统:数据库管理系统中,不同的进程(如查询处理进程、事务管理进程等)可能需要共享一些数据结构,如索引、缓冲区等。共享内存可以使这些进程快速地访问和修改共享数据,提高数据库的性能。
  3. 多媒体处理:在多媒体应用中,如视频编解码、音频处理等,不同的模块可能需要共享一些缓冲区来存储媒体数据。共享内存可以满足这种高效的数据共享需求,确保数据的快速传输和处理。

通过以上详细的介绍和示例代码,相信你对Linux C语言中共享内存的分配与释放以及相关的同步机制有了更深入的理解。在实际应用中,需要根据具体的需求和场景,合理地使用共享内存,并结合适当的同步机制,以实现高效、稳定的进程间通信。