【进程与线程】共享内存(Shared Memory)

IPC对象的查看: ipcs -a

共享内存:
    1> 共享内存是内核实际存在的一块物理内存  
    2> 共享内存是通信效率最高的通信方式 
    3> 共享内存的读操作主动不会阻塞(共享内存的数据不会主动消失,bzero清空后,读操作也会阻塞); 
    
    -------------------------------------
    key        shmid      owner      perms      bytes      nattch               status 
    键值        id号       拥有者    权限       大小       当前使用的进程数       状态

System V 共享内存

共享内存(Shared Memory)是 System V IPC 机制中效率最高的进程间通信方式,允许多个进程直接访问同一块物理内存区域。

共享内存是内核中的一块物理内存,它由操作系统管理,可以被多个进程访问。共享内存是效率最高的通信方式,因为进程直接读写内存,无需系统调用或数据拷贝。

System V共享内存的基本概念和操作步骤:共享内存的创建通常使用 shmget 函数,连接使用 shmat,分离使用 shmdt,控制使用 shmctl。本文解析了这些函数的作用与函数和使用场景;以及共享内存的特性,比如生命周期、同步问题,以及为什么读操作不会阻塞,但清空后可能阻塞。

共享内存的数据不会主动消失,这意味着数据会一直保留直到被显式删除或系统重启,但使用bzero清空共享内存后,读操作可能阻塞,这似乎矛盾。因为bzero的作用只是将内存区域置零,但共享内存本身仍然存在。阻塞的原因则是因为进程在读取时没有数据可读,但共享内存本身并不管理数据的可用性,需要额外的同步机制,比如信号量,来协调读写操作。

共享内存是通信效率最高的通信方式,因为数据不需要在进程间复制;但其缺点在于需要手动同步,容易导致竞争条件。并且,共享内存的生命周期独立于进程,需要显式删除,否则会一直存在,可能导致资源泄漏 ——> 同步机制与异步机制不同,在使用共享内存的过程中尤其需要注意这一点,比如需要使用信号量或互斥锁来避免数据不一致。

共享内存适合大量数据交换且对速度要求高的应用,比如图像处理、科学计算等。共享内存的读操作通常不会阻塞,因为数据一旦写入就存在,但如果没有同步机制,读进程可能读到不完整的数据,或者写进程覆盖数据导致问题。

一、共享内存的核心特点

共享内存

  1. 内核管理的物理内存
    • 共享内存是内核分配的一块真实物理内存,通过系统调用映射到进程的虚拟地址空间。
    • 生命周期独立于进程:即使创建它的进程终止,共享内存仍存在,需显式删除(shmctl(..., IPC_RMID))。
  2. 通信效率最高
    • 数据直接在内存中读写,无需内核中转或用户态与内核态的数据拷贝
    • 适用于高频、大数据量的通信场景(如视频处理、科学计算)。
  3. 读写操作的阻塞特性
    • 默认不阻塞:读操作直接访问内存数据,不会因数据未就绪而阻塞。
    • 清空后的阻塞:若共享内存被清空(如 bzero()),读操作可能因无有效数据而需同步机制(如信号量)协调。

二、共享内存的操作步骤

使用共享内存区从服务器拷贝文件数据至客户端

  1. 创建或获取共享内存段

使用 shmget() 创建或获取共享内存标识符:

#include <sys/ipc.h>
#include <sys/shm.h>

key_t key = ftok("/tmp", 'A');  // 生成唯一键值
int shmid = shmget(key, size, IPC_CREAT | 0666);
  • key:唯一标识共享内存的键值(通过 ftok() 生成)。
  • size:共享内存大小(字节)。
  • shmflg:权限标志(如 IPC_CREAT | 0666)。
  1. 连接共享内存到进程地址空间

使用 shmat() 将共享内存映射到进程的虚拟地址空间:

char *shm_ptr = shmat(shmid, NULL, 0);
  • shmid:共享内存标识符。
  • shmaddr:映射地址(通常设为 NULL,由内核自动选择)。
  • shmflg:标志(如 SHM_RDONLY 只读模式)。
  1. 读写共享内存

直接通过指针操作内存:

// 写入数据
sprintf(shm_ptr, "Hello Shared Memory");

// 读取数据
printf("Data: %s\n", shm_ptr);
  1. 分离共享内存

使用 shmdt() 断开进程与共享内存的连接:

shmdt(shm_ptr);  // 分离共享内存
  1. 删除共享内存

使用 shmctl() 控制共享内存(如删除):

shmctl(shmid, IPC_RMID, NULL);  // 标记删除,当所有进程分离后释放

三、共享内存的同步问题

由于共享内存不提供内置同步机制需结合其他 IPC(如信号量)避免竞态条件

使用信号量同步:

#include <sys/sem.h>

// 创建信号量
int semid = semget(key, 1, IPC_CREAT | 0666);
semctl(semid, 0, SETVAL, 1);  // 初始值为1(互斥锁)

struct sembuf op;
op.sem_num = 0;
op.sem_op = -1;  // P操作(获取资源)
op.sem_flg = 0;
semop(semid, &op, 1);

// 临界区:操作共享内存
sprintf(shm_ptr, "Data with semaphore");

op.sem_op = 1;  // V操作(释放资源)
semop(semid, &op, 1);

四、共享内存的底层原理

  1. 内核数据结构
    • struct shmid_kernel:内核维护每个共享内存段的信息,包括权限、大小、连接数等。
    • 页表映射:共享内存被映射到进程的虚拟地址空间,通过页表指向同一物理内存。
  2. 读写操作的原子性
    • 共享内存本身不保证原子性,需用户自行同步。
    • 单次内存访问(如 int 读写)通常是原子的,但复杂操作(如结构体更新)需加锁。
  3. 清空内存与阻塞
    • bzero(shm_ptr, size):仅将内存数据清零,不改变共享内存的可用性
    • 若读操作依赖数据有效性(如等待非零值),需通过同步机制(如信号量)实现阻塞等待。
优点缺点
无数据拷贝,速度极快需手动同步(易引发竞态条件)
适合大数据量通信生命周期需显式管理
多进程可并发访问数据无内置结构(需约定格式)

代码示例:进程间通信

写进程(writer.c)
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
    //1- ftok()获得键值
    key_t key = ftok("/",0x2);
    if(key == -1)
    {
        perror("ftok");
        return -1;
    }       
    //2- 获得ID号
    int shmid = shmget(key,1024,IPC_CREAT | 0666);  //通过上一步获得键值得到共享内存的ID号
    if(shmid == -1)
    {
        perror("shmget");
        return -1;
    }     
     //3- 映射地址
    char* addr =  (char *)shmat(shmid,NULL,0);     //利用上一步获得的键值得到共享内存的映射地址
    if(addr ==  (void *)-1)
    {
        perror("shamt");
        return -1;
    }
    bzero(addr,1024);
        /* 向共享内存中写入数据 */
    while(1)
    {
            scanf("%s",addr);   //从键盘读取数据写入共享内存
            if(strncmp(addr,"quit",4) == 0)
                break;
    }
     //4- 解除映射
    if(shmdt(addr) == -1)
    {
            perror("shmat");
            return -1;
    } 
     
}
读进程(reader.c)
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
    //1- ftok()获得键值
    key_t key = ftok("/",0x2);
    if(key == -1)
    {
        perror("ftok");
        return -1;
    }    
    //2- 获得ID号
    int shmid = shmget(key,1024,IPC_CREAT | 0666);  //通过上一步获得键值得到共享内存的ID号
    if(shmid == -1)
    {
        perror("shmget");
        return -1;
    }    
    //3- 映射地址
    char* addr =  (char *)shmat(shmid,NULL,0);     //利用上一步获得的键值得到共享内存的映射地址
    if((addr ==  (void *)-1))
    {
        perror("shamt");
        return -1;
    }
        /* 读取数据 */
    while(1)
    {
            printf("%s",addr);
            fflush(stdout);
            
            
            if(strncmp(addr,"quit",4) == 0)
                    break;
            bzero(addr,1024);
            sleep(1);
            
    }     
     //4- 解除映射
    if(shmdt(addr) == -1)
    {
            perror("shmat");
            return -1;
    } 
    
}

运行步骤:

  1. 编译并运行写进程:
gcc writer.c -o writer
./writer
  1. 编译并运行读进程:
gcc reader.c -o reader
./reader

简而言之,重点记住共享内存是最高效的 IPC,适用于高性能数据共享;并且,必须结合同步机制(如信号量)避免数据竞争;生命周期管理关键:显式创建、分离和删除。

以上。仅供学习与分享交流,请勿用于商业用途!转载需提前说明。

我是一个十分热爱技术的程序员,希望这篇文章能够对您有帮助,也希望认识更多热爱程序开发的小伙伴。
感谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫猫的小茶馆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值