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

Linux C语言共享内存基础与应用

2023-03-291.3k 阅读

共享内存概述

在Linux系统的进程间通信(IPC,Inter - Process Communication)机制中,共享内存是一种高效的数据共享方式。与其他IPC机制如管道(Pipe)、消息队列(Message Queue)相比,共享内存允许不同的进程直接访问同一块物理内存区域,这避免了数据在进程间复制的开销,大大提高了数据传输的效率。

从操作系统原理角度来看,每个进程在内存中都有自己独立的虚拟地址空间。共享内存的实现原理是,操作系统将同一块物理内存映射到多个进程的虚拟地址空间中。这样,不同进程通过访问各自虚拟地址空间中的这块映射区域,就可以实现数据的共享。

Linux下共享内存相关函数

shmget函数

  1. 函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
  1. 参数说明
    • key:是一个key_t类型的键值,它是共享内存对象的标识。通常可以使用ftok函数生成一个唯一的key值。如果keyIPC_PRIVATE,则会创建一个新的、私有的共享内存对象,只有创建者及其子进程可以访问。
    • size:指定共享内存的大小,单位是字节。这个大小必须是系统页面大小的整数倍。
    • shmflg:是一组标志位,用于指定共享内存的创建和访问权限。常见的标志位有IPC_CREAT(如果共享内存不存在则创建)、IPC_EXCL(与IPC_CREAT一起使用,确保共享内存是新建的,如果已存在则返回错误),以及文件访问权限标志(如0666表示读写权限)。
  2. 返回值: 成功时返回共享内存标识符(一个非负整数),失败时返回 -1,并设置errno以指示错误原因。

shmat函数

  1. 函数原型
#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);
  1. 参数说明
    • shmid:是由shmget函数返回的共享内存标识符。
    • shmaddr:指定共享内存应映射到调用进程虚拟地址空间中的地址。如果为NULL,系统会自动选择一个合适的地址进行映射。
    • shmflg:标志位,常见的有SHM_RDONLY(以只读方式映射共享内存),如果为0则以读写方式映射。
  2. 返回值: 成功时返回指向共享内存映射区域的指针,失败时返回(void *)-1,并设置errno

shmdt函数

  1. 函数原型
#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);
  1. 参数说明shmaddr是由shmat函数返回的指向共享内存映射区域的指针。
  2. 返回值: 成功时返回0,失败时返回 -1,并设置errno

shmctl函数

  1. 函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  1. 参数说明
    • shmid:共享内存标识符。
    • cmd:是一个命令码,用于指定对共享内存对象执行的操作。常见的命令码有IPC_STAT(获取共享内存对象的状态信息,存储在buf指向的结构中)、IPC_SET(设置共享内存对象的状态信息,从buf指向的结构中获取)、IPC_RMID(删除共享内存对象)。
    • buf:是一个指向struct shmid_ds结构的指针,用于存储或传递共享内存对象的状态信息。
  2. 返回值: 成功时返回0,失败时返回 -1,并设置errno

共享内存应用示例

下面通过一个简单的生产者 - 消费者模型示例来展示共享内存的使用。

代码示例

  1. 生产者代码(producer.c)
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

#define SHM_SIZE 1024

int main() {
    key_t key;
    int shmid;
    char *shm, *s;

    // 创建一个唯一的key
    if ((key = ftok(".", 'a')) == -1) {
        perror("ftok");
        exit(1);
    }

    // 获取共享内存标识符,如果不存在则创建
    if ((shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666)) == -1) {
        perror("shmget");
        exit(1);
    }

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

    s = shm;
    // 向共享内存中写入数据
    strcpy(s, "Hello, Consumer!");

    // 等待消费者读取数据
    while (*shm != '*') {
        sleep(1);
    }

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

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

    return 0;
}
  1. 消费者代码(consumer.c)
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

#define SHM_SIZE 1024

int main() {
    key_t key;
    int shmid;
    char *shm, *s;

    // 创建与生产者相同的key
    if ((key = ftok(".", 'a')) == -1) {
        perror("ftok");
        exit(1);
    }

    // 获取共享内存标识符
    if ((shmid = shmget(key, SHM_SIZE, 0666)) == -1) {
        perror("shmget");
        exit(1);
    }

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

    s = shm;
    // 等待生产者写入数据
    while (*s == '\0') {
        sleep(1);
    }

    // 读取共享内存中的数据
    printf("Received: %s\n", s);

    // 标记数据已读取
    *shm = '*';

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

    return 0;
}

代码说明

  1. 生产者部分

    • 使用ftok函数生成一个唯一的key值,ftok函数的第一个参数是一个存在的文件路径,第二个参数是一个字符,通过这两个参数生成一个唯一的key
    • 调用shmget函数获取共享内存标识符,如果共享内存不存在则创建一个大小为SHM_SIZE(这里定义为1024字节)、权限为0666(读写权限)的共享内存对象。
    • 使用shmat函数将共享内存映射到进程的虚拟地址空间,返回一个指向共享内存区域的指针shm
    • 向共享内存中写入字符串"Hello, Consumer!"
    • 等待消费者标记数据已读取(通过共享内存中的一个字符*来标识)。
    • 最后,调用shmdt函数分离共享内存,调用shmctl函数删除共享内存对象。
  2. 消费者部分

    • 生成与生产者相同的key值。
    • 调用shmget函数获取已存在的共享内存标识符。
    • 使用shmat函数将共享内存映射到进程的虚拟地址空间。
    • 等待生产者写入数据(共享内存中第一个字符不为\0)。
    • 读取共享内存中的数据并打印。
    • 标记数据已读取,将共享内存中的第一个字符设为*
    • 调用shmdt函数分离共享内存。

共享内存使用中的同步问题

虽然共享内存提供了高效的数据共享方式,但在多进程并发访问共享内存时,同步问题变得至关重要。如果多个进程同时对共享内存进行读写操作,可能会导致数据不一致或竞争条件(Race Condition)。

为了解决这些问题,通常可以结合其他同步机制,如信号量(Semaphore)。信号量是一个计数器,它可以用来控制对共享资源的访问。例如,在上述生产者 - 消费者模型中,可以引入两个信号量:一个用于表示共享内存中有数据可供消费(empty信号量),另一个用于表示共享内存中有空间可供生产(full信号量)。

  1. 引入信号量后的生产者代码(producer_with_sem.c)
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <semaphore.h>

#define SHM_SIZE 1024

int main() {
    key_t key;
    int shmid;
    char *shm, *s;
    sem_t *empty, *full;

    // 创建一个唯一的key
    if ((key = ftok(".", 'a')) == -1) {
        perror("ftok");
        exit(1);
    }

    // 获取共享内存标识符,如果不存在则创建
    if ((shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666)) == -1) {
        perror("shmget");
        exit(1);
    }

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

    // 打开或创建信号量
    if ((empty = sem_open("/empty", O_CREAT, 0666, 1)) == SEM_FAILED) {
        perror("sem_open empty");
        exit(1);
    }
    if ((full = sem_open("/full", O_CREAT, 0666, 0)) == SEM_FAILED) {
        perror("sem_open full");
        exit(1);
    }

    s = shm;
    // 等待共享内存有空间可供生产
    sem_wait(empty);
    // 向共享内存中写入数据
    strcpy(s, "Hello, Consumer!");
    // 标记共享内存中有数据可供消费
    sem_post(full);

    // 等待消费者读取数据
    sem_wait(empty);

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

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

    // 关闭并删除信号量
    sem_close(empty);
    sem_close(full);
    sem_unlink("/empty");
    sem_unlink("/full");

    return 0;
}
  1. 引入信号量后的消费者代码(consumer_with_sem.c)
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <semaphore.h>

#define SHM_SIZE 1024

int main() {
    key_t key;
    int shmid;
    char *shm, *s;
    sem_t *empty, *full;

    // 创建与生产者相同的key
    if ((key = ftok(".", 'a')) == -1) {
        perror("ftok");
        exit(1);
    }

    // 获取共享内存标识符
    if ((shmid = shmget(key, SHM_SIZE, 0666)) == -1) {
        perror("shmget");
        exit(1);
    }

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

    // 打开信号量
    if ((empty = sem_open("/empty", 0)) == SEM_FAILED) {
        perror("sem_open empty");
        exit(1);
    }
    if ((full = sem_open("/full", 0)) == SEM_FAILED) {
        perror("sem_open full");
        exit(1);
    }

    s = shm;
    // 等待共享内存中有数据可供消费
    sem_wait(full);
    // 读取共享内存中的数据
    printf("Received: %s\n", s);
    // 标记共享内存有空间可供生产
    sem_post(empty);

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

    // 关闭信号量
    sem_close(empty);
    sem_close(full);

    return 0;
}

共享内存的优缺点

优点

  1. 高效性:共享内存直接在进程间共享物理内存,避免了数据在进程间的复制,大大提高了数据传输的速度。这使得它在对性能要求较高的场景,如大数据量的实时处理中非常适用。
  2. 灵活性:共享内存的大小可以根据实际需求进行调整,并且可以被多个进程同时访问,适用于多种复杂的进程间通信场景。

缺点

  1. 同步复杂性:由于多个进程可以同时访问共享内存,必须使用额外的同步机制(如信号量、互斥锁等)来保证数据的一致性和避免竞争条件。这增加了编程的复杂性。
  2. 缺乏保护机制:共享内存本身没有提供对数据访问的保护机制。如果一个进程意外崩溃或错误地修改了共享内存中的数据,可能会影响到其他依赖该共享内存的进程,甚至导致系统不稳定。

共享内存的实际应用场景

  1. 数据库系统:数据库管理系统(DBMS)经常使用共享内存来缓存数据页和索引信息。多个数据库进程可以同时访问这些共享内存区域,提高数据查询和更新的效率。例如,在MySQL数据库中,InnoDB存储引擎使用共享内存来管理缓冲池(Buffer Pool),缓存经常访问的数据页,减少磁盘I/O操作。
  2. 多媒体处理:在视频流处理、音频编码等多媒体应用中,不同的处理模块(如视频解码模块、图像渲染模块)可能需要共享一些数据,如解码后的视频帧数据。共享内存可以高效地在这些模块之间传递数据,满足多媒体处理对实时性的要求。
  3. 分布式系统:在分布式计算环境中,不同节点上的进程可能需要共享一些配置信息或中间计算结果。通过共享内存和网络通信相结合的方式,可以实现分布式系统中进程间的数据共享和协同工作。例如,Hadoop分布式文件系统(HDFS)的一些组件可能会使用共享内存来提高数据处理的效率。

在实际应用中,开发人员需要根据具体的需求和场景,权衡共享内存的优缺点,合理地使用共享内存以及相关的同步机制,以实现高效、稳定的进程间通信。同时,要注意对共享内存的管理和保护,确保系统的可靠性和安全性。