共享内存&管道通信

一、共享内存:

原理是多个进程可以访问同一块内存区域,这样数据不需要在进程间复制,因此效率很高。不过,由于共享内存没有内建的同步机制,所以需要配合互斥锁或信号量来避免竞争条件。

1.共享内存实现原理:

内存共享:通过shmget创建共享内存段,shmat将其映射到进程地址空间

同步机制:使用信号量保证读写顺序:

父进程获取锁(P操作)-> 写入数据 -> 释放锁(V操作)

子进程获取锁 -> 读取数据 -> 释放锁

数据流动:直接通过内存地址访问,无数据拷贝

清理机制:父进程最后删除共享内存和信号量

2.共享内存代码:

3.关键思想:

零拷贝:直接内存访问,性能极高

显式同步:必须通过额外机制(信号量)保证数据一致性

生命周期管理:需要显式删除共享资源

#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <unistd.h>
#include <cstring>

// 定义信号量操作函数
void sem_op(int semid, int num, int op) {
    struct sembuf sb;
    sb.sem_num = num;
    sb.sem_op = op;
    sb.sem_flg = 0;
    semop(semid, &sb, 1);
}

int main() {
    // 创建共享内存(1KB)
    int shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666);
    char* shm = (char*)shmat(shmid, NULL, 0);

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

    if (fork() == 0) { // 子进程
        sem_op(semid, 0, -1); // P操作(获取锁)
        printf("Child read: %s\n", shm);
        sem_op(semid, 0, 1);  // V操作(释放锁)
        shmdt(shm);
    } else {          // 父进程
        sem_op(semid, 0, -1); // P操作
        strcpy(shm, "Hello via Shared Memory!");
        sem_op(semid, 0, 1);  // V操作
        wait(NULL);
        shmdt(shm);
        shmctl(shmid, IPC_RMID, NULL); // 清理
        semctl(semid, 0, IPC_RMID);
    }
    return 0;
}

共享内存的例子中使用信号量来确保读写顺序,父进程先写,子进程后读。

函数解读

sem_op 函数:用来操作信号量。

信号量用于同步访问共享资源,防止并发访问产生问题(例如竞争条件)。
sem_num 表示操作的信号量的编号(此处我们只有一个信号量,所以是 0)。
sem_op 的值可以是负数(P 操作,表示等待获取锁),或者是正数(V 操作,表示释放锁)。
sem_flg 是信号量操作的标志,通常为 0。
semop 是用于执行信号量操作的系统调用。

shmget:创建或获取一个共享内存段。

  • IPC_PRIVATE:表示该共享内存是私有的,仅在当前进程和它的子进程之间使用。
  • 1024:共享内存段的大小,单位是字节。
  • IPC_CREAT | 0666:如果共享内存段不存在,则创建一个新段,权限为 0666(所有用户都可以读写)。

shmat:将共享内存段连接到当前进程的地址空间。

  • shmid:共享内存段的标识符。
  • NULL:让操作系统自动选择共享内存连接的地址。
  • 0:指定默认的连接选项,表示允许读写。

semget:创建或获取一个信号量集。

  • IPC_PRIVATE:表示这是一个私有的信号量集,通常只有当前进程及其子进程能访问。
  • 1:指定信号量集中的信号量个数,这里只有一个信号量(0)。
  • IPC_CREAT | 0666:如果信号量不存在,则创建一个新的信号量,权限为 0666

semctl:控制信号量集的操作。

  • semid:信号量集的标识符。
  • 0:指定要操作的信号量的编号(此处只有一个信号量,所以编号为 0)。
  • SETVAL:设置信号量的值。
  • 1:将信号量的初始值设为 1,表示信号量初始为可用状态。

shmdt:断开与共享内存的连接,释放资源。

wait:父进程等待子进程结束,确保子进程先完成读取操作再退出。

shmctl:删除共享内存段,清理资源。IPC_RMID 表示删除共享内存。

semctl:删除信号量集,清理资源。IPC_RMID 表示删除信号量。

二、管道

1.管道:

管道分为匿名管道和命名管道,匿名管道通常用于父子进程之间的通信,是单向的,数据通过内核缓冲区传输,适合小规模数据。

确保没有死锁或竞争条件。

2.管道代码

#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/wait.h>

int main() {
    int fd[2];
    char buf[256];
    
    pipe(fd); // 创建匿名管道

    if (fork() == 0) { // 子进程
        close(fd[1]);  // 关闭写端
        read(fd[0], buf, sizeof(buf));
        printf("Child received: %s\n", buf);
        close(fd[0]);
    } else {          // 父进程
        close(fd[0]);  // 关闭读端
        const char* msg = "Hello via Pipe!";
        write(fd[1], msg, strlen(msg)+1);
        close(fd[1]);
        wait(NULL);
    }
    return 0;
}

管道例子中,父进程关闭读端,子进程关闭写端,正确使用文件描述符。

3.pipe()

pipe() 创建了一个管道,它包含两个文件描述符,fd[0] 用于读取数据,fd[1] 用于写入数据。

fork() 后,父进程和子进程共享这两个文件描述符。具体来说:

子进程通过 fd[0] 读取数据,父进程通过 fd[1] 写入数据。

pipe() 创建管道时,操作系统会自动为我们打开管道的读端和写端,并将其分别赋值给 fd[0]fd[1]

问题:为什么要分别关闭 fd[1]fd[0]

在进程间通信中,我们通常需要根据管道的方向(读或写)来管理文件描述符。关闭不必要的文件描述符,可以避免不必要的操作,并保证数据流的正确性。

三、管道&共享内存对比分析

特性共享内存管道
数据拷贝零拷贝两次拷贝(用户->内核->用户)
最大传输量仅受内存限制默认64KB(可配置)
同步机制需要显式同步(信号量/锁)内置流量控制
进程关系任意进程必须存在亲缘关系
使用场景大数据量、高性能需求小数据、简单通信
复杂度高(需管理同步和生命周期)低(自动管理)


四、设计思想总结

1.共享内存核心思想:


空间换时间:通过共享物理内存页实现零拷贝

分离数据通道与控制通道:数据通道是共享内存,控制通道需要独立同步机制

直接内存访问:需要处理字节序、内存对齐等底层细节

2.管道核心思想:

流式接口:提供类似文件操作的read/write语义

内核托管:由操作系统管理缓冲区和同步

生命周期绑定:管道随进程终止自动销毁

3.同步设计要点:

共享内存必须配合锁机制(如示例中的信号量)

管道通过内核缓冲区自动实现流量控制

原子操作:管道的小数据写入(< PIPE_BUF)是原子的

4.建议在实际项目中:

大数据传输(>1MB)优先考虑共享内存

简单消息传递使用管道

需要双向通信时,管道需要创建两个管道

注意资源泄漏问题(特别是共享内存)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值