【Linux系统编程】17. 进程间通信(下)

文章目录

一、system V 共享内存

1、理解共享内存

  • 本质: 由内核分配并管理的一段物理内存区域 ,允许多个进程将其映射到各自的虚拟地址空间中,从而实现直接读写共享数据,形成高效的双向数据交互
  • 使用场景:
    1. 一方通过 shmget() 创建或获取共享内存段。
    2. 各进程通过 shmat() 将共享内存段附加到自己的地址空间,获得指向该内存的指针。
    3. 双方直接通过指针读写共享内存进行数据交互,可用于对性能要求极高的场景(如大数据量实时传输)。
    4. 通信结束后,通过 shmdt() 分离共享内存,并可通过 shmctl() 删除该段。

2、共享内存示意图

  • 进程 A 和进程 B 通过将同一块物理内存映射到各自的虚拟地址空间,实现了数据共享。
  • 数据传递不再需要通过内核缓冲区,也无需执行 read/write 等系统调用,进程直接读写自己地址空间内的共享内存即可,因此是最快的 IPC 方式
  • 但是共享内存本身不提供同步机制,需要配合信号量、互斥锁等工具来避免竞态条件,确保数据一致性。

3、共享内存函数

1)ftok 函数

2)shmget 函数

3)shmat 函数

4)shmdt 函数

5)shmctl 函数

4、共享内存的使用样例

1)共享内存实现通信

a)Shm.hpp
cpp 复制代码
#pragma once

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


const int gdefaultid = -1;
const int gsize = 4096;
const std::string pathname = ".";
const int projid = 0x66;
const int gmode = 0666;
#define CREATER "creater"
#define USER "user"

#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)
    
    
class Shm
{
private:
    // 创建的一定要是一个全新的共享内存
    void CreateHelper(int flg)
    {
        printf("key: 0x%x\n", _key);
        // 共享内存的生命周期随内核,不随进程
        _shmid = shmget(_key, _size, flg);
        if (_shmid < 0)
        {
            ERR_EXIT("shmget");
        }
        printf("shmid: %d\n", _shmid);
    }

    void Create()
    {
        CreateHelper(IPC_CREAT | IPC_EXCL | gmode);
    }

    void Get()
    {
        CreateHelper(IPC_CREAT);
    }

    void Attach()
    {
        _start_mem = shmat(_shmid, nullptr, 0);
        if ((long long)_start_mem < 0)
        {
            ERR_EXIT("shmat");
        }
        printf("attach success\n");
    }

    void Detach()
    {
        int n = shmdt(_start_mem);
        if (n == 0)
        {
            printf("detach success\n");
        }
    }

    void Destroy()
    {
        if (_shmid == gdefaultid)
            return;

        Detach();
        if (_usertype == CREATER)
        {
            int n = shmctl(_shmid, IPC_RMID, nullptr);
            if (n == 0)
            {
                printf("shmctl delete shm: %d success!\n", _shmid);
            }
            else
            {
                ERR_EXIT("shmctl");
            }
        }
    }

public:
    Shm(const std::string &pathname, int projid, const std::string &usertype)
        : _shmid(gdefaultid), _size(gsize), _start_mem(nullptr), _usertype(usertype)
    {
        _key = ftok(pathname.c_str(), projid);
        if (_key < 0)
        {
            ERR_EXIT("ftok");
        }

        if (_usertype == CREATER)
            Create();
        else if (_usertype == USER)
            Get();
        else
        {
        }

        Attach();
    }

    ~Shm()
    {
        if (_usertype == CREATER)
            Destroy();
    }

    void *VirtualAddr()
    {
        printf("VirtualAddr: %p\n", _start_mem);
        return _start_mem;
    }

    int Size()
    {
        return _size;
    }

    void Attr()
    {
        struct shmid_ds ds;
        int n = shmctl(_shmid, IPC_STAT, &ds);
        printf("shm_segsz: %ld\n", ds.shm_segsz);
        printf("key: 0x%x\n", ds.shm_perm.__key);
    }

private:
    int _shmid;
    key_t _key;
    int _size;
    void *_start_mem; // 指向起始虚拟地址空间的指针
    std::string _usertype;
};
b)server.cc
cpp 复制代码
#include "Shm.hpp"

int main()
{
    Shm shm(pathname, projid, CREATER);
    char *mem = (char *)shm.VirtualAddr();
    while (true)
    {
        printf("%s\n", mem);
        sleep(1);
    }
    return 0;
}
c)client.cc
cpp 复制代码
#include "Shm.hpp"

int main()
{
    Shm shm(pathname, projid, USER);
    char *mem = (char *)shm.VirtualAddr();
    for (char c = 'A'; c <= 'Z'; c++)
    {
        mem[c - 'A'] = c;
        sleep(1);
    }

    return 0;
}

效果:

注意: 进程结束了,如果没有删除共享内存,共享内存资源会一直存在。因为共享内存的资源生命周期随内核!

查看共享内存段:ipcs -m

删除共享内存有两种方式:

  1. 指令删除
powershell 复制代码
ipcrm -m [shmid]
  1. 代码删除
cpp 复制代码
shmctl(shmid, IPC_RMID, null);

2)借助命名管道实现访问控制版的共享内存

a)Comm.hpp
cpp 复制代码
#pragma once

#include <cstdio>
#include <cstdlib>

#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)
    
b)Shm.hpp
cpp 复制代码
#pragma once

#include <iostream>
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "Comm.hpp"

const int gdefaultid = -1;
const int gsize = 4096;
const std::string pathname = ".";
const int projid = 0x66;
const int gmode = 0666;
#define CREATER "creater"
#define USER "user"

class Shm
{
private:
    // 创建的一定要是一个全新的共享内存
    void CreateHelper(int flg)
    {
        printf("key: 0x%x\n", _key);
        // 共享内存的生命周期随内核,不随进程
        _shmid = shmget(_key, _size, flg);
        if (_shmid < 0)
        {
            ERR_EXIT("shmget");
        }
        printf("shmid: %d\n", _shmid);
    }

    void Create()
    {
        CreateHelper(IPC_CREAT | IPC_EXCL | gmode);
    }

    void Get()
    {
        CreateHelper(IPC_CREAT);
    }

    void Attach()
    {
        _start_mem = shmat(_shmid, nullptr, 0);
        if ((long long)_start_mem < 0)
        {
            ERR_EXIT("shmat");
        }
        printf("attach success\n");
    }

    void Detach()
    {
        int n = shmdt(_start_mem);
        if (n == 0)
        {
            printf("detach success\n");
        }
    }

    void Destroy()
    {
        if (_shmid == gdefaultid)
            return;

        Detach();
        if (_usertype == CREATER)
        {
            int n = shmctl(_shmid, IPC_RMID, nullptr);
            if (n == 0)
            {
                printf("shmctl delete shm: %d success!\n", _shmid);
            }
            else
            {
                ERR_EXIT("shmctl");
            }
        }
    }

public:
    Shm(const std::string &pathname, int projid, const std::string &usertype)
        : _shmid(gdefaultid), _size(gsize), _start_mem(nullptr), _usertype(usertype)
    {
        _key = ftok(pathname.c_str(), projid);
        if (_key < 0)
        {
            ERR_EXIT("ftok");
        }

        if (_usertype == CREATER)
            Create();
        else if (_usertype == USER)
            Get();
        else
        {
        }

        Attach();
    }

    ~Shm()
    {
        if (_usertype == CREATER)
            Destroy();
    }

    void *VirtualAddr()
    {
        printf("VirtualAddr: %p\n", _start_mem);
        return _start_mem;
    }

    int Size()
    {
        return _size;
    }

    void Attr()
    {
        struct shmid_ds ds;
        int n = shmctl(_shmid, IPC_STAT, &ds);
        printf("shm_segsz: %ld\n", ds.shm_segsz);
        printf("key: 0x%x\n", ds.shm_perm.__key);
    }

private:
    int _shmid;
    key_t _key;
    int _size;
    void *_start_mem; // 指向起始虚拟地址空间的指针
    std::string _usertype;
};
c)Fifo.hpp
cpp 复制代码
#pragma once

#include <iostream>
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include "Comm.hpp"

#define PATH "."
#define FILENAME "fifo"


class NameFifo
{
public:
    NameFifo(const std::string &path, const std::string &name)
        : _path(path), _name(name)
    {
        _fifoname = _path + "/" + _name;
        umask(0);
        // 新建管道
        int n = mkfifo(_fifoname.c_str(), 0666);
        if (n < 0)
        {
            ERR_EXIT("mkfifo");
        }
        else
        {
            std::cout << "mkfifo success" << std::endl;
        }
    }

    ~NameFifo()
    {
        // 删除管道文件
        int n = unlink(_fifoname.c_str());
        if (n == 0)
        {
            std::cout << "remove fifo " << _fifoname << " success" << std::endl;
        }
        else
        {
            std::cout << "remove fifo " << _fifoname << " failed" << std::endl;
        }
    }

private:
    std::string _path;
    std::string _name;
    std::string _fifoname;
};

class FileOper
{
public:
    FileOper(const std::string &path, const std::string &name)
        : _path(path), _name(name), _fd(-1)
    {
        _fifoname = _path + "/" + _name;
    }

    ~FileOper()
    {
    }

    void OpenForRead()
    {
        // write方没有open的时候,read方就要在open内部进行阻塞
        // 直到有人把管道文件打开了,open才会返回!
        _fd = open(_fifoname.c_str(), O_RDONLY);
        if (_fd < 0)
        {
            ERR_EXIT("open");
        }
        std::cout << "open fifo success" << std::endl;
    }

    void OpenForWrite()
    {
        _fd = open(_fifoname.c_str(), O_WRONLY);
        if (_fd < 0)
        {
            ERR_EXIT("open");
        }
        std::cout << "open fifo success" << std::endl;
    }

    void Wakeup()
    {
        // 写入操作
        int c = 'c';
        int n = write(_fd, &c, 1);
        printf("尝试唤醒: %d\n", n);
    }

    bool Wait()
    {
        char c;
        int number = read(_fd, &c, 1);
        if(number>0)
        {
            printf("醒来: %d\n", number);
            return true;
        }
        return false;
    }

    void Close()
    {
        if (_fd > 0)
            close(_fd);
    }

private:
    std::string _path;
    std::string _name;
    std::string _fifoname;
    int _fd;
};
d)server.cc
cpp 复制代码
#include "Shm.hpp"
#include "Fifo.hpp"

int main()
{
    Shm shm(pathname, projid, CREATER);
    shm.Attr();

    NameFifo fifo(PATH, FILENAME);

    FileOper readerfile(PATH, FILENAME);
    readerfile.OpenForRead();

    char *mem = (char *)shm.VirtualAddr();
    while (true)
    {
        if (readerfile.Wait())
        {
            printf("%s\n", mem);
        }
        else
            break;
    }
    readerfile.Close();
    std::cout << "server and normal" << std::endl;

    return 0;
}
e)client.cc
cpp 复制代码
#include"Shm.hpp"
#include"Fifo.hpp"

int main()
{
    FileOper writerfile(PATH, FILENAME);
    writerfile.OpenForWrite();

    Shm shm(pathname, projid, USER);
    char *mem = (char *)shm.VirtualAddr();
    // 读写共享内存,没有使用系统调用
    int index = 0;
    for (char c = 'A'; c <= 'Z'; c++, index += 2)
    {
        sleep(1);
        mem[index] = c;
        mem[index + 1] = c;
        sleep(1);
        mem[index + 2] = 0;

        writerfile.Wakeup();
    }
    writerfile.Close();

    return 0;
}

5、共享内存的优缺点


优点(速度极致)

  1. IPC 中速度最快: 核心原因是映射到进程地址空间后,数据读写无需经过内核;
  2. 数据实时可见: 一个进程修改共享内存的数据,另一个进程可直接看到,无数据拷贝;
  3. 无系统调用开销: 无需通过read/write等系统调用传递数据,直接操作内存,效率最大化。

缺点(无保护机制)

  1. 无同步 / 互斥机制: 共享内存本身不提供 "锁" 或 "信号量",多个进程同时读写时,会导致数据覆盖、读取脏数据等不一致问题;
  2. 无访问控制: 无法限制进程的读写时机 / 顺序,需手动实现保护逻辑。

解决方案

若要保护共享内存数据一致性,可结合命名管道(FIFO) 实现访问控制:

  • 同步: 用命名管道的 "阻塞特性" 控制进程读写共享内存的时机(如读端等待写端通知后再读取);
  • 互斥: 用命名管道实现 "锁" 的逻辑(同一时间仅允许一个进程操作共享内存)。

二、system V 消息队列

1、理解消息队列

  • 本质: 由内核维护的一条消息链表 ,按消息类型(type)组织,消息按优先级或顺序排队,进程可向队列发送 / 接收特定类型的消息,形成有类型、可优先级的异步通信

  • 使用场景:

    1. 一方通过 msgget() 创建或获取消息队列。
    2. 发送方通过 msgsnd() 向队列发送带类型的消息。
    3. 接收方通过 msgrcv() 按类型(或顺序)从队列接收消息,可用于异步任务分发、消息路由、多对一 / 一对多通信等场景。
    4. 通信结束后,通过 msgctl() 删除队列,释放内核资源。

2、消息队列的特性

  • 提供了一种从一个进程向另一个进程发送有类型数据块的方式。
  • 每个数据块都被认为有一个类型,接收进程可以根据不同的类型值来选择性地接收数据块。
  • 消息队列的生命周期随内核,IPC 资源不会自动清除,必须显式调用 msgctl() 或使用命令行工具删除。
  • 常用管理命令:
    • 查看消息队列资源:ipcs -q
    • 删除消息队列资源:ipcrm -q [msqid]

3、消息队列函数

1)msgget 函数

2)msgsnd 函数

3)msgrcv 函数

4)msgctl 函数

三、system V 信号量

1、理解信号量

  • 本质: 信号量本质是一个计数器 ,用来表明临界资源中可用资源的数量,是一种对共享资源的预订机制
  • 使用场景:
    1. 一方通过 semget() 创建或获取信号量集。
    2. 初始化信号量值(通过 semctl()SETVALSETALL 命令)。
    3. 进程通过 semop() 执行 P/V 操作,申请或释放资源,可用于共享内存保护、临界区互斥、生产者 - 消费者模型等场景。
    4. 通信结束后,通过 semctl() 删除信号量集,释放内核资源。

2、信号量的操作

  1. 核心操作(PV 操作):
    • P 操作(申请资源):计数器减 1。若结果 > 0,进程继续;若结果≤0,进程阻塞挂起,等待资源释放。
    • V 操作(释放资源):计数器加 1,唤醒等待该资源的阻塞进程。
  2. 二元信号量: 信号量值只有 0 或 1 两种状态,用于实现互斥锁,保证同一时间只有一个进程进入临界区。

3、信号量的特性

  • 生命周期: 信号量的生命周期随内核,IPC 资源不会自动清除,必须显式删除。
  • 常用管理命令:
    • 查看信号量资源:ipcs -s
    • 删除信号量资源:ipcrm -s [semid]

4、并发编程

1、核心定义

  • 共享资源: 多个执行流(进程 / 线程)能够同时看到和访问的同一份公共资源。
  • 临界资源(互斥资源): 系统中一次只允许一个执行流使用的资源。被保护起来的共享资源就是临界资源。
  • 临界区: 进程中涉及访问临界资源的代码段。程序代码 = 临界区(访问临界资源) + 非临界区(不访问临界资源)。

2、保护本质

  • 对共享资源进行保护,本质是对访问共享资源的代码(临界区)进行保护

3、保护方式

  • 互斥: 任何时刻,只允许一个执行流进入临界区访问资源,防止竞争冲突。
  • 同步: 多个执行流访问临界资源时,需要按照预定的顺序进行,保证时序正确性。

4、原子性要求

  • 锁本身也是共享资源,在申请锁(加锁)时,操作必须是原子的(要么完全执行,要么完全不执行),才能保证锁的安全。

5、信号量函数

1)semget 函数

2)semop 函数

3)semctl 函数

相关推荐
山岚的运维笔记1 小时前
SQL Server笔记 -- 第69章:时态表
数据库·笔记·后端·sql·microsoft·sqlserver
白云偷星子1 小时前
RHCSA笔记4
运维
Hank Nie1 小时前
操作系统实践 0 | xv6入门与配置
linux·运维·服务器·系统架构
czxyvX1 小时前
011-Linux进程控制
linux
DeeplyMind2 小时前
第27章 常见问题与解决方案
运维·docker·容器
_OP_CHEN2 小时前
【Linux系统编程】(三十六)深挖信号保存机制:未决、阻塞与信号集的底层实现全解析
linux·运维·操作系统·进程·c/c++·信号·信号保存
catoop2 小时前
Nginx 解决 upstream sent too big header 错误
运维·nginx
IvanCodes2 小时前
六、Linux核心服务与包管理
linux
ayaya_mana2 小时前
Linux一键部署Docker与镜像加速配置
linux·运维·docker