Linux | 进程间通信

news2024/11/27 4:24:14

目录

前言

一、进程间通信的基本概念

二、管道

1、管道的基本概念

2、匿名管道

(1)原理

(2)测试代码

(3)读写控制相关问题

a、读端关闭

b、写端关闭

c、读快写慢

d、读慢些快

(4)进程池代码拓展

3、命名管道

(1)原理 

(2)测试代码

三、共享内存

1、共享内存的原理

2、测试代码

3、共享内存的特性


前言

        进程间通信的方式有很多,如管道、共享内存、消息队列、信号量、socket套接字等等;本文主要讲解其中管道和共享内存两种方式;

一、进程间通信的基本概念

        我们要理解进程间通信呢首先得知道以下三个问题;

进程间通信是什么?

我们为什么要进程间通信?

怎么进行进程间通信?

        对于所有知识,我们拥有上述三个问题得答案我们就可彻底弄清进程间通信得本质了;

进程间通信是什么?

        进程间通信就是让两个进程,看到同一块空间(内存),以达到我们通过这块共同空间来进程交互得过程;

        我们都知道,我们可以通过fork创建子进程,fork创建后得子进程与父进程共享同一块代码;那么我们是否可以通过创建一个创建变量的资源缓冲区来使这两个父子进程看到同一块空间(内存)呢?很不幸,由于我们进程间的独立性,所以我们无法通过全局变量来让两个进程看到同一块空间,并且使用这个空间进行通信,准确来说是可以看到,但是不能进行通信,因为当我们父进程或子进程对这块空间内容进行修改时会发生写时拷贝;这也是保证进程间独立性而产生的机制;也正是由于进程具有独立性,所以我们的进程间通信便没有那么容易;

为什么要进行进程间通信?

        在实际开发中,我们可能会有并发编程的需求,而我们的单进程是不具备并发能力的,而有时我们的并发之间的多进程需要一起协同配合,既然需要协同配合就可能需要进行通信,因此我们需要进程间通信;

怎么进行进程间通信?

        关于如何进行进程间通信,我们主要有以下几种策略,分别来自于不同的方;

Linux原生方案:管道(匿名管道、命名管道)

System V方案:共享内存、消息队列(不常用)、信号量;

POSIX 方案:socket套接字

        上述为一些主流方案,本文主要讲解 管道方案 与 System V 提供的共享内存方案;

二、管道

1、管道的基本概念

        首先管道我们在前面学习指令的时候就已经接触过了,只不过我们对其了解并不深刻;当时我们只知道我们可以通过管道将一个指令(进程)的输出结果传输给另一个指令(进程);

1、管道是一个信息传输的媒介!如上图所示,进程A将信息写入管道,而进程B从管道中读取数据;

2、管道是一个文件;我们之前提过,Linux下,一切皆文件的理念,那么我们的管道也应该是一个文件,只不过这个文件是属于内存级文件,不会将数据刷新到缓冲区中,也没有必要刷新到缓冲区中做持久化的动作;

3、管道的文件的通信方式为半双工通信;关于这我们需要补充以下几个概念;

单工通信:只有一个方向的通信,且只有固定的一端作为接收端,一端作为输入端;

半双工通信:通信的双方都可以作为接收端也可以作为输入端,当某一时刻,只能有一个端发送,一端接收;就好像两个人聊天,你说一句,我接收后,再回复一句,你再接收;

全双工通信:通信的双方既可以作为接收端也可以作为输入端,某一个时刻,既可以输入也可以接收;就好像两个人吵架,每个人都可以挺别人说话的同时对别人说话;

2、匿名管道

(1)原理

        首先,我们来介绍匿名管道实现进程间通信的原理,再使用匿名管道作为进程间通信的方法之前,我们得确保通信双方进程为父子关系;这是使用匿名管道实现进程间通信得基本前提;

        前面我们讲过进程的相关内核数据,与我们创建一个进程会发生什么?以及当我们调用fork会发生什么?有了上述知识铺垫,我们不难想到,当我们使用fork创建子进程时,我们的子进程会创建自己的内核数据,如PCB控制块,虚拟地址空间、页表、文件描述符结构体等内核数据,其中我们还讲过子进程会拷贝父进程内核数据中的某些数据,当我们对这些数据需要进行修改时会发生写时拷贝的现象;那么问题来了,我们的维护当前进程打开的文件的结构体 files_struct 是否也会发生拷贝呢?当然,这是肯定的?因此,我们使用fork时,应该如下图所示;

        我们匿名管道实现进程间通信就是基于这一特性---- “子进程会继承父进程的文件描述符数组”;因此我们不妨首先创建一个管道文件,然后父进程分别以读和写的方式打开这个管道文件,接着我们创建子进程,子进程必然会对文件描述符数组的内容进行拷贝,子进程也拥有对该文件读和写这两个文件描述符,此时若我们想让父进程写,子进程读,我们只需要将父进程的读文件描述符关闭,子进程写文件描述符关闭,然后我们再调用系统调用read和write,分别向对应文件描述符读和写即可!这就是我们使用匿名管道的方式实现进程间通信的过程;

(2)测试代码

        在正是实现代码之前,我们首先介绍几个与管道相关的接口;

pipe:创建匿名管道

参数:该函数只有一个参数,是一个整型数组,当我们调用该函数时,该函数会为我们创建一个匿名管道文件,并打开这个管道文件其中数组的 0 号下标放的是以读的方式打开该文件的文件描述符,1 号下标方式的以写的方式打开该管道文件的文件描述符;(记忆:0想象成嘴巴,代表读,1想象成笔,代表写);

返回值:若函数调用成功,返回0,调用失败,返回-1,错误码被设置;

        有了上面函数的学习,我们就可以写一段简单的基于匿名管道的进程间通信代码了;如下所示;

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>


int main()
{
    // 1、创建管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    if(n == -1)
    {
        perror("pipe");
        exit(1);
    }
    // 2、创建子进程
    pid_t id = fork();
    if(id == 0)
    {
        // 子进程(读)
        // 3、建立单向信道
        // 关闭子进程写端文件描述符
        close(pipefd[1]);
        // 4、进行通信
        char buf[1024];  // 接收缓冲区
        while(true)
        {
            // 读取来自父进程的信息
            ssize_t sz = read(pipefd[0], buf, sizeof(buf));// 这里就不对read进行差错检验了
            
            buf[sz] = '\0';   // 我们想要读的是字符串,因此在语言层面得加 \0
            if(sz > 0)
            {
                std::cout << "读数据前\n";
                std::cout << "child[" << getpid() << "], father: " << buf << std::endl;
            }
            else if(sz == 0)
            {
                std::cout << "father stop write, me quit" << std::endl;
                break;
            }
            sleep(1);
        }

        exit(0);
    }
    // 父进程(写)
    // 3、建立单向信道
    close(pipefd[0]); // 关闭读端
    // 4、进行通信
    const char* msg = "这是发给子进程的消息 ";
    char tmp[1024] = {0};
    int count = 0;
    while(true)
    {
        memset(tmp, 0, sizeof(tmp));
        snprintf(tmp, sizeof(tmp), "%s[%d], %d\n", msg, getpid(), count++);
        ssize_t sz = write(pipefd[1], tmp, strlen(tmp));
        sleep(1);
    }
    // 5、回收子进程
    waitpid(id, nullptr, 0);
    // 6、关闭文件描述符(可关可不关,因为程序快运行结束,OS会自动释放)
    close(pipefd[1]);
    return 0;

        上述代码是实现了一个父进程写,子进程不断的读的功能;

(3)读写控制相关问题

        基于上述代码,我们还要进行更深层次的研究;我们分别测试以下几种情况下会发生什么?

a、读端关闭

        我们给上述代码读端设置一个计数器,设置成5秒后,读端退出;观察会发生什么;

 

         我们输入命令行监视脚本指令;如下所示;

while :; do ps -axj | head -1 && ps -axj | grep ./main | grep -v grep; sleep 1; echo "--------------------";done

 

        我们发现,当我们的读端关闭时,写端进程被终止了!也就是被杀掉了!由于上述我们让子进程关闭读端文件描述符后休眠5秒,因此我们的子进程没有退出;我们不难得出结论;

结论:读端关闭时,写端进程被操作系统杀死; 

b、写端关闭

        同样的道理,我们使用计数器的方式,使写端到一定的时间后关闭,我们观察读端会如何;

 

        运行结果如下所示;

        仔细观察,打印了我们在read返回值为0的输出内容;

结论:当我们写端关闭时,读端会读到文件的末尾,返回0;

c、读快写慢

        我们将上述代码更改一下;将我们的读端设置为一秒读一次,将我们的写端设置为三秒写一次;再运行代码,看一看会发生什么;

        我们会发现,我们的进程三秒才会打印一次;我们的读进程不是一秒读一次吗?那我们的读进程在干嘛呢?

结论:当读快写慢时,读进程会阻塞等待写进程进行写入;

d、读慢些快

        我们将上述代码改一下,读进程三秒读一次,而写进程一秒写一次;又会发生什么呢?

        我们发现第一次立刻打印了,后面三秒打印一次,且每次都打印了三条内容;这时结果也显而易见了;

结论:读慢写快时,我们的写进程会一直往管道文件里写,直至写满,写满后,写进程会阻塞,直至下一次读进程读取管道文件时,写进程才会被唤醒;

总结:

        综上所述,我们使用匿名管道进行进程间通信的本质是我们通过子进程会继承父进程文件描述符数组的特性,使我们的父进程和子进程看到同一个文件,它们可以通过该文件进行通信;在我看来进程间通信的本质是让两个进程看到同一块空间的方式!而通信是根据上层定义的;

        匿名管道提供了访问控制,所谓访问控制,就是我们上述讨论a、b、c、d四种不同的方式;并不是所有进程间通信具有访问控制,如我们后面讲的共享内存;

        匿名管道的生命周期是随着进程的,一旦进程结束,我们的匿名管道文件也随之销毁;

(4)进程池代码拓展

        通过上述知识,我们可以实现一个简单的进程池代码;

        具体代码如下所示;

// task.h文件,主要封装任务和任务管理器
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <map>
#include <functional>
#include <mutex>

// 定义函数对象类型
using func_t = std::function<void()>;

void webRequest()
{
    std::cout << "正在执行网络请求任务" << std::endl;
}

void readMySQL()
{
    std::cout << "正在执行读取数据库任务" << std::endl;
}

void otherTask()
{
    std::cout << "正在执行其他任务" << std::endl;
}

// 设计一个单例类,管理所有任务
class ManageTask
{
public:
    static ManageTask* getInstance()
    {
        // 双检查加锁
        if(_pm == nullptr)   // 提高效率
        {
            _m.lock();
            if(_pm == nullptr) // 保证线程安全
            {
                _pm = new ManageTask();
            }
            _m.unlock();
        }
        return _pm;
    }
    // 加载类内成员
    void load()
    {
        // 增加任务描述与命令号的映射
        _dict.insert({_tasks.size(), "web请求"});
        // 增加命令
        _tasks.push_back(webRequest);
        _dict.insert({_tasks.size(), "读取数据库"});
        _tasks.push_back(readMySQL);
        _dict.insert({_tasks.size(), "其它任务"});
        _tasks.push_back(otherTask);
    }
    // 自定义新增任务
    void add(func_t& cb, std::string& str)
    {
        _dict.insert({_tasks.size(), str});
        _tasks.push_back(cb);
    }
    // 自定义删除任务(任务号)
    void del(int command)
    {
        _dict.erase(command);
        _tasks.erase(_tasks.begin() + command);
    }
    // 展示当前任务
    void show()
    {
        for(auto e : _dict)
        {
            std::cout << e.first << ": " << e.second << std::endl;
        }
    }
    // 获取任务
    func_t get_task(int command)
    {
        return _tasks[command];
    }
    // 获取任务个数
    size_t get_size()
    {
        return _tasks.size();
    }
    // 获取命令描述
    std::string get_command(int command)
    {
        return _dict[command];
    }
private:
    ManageTask(){}

    ManageTask(ManageTask& m){}
    ManageTask operator=(ManageTask& m){}
private:
    static ManageTask* _pm;
    static std::mutex _m;
    std::vector<func_t> _tasks;
    std::map<int, std::string> _dict;
};

// 初始化单例对象指针和锁
ManageTask* ManageTask::_pm = nullptr;
std::mutex ManageTask::_m;

// main.cc文件,主要实现线程池,以及派发任务等逻辑
#include <iostream>
#include <vector>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "task.hpp"


#define NUM 6

int main()
{
    srand(time(nullptr)); // 种下随机数种子
    ManageTask::getInstance()->load();  // 初始化任务表

    // 1、创建进程池
    std::vector<std::pair<pid_t, int>> desc; // 子进程pid与写端fd
    for(int i = 0; i < NUM; i++)
    {
        // 1.1 创建管道文件
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        if(n == -1) 
        {
            std::cerr << "create pipe fail" << std::endl;
            exit(1);
        }
        // 1.2 创建子进程
        int id = fork();
        if(id == 0)
        {
            usleep(100);
            // 子进程(读)
            // 1.3 关闭管道写端
            close(pipefd[1]);
            // 1.4 进行读取数据
            int command = 0;
            while(true)
            {
                ssize_t sz = read(pipefd[0], &command, sizeof(command));
                if(sz != sizeof(int) || command < 0 || command >= ManageTask::getInstance()->get_size())
                {
                    std::cerr << "读取无效命令" << std::endl;
                    continue;
                }
                // 1.5执行命令对应任务

                ManageTask::getInstance()->get_task(command)();
            }
            exit(0);
        }
        // 父进程(写)
        // 1.3 关闭对应读端,并将写端保存起来
        close(pipefd[0]);
        desc.push_back(std::pair<pid_t, int>(id, pipefd[1]));
    }

    // 2、使用进程池
    // 2.1 获取菜单
    while(true)
    {
        int select = 0;
        int command = 0;
        std::cout << "******************************" << std::endl;
        std::cout << "******    1、showTask   ******" << std::endl;
        std::cout << "******    2、execute    ******" << std::endl;
        std::cout << "******************************" << std::endl;
        // 2.2 从用户获取选择
        std::cout << "Enter select> ";
        std::cin >> select;
        // 2.3 从用户获取命令选项
        if(select == 1)
        {
            ManageTask::getInstance()->show();
        }
        else if(select == 2)
        {
            // 2.4 获取命令
            std::cout << "Enter command> ";
            std::cin >> command;
            // 2.5 选择一个子进程执行(随机数实现负载均衡)
            int proc = rand() % ManageTask::getInstance()->get_size();
            // 2.6 向指定进程发送命令
            write(desc[proc].second, &command, sizeof(command));
            std::cout << "已经成功给" << desc[proc].first << "进程,对应命令:" << ManageTask::getInstance()->get_command(command) << std::endl;
        }
        else
        {
            std::cerr << "选择有误,请重新选择" << std::endl;
            continue;
        }
        usleep(500);
    }

    // 3、关闭进程池
    // 关闭写端fd
    for(int i = 0; i < desc.size(); i++)
    {
        close(desc[i].second);
    }
    // 回收子进程
    for(int i = 0; i < desc.size(); i++)
    {
        waitpid(desc[i].first, nullptr, 0);
    }
    return 0;
}

3、命名管道

(1)原理 

        命名管道的原理与匿名管道不同,命名管道则是通过自己创建一个管道文件,然后双方通过打开这个文件实现看到同一块内存资源的功能;命名管道的最大优势在于命名管道可以使不具有血缘关系的两个进程进行通信;原理非常简单,这里就不做过多解释,可通过后面代码来学习命名管道;

(2)测试代码

        在正是学习命名管道之前,我们同样先补充一批接口的使用;

        首先我们学习一个命令行指令,mkfifo,该指令加上文件名即可创建一个指定名字的管道文件;具体看如下演示;

mkfifo:创建一个管道文件(这是一个函数,与我们上述命令行指令重名而已)

参数一:这个参数为我们要创建管道文件的文件名(默认在当前目录下创建文件)

参数二:这个参数为我们创建管道文件的权限,这个权限会与我们默认权限掩码进行计算最终权限,计算规则(我们设置权限 & (~默认权限));

返回值:若调用成功则返回0,失败则返回-1,且错误码被设置;

unlink:删除一个文件,与我们命令行下的rm指令功能一致;

参数:要删除文件的路径

返回值:若调用成功,则返回0,失败则返回-1,错误码被设置;

        有了上述的知识铺垫,我们可以很容易的写出一个命名管道通信的程序,如下所示;

// comm.hpp文件主要存放公共代码,如管道文件名
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

// 管道文件名
#define PipeName "FIFO.ipc"

// pipeServer.cc文件主要作为接收端,该端要创建管道文件,运行结束后要删除管道文件
#include "comm.hpp"

int main()
{
    umask(0); // 设置创建文件临时的权限掩码
    // 1、创建管道文件
    int n = mkfifo(PipeName, 0666);
    if(n == -1)
    {
        perror("mkfifo");
        exit(1);
    }
    // 2、打开管道文件
    int fd = open(PipeName, O_RDONLY, 0666);
    if(fd == -1)
    {
        perror("open");
        exit(2);
    }
    // 3、通信
    char buf[1024];
    while(true)
    {
        ssize_t sz = read(fd, buf, sizeof(buf) - 1);
        buf[sz] = '\0'; // 去掉默认换行
        if(strcmp(buf, "quit") == 0)
        {
            std::cout << "client quit, me too" << std::endl;
            break;
        }
        std::cout << "client# " << buf << std::endl;
    }
    // 4、关闭管道文件
    close(fd);
    // 5、删除管道文件
    unlink(PipeName);

    return 0;
}
// pipeClient.cc文件作为通信的发送端实现,主要向服务端发送请求
#include "comm.hpp"

int main()
{
    // 1、打开管道文件
    int fd = open(PipeName, O_WRONLY, 0666);
    if(fd == -1)
    {
        perror("open");
        exit(2);
    }
    // 2、通信
    std::string buf;
    while(true)
    {
        std::getline(std::cin, buf);
        write(fd, buf.c_str(), buf.size());
    }
    // 3、关闭管道文件
    close(fd);
    return 0;
}

        我们发现,只要我们将管道文件创建好,其他的就像我们操作普通文件一样简单;命名管道的使用比匿名管道会容易很多;

注意:这里有一个细节,在我们使用管道文件时,若我们服务端使用open打开管道文件时,此时若我们的客户端没有调用open函数打开管道文件,我们的服务端会一只卡在open函数内,直至我们客户端也使用open函数打开管道文件,这时我们的服务端open函数才会返回文件描述符;这个细节在我们后面共享内存的一份代码中有一定重要的作用;

三、共享内存

1、共享内存的原理

        之前在讲解进程地址空间时,我们曾经讲过,堆栈中间有一块共享区,我们之前的动态库就是会被映射到这块共享区中;而今天我们的主角,共享内存的原理也与这块空间有关;进程间通信的本质就是看到同一块内存空间,而我们共享内存实现进程间通信的方案就是我们首先在内存中申请一块空间,然后将我们需要进程间通信的进程与这块物理空间进行关联,映射到自己进程地址空间的共享区中;这样便可以实现进程间通信了;如下图所示;

        这样进程间通信就只需要往自己的进程地址空间的某个位置写入读取即可;

2、测试代码

        在使用共享内存实现进程间通信前,我们需要学习下面几个函数接口;

shmget:申请一块共享内存空间并返回对应的shmid / 获取一块共享内存空间的shmid;

参数一:通过key值获取或者申请一块共享内存空间,每块共享内存空间的key值不同;

参数二:申请 / 获取共享内存空间的大小

参数三:标志位,通常是由 IPC_CREAT 或 IPC_EXCL 这两个宏加上权限组成;

返回值:若函数调用成功则返回 shmid,这个也就是共享内存的句柄,与fd类似;若调用失败,则返回-1,错误码被设置;

注意:

1、关于参数三,我们若想获取某个key对应的shmid,我们直接填0即可;若某个key对应的共享内存空间不存在,我们需要创建,则我们一般会填 IPC_CREAT | IPC_EXCL | 0666;其中0666为共享内存空间的访问权限;

IPC_CREAT:若共享内存空间不存在,则创建之;

IPC_EXCL:通常配合上面IPC_CREAT使用,若共享空间存在,则报错返回-1;因此这两个选项配合使用可以保证获得到的共享内存是一个新创建的共享内存!

2、关于上述的参数一key,我们可以通过下面这个函数来获取;

ftok:通过路径和项目id生成唯一key值;

参数一:项目路径,这里可以随便填写一个;

参数二:项目id,这里也可以自己设置一个;

返回值:若函数调用成功,则返回key值,若失败,则返回-1,错误码被设置;

shmat:是共享内存空间与当前虚拟地址空间进行相关联,建立映射关系;

参数一:shmid值,之前我们在shmget中获取的 id;

参数二:我们想要与哪一块虚拟地址空间绑定,建立映射关系;这里通常填NULL,表示让OS系统随机分配一块空间建立映射关系;

参数三:一般设置为0,可不关心;

返回值:若调用成功,则返回与共享内存建立映射的虚拟地址,若调用使用,则返回-1,错误码被设置,注意这里的-1被强制装换成了 void* 类型;

shmdt:将共享内存与我们的共享内存去关联,与我们的shmat相对应;

参数一:要去关联的共享内存的地址,也就是shmat的返回值;

返回值:若函数调用成功,则返回0,若失败,则返回-1,错误码被设置;

shmctrl:共享内存的控制

参数一:shmid值;

参数二:这里有三个宏来控制这个函数的行为,我们一般选择IPC_RMID,表示我们要删除这块共享内存空间;

参数三:填NULL即可;

返回值:若调用成功则返回0,若失败则返回-1,错误码被设置;

        学习上述的四个函数我们就可以完成进程间通信了,上面四个函数基本囊括了使用共享内存进行进程间通信的整个过程,首先调用ftok获取key值,然后再通过shmget 创建共享内存 / 获取shmid,然后通过shmat 与当前进程的虚拟地址空间进行关联,接着就可以开始进行通信了,通信完毕后,我们使用 shmdt 将虚拟地址与共享内存去关联;最后我们使用shmctl删掉申请的共享内存空间;

        接着我们需要学习一些命令行来获取共享内存相关信息,我们可以通过 ipcs -m 来获取当前机器的共享内存申请使用情况;使用 ipcrm -m shmid 来删除指定的共享内存;下面为我们使用共享内存进行进程间通信的代码;

// comm.hpp 文件用于保存一些共享代码

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <sys/shm.h>
#include <unistd.h>

// 形成key所需路径
#define pathName "/home/zsw/linuxCode/shm"
// 形成key所需项目id
#define proj_id 0x13
// 共享内存大小
#define SIZE 4096


// shmServer.cc 文件实现服务端接收客户端发送信息,其中服务端承担创建共享内存,删除共享内存的任务

#include "comm.hpp"

int main()
{
    // 1、生成唯一key
    int k = ftok(pathName, proj_id);
    if(k == -1)
    {
        perror("ftok");
        exit(1);
    }
    std::cout << "ftok success\n";
    // 2、创建新的共享空间
    int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666); // 创建一个新的共享内存,k存在,则创建失败
    if(shmid == -1)
    {
        perror("shmget");
        exit(2);
    }
    std::cout << "shmget success\n";

    // 3、与共享空间相关联
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);
    if(*(int*)shmaddr == -1)
    {
        perror("shmat");
        // 退出前释放共享空间
        shmctl(shmid, IPC_RMID, nullptr);
        exit(3);
    }
    std::cout << "shmat success\n";

    // 4、通信(读)
    while(true)
    {
        printf("%s\n", shmaddr);
        if(shmaddr[0] == 'z')
            break;
        sleep(1);
    }
    
    // 5、去关联
    int n = shmdt(shmaddr);
    if(n == -1)
    {
        perror("shmdt");
        exit(4);
    }
    std::cout << "shmdt success\n";

    // 6、释放共享空间
    shmctl(shmid, IPC_RMID, nullptr);
    std::cout << "shm rm success\n";

    return 0;
}
// shmClient.cc 文件用于客户端发送信息给服务端,其中仅需对共享内存关联,通信、去关联等操作即可

#include "comm.hpp"

int main()
{
    // 1、获取唯一key
    int k = ftok(pathName, proj_id);
    if(k == -1)
    {
        perror("ftok");
        exit(1);
    }
    // 2、获取共享内存
    int shmid = shmget(k, SIZE, 0);
    if(shmid == -1)
    {
        perror("shmget");
        exit(2);
    }
    // 3、关联
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);
    if(*(int*)shmaddr == -1)
    {
        perror("shmat");
        exit(3);
    }
    // 4、通信(写)
    char ch = 'a';
    while(ch <= 'z')
    {
        shmaddr[0] = ch;
        ch++;
        sleep(1);
    }
    // 5、去关联
    int n = shmdt(shmaddr);
    if(n == -1)
    {
        perror("shmdt");
        exit(4);
    }
    return 0;
}


        我们编译运行服务端代码,如下所示;

        确实,我们的服务端一直读取数据,可是读取到的数据为空,此时我们并没有运行客户端,且客户端也不可能进行输出发送;可我们依旧可以读取数据,只不过数据为空罢了,这就与我们的管道通信有了本质的区别,共享内存的进程间通信并没有访问控制!

        接着我们通过ipcs -m 查找我们创建的共享内存,确实存在,其shmid为25,key就是我们通过 ftok 生成的key,owner就是共享内存的拥有者,也就是当前用户,perms就是这块共享内存的访问权限,我们设置成了 666,4096就是我们申请这块共享内存的大小,nattach就是关联到这块共享内存空间的进程数,status就是这块共享内存的状态;

        我们接着启动客户端;如下所示;

        我们发现服务端已经收到客户端发来的信息,并且我们的nattach的数量也由1变成了2,随后当客户端发送完26个英文字母后退出,此时服务端也读取到了字符z,两个进行相继推出,nattach也由2变成了0,共享内存被删除;

        前面的实验,我们也不难发现,我们的共享内存不具备访问控制,那我们要是想使用共享内存实现类似管道的访问控制是否可以做到呢?其实也不难,我们可以通过管道来实现,如下面的代码;

// comm.hpp 文件

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <sys/shm.h>
#include <unistd.h>
#include <fcntl.h>

// 形成key所需路径
#define pathName "/home/zsw/linuxCode/shm/shm_pipe"
// 形成key所需项目id
#define proj_id 0x14
// 共享内存大小
#define SIZE 4096
// 管道文件名
#define pipeName "./fifo.ipc"

// 负责创建与销毁管道文件
class Init
{
public:
    Init()
    {
        umask(0);
        int n = mkfifo(pipeName, 0666);
        if(n == -1)
        {
            perror("mkfifo");
            exit(5);
        }
    }
    ~Init()
    {
        unlink(pipeName);
    }
};

// 通过read阻塞来形成等待效果
void wait(int fd)
{
    int tmp = 1;
    ssize_t sz = read(fd, &tmp, sizeof(int));
    if(sz != sizeof(tmp))
    {
        std::cerr << "fd: " << fd << " 等待错误\n";
        printf("sz:%d, sizeof(tmp):%d\n", sz, sizeof(tmp));
        perror("wait");
        exit(6);
    }
}

// 通过write唤醒等待进程
void signal(int fd)
{
    int tmp = 1;
    ssize_t sz = write(fd, &tmp, sizeof(tmp));
    if(sz != sizeof(int))
    {
        std::cerr << "write err\n";
        exit(7);
    }
}
// shmServer.cc

#include "comm.hpp"

int main()
{
    Init init; // 创建管道文件
    // 打开管道文件
    int fd = open(pipeName, O_RDONLY, 0666);

    // 1、生成唯一key
    int k = ftok(pathName, proj_id);
    if(k == -1)
    {
        perror("ftok");
        exit(1);
    }
    std::cout << "ftok success, k:" << k << std::endl;
    // 2、创建新的共享空间
    int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666); // 创建一个新的共享内存,k存在,则创建失败
    if(shmid == -1)
    {
        perror("shmget");
        exit(2);
    }
    std::cout << "shmget success\n";
    // 3、与共享空间相关联
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);
    if(*(int*)shmaddr == -1)
    {
        perror("shmat");
        // 退出前释放共享空间
        shmctl(shmid, IPC_RMID, nullptr);
        exit(3);
    }
    std::cout << "shmat success\n";
    sleep(5);
    // 打开管道文件
    int fd = open(pipeName, O_RDONLY, 0666);
    // 4、通信(读)
    while(true)
    {
        wait(fd);
        printf("%s\n", shmaddr);
    }
    
    // 5、去关联
    int n = shmdt(shmaddr);
    if(n == -1)
    {
        perror("shmdt");
        exit(4);
    }
    std::cout << "shmdt success\n";

    // 6、释放共享空间
    shmctl(shmid, IPC_RMID, nullptr);
    std::cout << "shm rm success\n";

    return 0;
}
// shmClient.cc文件

#include "comm.hpp"

int main()
{
    // 小坑:不能在这打开管道文件,因为打开管道文件有一个特性,如果对端不打开,这里将会一直阻塞
    // 然后在后续过程中,若客户端进程先执行,下面shmget函数可能出现文件未创建的错误,因为客户端
    // 执行的比服务端快,服务端还未创建共享内存,而客户端就想访问了
    // int fd = open(pipeName, O_WRONLY, 0666);

    // 1、获取唯一key
    int k = ftok(pathName, proj_id);
    if(k == -1)
    {
        perror("ftok");
        exit(1);
    }
    std::cout << "ftok success, k:" << k << std::endl;
    // 2、获取共享内存
    int shmid = shmget(k, SIZE, 0);
    if(shmid == -1)
    {
        perror("shmget");
        exit(2);
    }
    std::cout << "shmget success\n";
    // 打开管道文件
    int fd = open(pipeName, O_WRONLY, 0666);
    // 3、关联
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);
    if(*(int*)shmaddr == -1)
    {
        perror("shmat");
        exit(3);
    }
    std::cout << "shmat success\n";
    // 4、通信(写)
    std::string msg;
    while(true)
    {
        std::cout << "Enter message> ";
        getline(std::cin, msg);
        strcpy(shmaddr, msg.c_str());
        signal(fd);
    }
    // 5、去关联
    int n = shmdt(shmaddr);
    if(n == -1)
    {
        perror("shmdt");
        exit(4);
    }
    std::cout << "shmdt success\n";
    return 0;
}

        这段代码就有我们前面所说管道的一个性质,并且,这里有一个小坑;正常情况下,我们一般先运行服务端代码,因为我要保证客户端代码在获取 shmid 时,已经被创建了,因此在测试代码一(不加管道代码)时,我们总是先运行服务端代码;

        如上图所示,若我们将打开管道文件的代码放到最上面,也就是最先执行,此时,当我们的服务端打开管道文件时,由于对端未打开管道文件,因此,我们会阻塞住,此时我们接着运行客户端代码,由于客户端打开管道文件时,由于对端已经在等待了,因此可以直接返回管道文件对应文件描述符,此时若我们的客户端继续运行,当调用shmget获取shmid时,由于key所对应的共享内存并未创建,因此我们的客户端会直接运行失败;

3、共享内存的特性

1、共享内存仅需内存级读写即可,与管道不同,使用管道需要调用系统调用read、write等;而共享内存仅需往指定虚拟地址空间写入、读取即可;

2、共享内存是最快的通信方式,因为拷贝次数最少;写入最少拷贝仅需从键盘文件对应缓冲区拷贝到共享内存中即可,写入仅需从共性内存中拷贝到要写入的缓冲区即可;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1216705.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

线程的面试八股

Callable接口 Callable是一个interface,相当于给线程封装了一个返回值,方便程序猿借助多线程的方式计算结果. 代码示例: 使用 Callable 版本,创建线程计算 1 2 3 ... 1000, 1. 创建一个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型…

高级数据结构——树状数组

树状数组&#xff08;Binary Index Tree, BIT&#xff09;&#xff0c;是一种一般用来处理单点修改和区间求和操作类型的题目的数据结构&#xff0c;时间复杂度为O(log n)。 对于普通数组来说&#xff0c;单点修改的时间复杂度是 O(1)&#xff0c;但区间求和的时间复杂度是 O(n…

【备忘】websocket学习之挖坑埋自己

背景故事 以前没有好好学习过websocket&#xff0c;只知道它有什么用途&#xff0c;也知道是个好东西&#xff0c;平时在工作中没用过&#xff0c;所以对它并不知所以然。如今要做个自己的项目&#xff0c;要在付款的时候实时播报声音。自己是个开发者&#xff0c;也不想用别人…

类加载中的执行顺序

结论&#xff1a; 先静态再实例 实例化一个子类(这个颜色主要是实例化会执行的部分)&#xff1a; 父类静态属性&#xff0d;&#xff1e;父类静态代码块&#xff0d;&#xff1e;子类静态属性&#xff0d;&#xff1e;子类静态代码块&#xff0d;&#xff1e;父类代码块&…

jQuery【jQuery树遍历、jQuery动画(一)、jQuery动画(二)】(四)-全面详解(学习总结---从入门到深化)

目录 jQuery树遍历 jQuery动画(一) jQuery动画(二) jQuery树遍历 1、 .children() 获得子元素&#xff0c;可以传递一个选择器参数 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-…

【Effective C++ 笔记】(四)设计与声明

【四】设计与声明 条款18 &#xff1a; 让接口容易被正确使用&#xff0c;不易被误用 Item 18: 让接口容易被正确使用&#xff0c;不易被误用 Make interfaces easy to use correctly and hard to use incorrectly. “让接口容易被正确使用&#xff0c;不易被误用”&#xff0…

OpenCV基础应用(3)— 把.png图像保存为.jpg图像

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。本节课就手把手教你如何把.png图像保存为.jpg图像&#xff0c;希望大家学习之后能够有所收获~&#xff01;&#x1f308; 目录 &#x1f680;1.技术介绍 &#x1f680;2.实现代码 &#x1f680;1.技术介绍 如果在电脑某…

web缓存-----squid代理服务

squid相关知识 1 squid的概念 Squid服务器缓存频繁要求网页、媒体文件和其它加速回答时间并减少带宽堵塞的内容。 Squid代理服务器&#xff08;Squid proxy server&#xff09;一般和原始文件一起安装在单独服务器而不是网络服务器上。Squid通过追踪网络中的对象运用起作用。…

渗透测试--实战若依ruoyi框架

免责声明&#xff1a; 文章中涉及的漏洞均已修复&#xff0c;敏感信息均已做打码处理&#xff0c;文章仅做经验分享用途&#xff0c;切勿当真&#xff0c;未授权的攻击属于非法行为&#xff01;文章中敏感信息均已做多层打马处理。传播、利用本文章所提供的信息而造成的任何直…

YOLOv3 学习记录

文章目录 简介整体介绍整体架构图 网络架构的改进Backbone 的改进FPNAnchor 机制 坐标表示与样本匹配目标边界框的预测正负样本匹配 损失函数 简介 关注目标在哪里 目标是什么 目标检测的发展路径&#xff1a; proposal 两阶段 --> anchor-base/ anchor-free --> nms f…

SpringCloud微服务:Nacos的集群、负载均衡、环境隔离

目录 集群 在user-service的yml文件配置集群 启动服务 负载均衡 order-service配置集群 设置负载均衡 当本地集群的服务挂掉时 访问权重 环境隔离 1、Nacos服务分级存储模型 一级是服务&#xff0c;例如userservice 二级是集群&#xff0c;例如杭州或上海 …

计算机网络的发展

目录 一、计算机网络发展的四个阶段 1、第一阶段&#xff1a;面向终端的计算机网络&#xff08;20世纪50年代&#xff09; 2、第二阶段&#xff1a;计算机—计算机网络&#xff08;20世纪60年代&#xff09; 3、第三阶段&#xff1a;开放式标准化网络&#xff08;20世纪70年…

【2023最全教程】python+appium自动化测试元素定位(建议收藏)

关于app自动化测试&#xff0c;元素定位工具有三个&#xff1a; appium自带的Appium Inspector工具 Android ADT原生的工具 python版uiautomator2中的weditor 由于我常用的是前两个&#xff0c;所以下面只介绍前面两种元素定位工具&#xff08;以下内容中均以微博为例子&am…

idea中把spring boot项目打成jar包

打jar包 打开项目&#xff0c;右击项目选中Open Module Settings进入project Structure 选中Artifacts&#xff0c;点击中间的加号&#xff08;Project Settings->Artifacts->JAR->From modules with dependencies &#xff09; 弹出Create JAR from Modules&#…

OpenCV基础应用(4)— 如何改变图像的透明度

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。本节课就手把手教你如何改变图像的透明度&#xff0c;希望大家学习之后能够有所收获~&#xff01;&#x1f308; 目录 &#x1f680;1.技术介绍 &#x1f680;2.实现代码 &#x1f680;1.技术介绍 改变图像透明度的实…

从内网到公网:使用Axure RP和内网穿透技术发布静态web页面的完整指南

文章目录 前言1.在AxureRP中生成HTML文件2.配置IIS服务3.添加防火墙安全策略4.使用cpolar内网穿透实现公网访问4.1 登录cpolar web ui管理界面4.2 启动website隧道4.3 获取公网URL地址4.4. 公网远程访问内网web站点4.5 配置固定二级子域名公网访问内网web站点4.5.1创建一条固定…

振南技术干货集:比萨斜塔要倒了,倾斜传感器快来!(1)

注解目录 1、倾斜传感器的那些基础干货 1.1 典型应用场景 &#xff08;危楼、边坡、古建筑都是对倾斜敏感的。&#xff09; 1.2 倾斜传感器的原理 1.2.1 滚珠式倾斜开关 1.2.2 加速度式倾斜传感器 1)直接输出倾角 2)加速度计算倾角 3)倾角精度的提高 &#xff08;如果…

JS-项目实战-新增水果库存功能实现

1、fruit.js function $(name) {if (name) {//假设name是 #fruit_tblif (name.startsWith("#")) {name name.substring(1); //fruit_tblreturn document.getElementById(name);} else {return document.getElementsByName(name); //返回的是NodeList类型}} }//当…

一言成文大模型:大模型实践落地之路

百度CEO&#xff0c;李彦宏指出、深度学习技术&#xff0c;大语言模型具备了理解、生成、逻辑、记忆等人工智能的核心基础能力&#xff0c;为通用人工智能带来曙光。 元宇宙_一言成文大模型 一言成文大模型&#xff1a;大模型实践落地之路

光伏含氟废水吸附处理

#光伏含氟废水吸附处理 氟的来源是冰晶石、萤石、氟磷灰等矿物&#xff0c;在钢铁、有色金属冶炼、铝、玻璃、化肥等工业领域得到广泛应用。 目前&#xff0c;在太阳能板生产中&#xff0c;一项关键工艺就是将氟化氢溶液浸泡在硅片上&#xff0c;以除去表面的磷硅玻璃&#xf…