【Linux】进程间通信(2)命名管道&&共享内存

目录

[一 命名管道](#一 命名管道)

[1 概念](#1 概念)

[2 创建一个命名管道](#2 创建一个命名管道)

[3 命名管道的特点](#3 命名管道的特点)

[4 命名管道和匿名管道的区别](#4 命名管道和匿名管道的区别)

[5 命名管道的打开规则](#5 命名管道的打开规则)

[二 共享内存](#二 共享内存)

[1 原理](#1 原理)

[2 代码](#2 代码)

[3 内核如何管理IPC资源(包括:shm,asgq,sem)](#3 内核如何管理IPC资源(包括:shm,asgq,sem))

[4 共享内存完整代码](#4 共享内存完整代码)

[三 理解消息队列基本原理](#三 理解消息队列基本原理)


一 命名管道

命名管道(Named Pipe / FIFO)是一种常用的跨进程通信(IPC)机制,核心是文件系统中的特殊文件,打破了匿名管道仅能用于亲缘进程的限制,让任意进程通过路径名即可实现通信

1 概念

  • 全称:Named Pipe,又称 FIFO(First In First Out,先进先出)。

  • 本质:文件系统中可见的特殊文件(ls -l 显示类型为 p),有路径、权限但不占用磁盘数据块,数据仅存于内核缓冲区,性能接近匿名管道。

  • 核心优势:无亲缘关系的进程(跨会话、无关程序),只要知道管道路径,就能打开并通信。

2 创建一个命名管道

可以从命令行上创建

bash 复制代码
$ mkfifo filename

也可以从程序里创建,相关函数有:

bash 复制代码
int mkfifo(const char *filename,mode_t mode);

3 命名管道的特点

进程间通信的本质是:让不同的进程,看到同一份资源

命名管道有自己的inode,但是不会把自己的内存数据刷新到文件内部;命名管道在open的时候,就已经进行了让读写同步

通信方式:默认半双工(同一时间只能单向传输),双向通信需创建两个管道。

阻塞机制:默认阻塞(读空、写满时进程挂起等待),可通过 O_NONBLOCK 设为非阻塞。

4 命名管道和匿名管道的区别

特性 匿名管道(pipe) 命名管道(FIFO)
创建方式 pipe() 系统调用 mkfifo 命令 / mkfifo() 函数
可见性 无文件名,仅内核可见 文件系统可见,有路径名
适用进程 仅亲缘关系(父子/兄弟) 任意进程(无关/跨会话)
生命周期 随进程退出销毁 文件持久存在,需手动删除

5 命名管道的打开规则

如果当前打开操作是为而打开 FIFO 时:

O_NONBLOCK 关闭(disable):阻塞直到有相应进程为写而打开该 FIFO

O_NONBLOCK 开启(enable):立刻返回成功

如果当前打开操作是为而打开 FIFO 时:

O_NONBLOCK 关闭(disable):阻塞直到有相应进程为读而打开该 FIFO

O_NONBLOCK 开启(enable):立刻返回失败,错误码为 ENXIO
总结

核心区别:读打开非阻塞时直接成功,写打开非阻塞时直接失败;阻塞模式下,读写打开都会等待对应端进程打开。

但是我们使用管道的通信方式效率低,应用场景少,就需要有新的进程间通信

我们要规定标准**:System V (只能做一台主机上的进程间通信)**

我们基于System V这个标准,产生了三种进程间通信:共享内存,消息队列,信号量

我们下面来学习共享内存


二 共享内存

1 原理

进程的虚拟地址空间由mm_struct结构体统一管理。共享内存的核心实现机制是物理内存映射,具体流程如下:
内核在物理内存中划分出一段独立的物理内存空间,记录其起始地址和长度;
分别将该物理内存段映射到进程 A、进程 B 的虚拟地址空间中(为 A 和 B 分配各自的虚拟起始地址);
最终实现同一块物理内存映射到不同进程的虚拟地址空间,让进程 A 和进程 B "看到同一份内存数据",完成跨进程的数据共享。

这种映射方式打破了进程地址空间的独立性

在这个过程中,构建映射关系,这一步叫做attach-->挂接 ;不想玩了怎么办?detech--->去关联

两个问题:

(1):这整个过程是谁做的?操作系统,并提供系统调用

(2):共享区用户能直接访问吗?

共享内存被映射到进程的用户地址空间中,因此用户进程可以直接通过指针访问共享内存的内容,无需额外调用系统调用读写数据。创建和删除共享内存(shm)需要系统调用,使用shm不需要系统调用,类似于malloc(),创建和使用管道都用到系统调用

理解:

共享内存的存在是可以同时存在多份的,对于不同的共享内存操作系统是需要进行管理的-->先描述,再组织;内核中一定存在描述共享内存的描述体-->struct shmxx

例如:

cpp 复制代码
struct shmid_ds {
 struct ipc_perm shm_perm; /* operation perms */
 int shm_segsz; /* size of segment 
(bytes) */
 __kernel_time_t shm_atime; /* last attach time 
*/
 __kernel_time_t shm_dtime; /* last detach time 
*/
 __kernel_time_t shm_ctime; /* last change time 
*/
 __kernel_ipc_pid_t shm_cpid; /* pid of creator */
 __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
 unsigned short shm_nattch; /* no. of current 
attaches */
 unsigned short shm_unused; /* compatibility */
 void shm_unused2; / ditto - used by 
DIPC */
 void shm_unused3; / unused */
};

2 代码

.hpp文件后缀:可以在其他文件开头包括 #include"xxx.hpp",就能直接使用.hpp文件内创建的类等

系统调用:shmget:作用是创建或获取一个共享内存段

cpp 复制代码
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

key_t key

作用:作为共享内存段在系统中的唯一标识符,用于让不同进程找到同一块共享内存。

特点:由程序员指定,通常通过ftok() 生成 ,确保多个进程能获取同一个 key。

补充:key 是内核层面的标识,进程无法直接使用它访问内存,只能用它获取 shmid

size_t size

作用:指定要创建的共享内存段的大小。

注意:实际分配的大小通常会向上取整为 4KB(一页)的整数倍,以适配内存管理机制

int shmflg

控制共享内存的创建行为,核心标志位

标志位组合 含义 用途
IPC_CREAT 若共享内存不存在则创建,若存在则直接获取 通用获取 / 创建
`IPC_CREAT IPC_EXCL` 若共享内存不存在则创建,若已存在则报错 确保创建全新的共享内存
0666 指定共享内存的读写权限(用户 / 组 / 其他均可读写) 配合创建标志使用

IPC_EXCL 单独使用无意义,必须和 IPC_CREAT 一起使用,才能实现 "不存在则创建,存在则失败" 的互斥效果

返回值

成功:返回一个有效的共享内存标识符 shmid (后续操作共享内存都用这个值)。

失败:返回 -1,并设置 errno 来指示错误原因

A和B要访问同一个键值key创建的共享内存,A得到了key,但是B怎么知道key是多少?怎么知道内存中那么多共享内存哪一个是自己的?

怎么样创建key,同时能让A,B通信?

程序员自己设定key,在不让A,B通信的情况下,采用约定的方式,一个用来创建时设置,一个用来获取;所以shmget要自己设置key

ftok:作用是生成一个唯一的 key_t 类型键值

cpp 复制代码
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

const char *pathname:指定一个已存在的文件路径名

int proj_id:自定义的 "项目 ID",通常取 1~255 之间的整数

为什么要用 ftok?

共享内存、消息队列等 IPC 资源需要一个全局唯一的key,如果手动指定容易冲突,ftok 可以基于文件路径和自定义 ID 稳定生成不冲突的key,让不同进程通过相同的pathname和proj_id拿到同一个key,从而访问同一个 IPC 资源。

3 内核如何管理IPC资源(包括:shm,asgq,sem)

共享内存的生命周期随内核,不随进程!!

程退出时,只会从自己的地址空间中 "卸载" 共享内存(shmdt),不会删除内核中的共享内存段

只要内核没有主动删除,共享内存就会一直存在,直到系统重启或被显式释放。

你可以用 ipcs -m 命令查看当前系统中所有未被释放的共享内存段,即使创建它的进程早已退出

查看共享内存

cpp 复制代码
ipcs -m

手动释放共享内存

cpp 复制代码
ipcrm -m <shmid>

系统调用:shmctl 共享内存的控制接口,支持删除、获取属性、设置属性等操作

cpp 复制代码
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)

buf:指向⼀个保存着共享内存的模式状态和访问权限的数据结构

返回值:成功返回0;失败返回-1

shmdt函数:将共享内存段与当前进程脱离

cpp 复制代码
int shmdt(const void *shmaddr);

参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段

共享内存是进程间通信,速度最快的

享内存没有自带保护机制,任何挂接进程可以随时访问共享内存-->如果想保护,我们就要学到一个知识:信号量

4 共享内存完整代码

cpp 复制代码
#ifndef __SHM_HPP
#define __SHM_HPP

#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/shm.h>
#include <string>

// 共享内存相关配置常量
const std::string proj_name = "/home";  // ftok使用的路径
const int proj_id = 0x6666;             // ftok使用的项目ID
const int g_size = 4096;                // 默认共享内存大小(4KB)

// 辅助函数:将数字转为十六进制字符串输出
static std::string ToHex(long long data)
{
    char hex[64];
    snprintf(hex, sizeof(hex), "0x%llx", data);
    return hex;
}

// 共享内存封装类
class Shm
{
public:
    Shm(int size = g_size) 
        : _shmid(-1), _size(size), _key(0), _start(nullptr)
    {
    }

    ~Shm() 
    {}

private:
    // 生成唯一的key值(用于标识共享内存)
    key_t GetKey()
    {
        _key = ftok(proj_name.c_str(), proj_id);
        if (_key < 0)
        {
            perror("ftok error");
        }
        return _key;
    }

    // 创建/获取共享内存核心函数
    bool CreateCoreHelper(int flags)
    {
        // 1. 获取唯一key
        key_t k = GetKey();
        // 2. 创建/获取共享内存
        _shmid = shmget(k, _size, flags);
        if (_shmid < 0)
        {
            perror("shmget error");
            return false;
        }
        return true;
    }

public:
    // 1. 创建全新的共享内存(若已存在则报错)
    bool Create()
    {
        return CreateCoreHelper(IPC_CREAT | IPC_EXCL | 0666);
    }

    // 2. 获取已存在的共享内存
    bool Get()
    {
        return CreateCoreHelper(IPC_CREAT | 0666);
    }

    // 3. 从内核中删除共享内存
    bool Delete()
    {
        int n = shmctl(_shmid, IPC_RMID, nullptr);
        return n != -1;
    }

    // 4. 打印共享内存内核属性
    void GetShmAttr()
    {
        struct shmid_ds ds;
        int n = shmctl(_shmid, IPC_STAT, &ds);
        if(n < 0)
        {
            perror("shmctl error");
            return;
        }

        std::cout << "\n===== 共享内存属性信息 =====" << std::endl;
        std::cout << "当前进程PID: " << getpid() << std::endl;
        std::cout << "创建者PID: " << ds.shm_cpid << std::endl;
        std::cout << "共享内存大小: " << ds.shm_segsz << " 字节" << std::endl;
        std::cout << "唯一KEY值: " << ToHex(ds.shm_perm.__key) << std::endl;
        std::cout << "==========================\n" << std::endl;
    }

    // 5. 将共享内存附加到当前进程地址空间
    void* Attach()
    {
        _start = shmat(_shmid, nullptr, 0);
        if(_start == (void*)-1)
        {
            perror("shmat error");
            _start = nullptr;
            return nullptr;
        }
        return _start;
    }

    // 6. 将共享内存从当前进程地址空间分离
    void Detach()
    {
        if(_start != nullptr)
        {
            shmdt(_start);
            _start = nullptr;
        }
    }

    // 调试打印:key与shmid
    void Debug()
    {
        std::cout << "===== 调试信息 =====" << std::endl;
        std::cout << "key:   " << ToHex(_key) << std::endl;
        std::cout << "shmid: " << _shmid << std::endl;
        std::cout << "====================\n" << std::endl;
    }

private:
    key_t _key;      // 共享内存唯一标识
    int   _shmid;    // 共享内存操作ID
    int   _size;     // 共享内存大小
    void* _start;    // 映射到进程地址空间的起始地址
};

// 共享内存中存储的数据结构(可自定义)
typedef struct Data
{
    int count;              // 计数变量
    char buffer[26 * 2];     // 数据缓冲区
} buffer_t;

#endif

三 理解消息队列基本原理

如果队列里面只有数据,操作系统怎么知道数据是A的还是B的?

A把数据给B,就必须保证这个数据块是有类型数据块

消息队列是一个进程,给另一个进程发送有类型数据块的方式(支持互相发消息)

如何进行通信?-->系统调用:msgsnd

cpp 复制代码
#include <sys/msg.h>

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数名 说明
msqid 消息队列 ID,由 msgget 获取
msgp 指向消息结构体的指针,必须以 long mtype 开头
msgsz 消息数据部分长度,不包含 mtype
msgflg 0:阻塞;IPC_NOWAIT:非阻塞,队列满立即出错
返回值 成功:0失败:-1

消息队列其他的内容我们就不在这里谈了

相关推荐
铅笔小新z2 小时前
【Linux】进程(下)
java·linux·运维
IT北辰2 小时前
centos 安装最新jdk25
linux·运维·centos
菱玖2 小时前
Centos重连IP改变问题解决
linux·tcp/ip·centos
Industio_触觉智能2 小时前
玩转RK3588远程控制,Ubuntu22.04 Wayland安装RustDesk工具
linux·ubuntu·rk3588·远程工具·rustdesk·wayland·ubuntu22.04
SpikeKing2 小时前
Server - 服务器 CentOS 安装与配置 Docker
服务器·docker·centos
idolao2 小时前
傲梅分区助手 使用教程:免安装硬盘分区管理工具
linux·运维·服务器
观无2 小时前
Jenkins 完整搭建 + .NET8 全自动发布
运维·jenkins
cyber_两只龙宝2 小时前
【Nginx】Nginx配置负载均衡详解
linux·运维·nginx·云原生·负载均衡
Deitymoon2 小时前
linux——信号量
linux