本文介绍: 想象现实中的管道,它一定是有一个出口有一个入口,用来传输资源资源只能从一端流向另一端进程间通信管道也同理,一个进程发送数据一个进程接收数据管道是由操作系统所提供的最古老的进程间通信的形式。以上就是今天要讲的内容本文仅仅简单介绍进程间通信管道共享内存

目录

一、进程间通信背景

1、进程间通信的理解

2、进程间通信的目的

3、进程间的必要性

二、管道

1、什么是管道

2、匿名管道

3、命名管道

4、管道通信的特点

三、System V IPC

1、共享内存

2、进程互斥

总结


一、进程通信背景

1、进程通信理解

进程运行具有独立性,进程想要直接进行进行通信难度比较大,进程间通信的前提是让不同资源能够看到同一块资源。

2、进程间通信的目的

资源共享多个进程之间共享同样的资源。
通知事件一个进程需要向另一个或一组进程发送消息通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变。

3、进程间的必要性

单进程无法使用并发能力,无法实现多进程协同传输数据同步执行流,消息通知等。

二、管道

1、什么管道

想象现实中的管道,它一定是有一个出口有一个入口,用来传输资源,资源只能从一端流向另一端

进程间通信的管道也同理,一个进程发送数据另一个进程接收数据

管道是由操作系统所提供的最古老的进程间通信的形式。

2、匿名管道

匿名管道只适用于具有亲缘关系的进程间通信——通常用于父子进程间的通信

一个进程创建子进程,父进程打开文件也会被子进程所继承,因为是同一个文件,所以父子进程打开的同一个文件具有相同的struct file,因此父子进程就同时看到了同一份内存资源,也就可以借助这个文件来进行父子进程间的通信。

匿名管道创建步骤

1、分别以读写方式打开同一个文件

2、fork()创建子进程

3、双方进程各自关闭自己需要的文件描述符

管道文件是不需要刷新磁盘的,它是专门用来进行进程间通信的文件,刷新磁盘,相当于进行IO使进程间通信的效率降低

我们可以使用pipe创建匿名管道,这里要传入一个数组,它是输出线参数

下标为0的代表读端,1代表写端

 这样前置工作准备好了

接下来以一个比较复杂例子演示匿名管道

写一个简单的单机版负载均衡

创建一个进程池,父进程通过匿名管道随机向子进程派发任务

//task.hpp
#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <functional&gt;
#include <unordered_map&gt;
#include <cassert&gt;
#include <ctime>
#include <sys/wait.h>
#include <unistd.h>

typedef std::function<void()> func;
std::vector<func> callbacks;
std::unordered_map<int, std::string> desc;

void readMySQL()
{
    std::cout << "sub process [ " << getpid() << " ] 执行访问数据库任务n" << std::endl;
}

void execuleUrl()
{
    std::cout << "sub process [ " << getpid() << " ] 执行URL解析n" << std::endl; 
}

void cal()
{
    std::cout << "sub process [ " << getpid() << " ] 执行加密任务n" << std::endl; 
}

void save()
{
    std::cout << "sub process [ " << getpid() << " ] 执行持久任务n" << std::endl;
}

void Load()
{
    desc.emplace(callbacks.size(), "readSQL: 读取数据库");
    callbacks.emplace_back(readMySQL);

    desc.emplace(callbacks.size(), "execuleURL: 进行url解析");
    callbacks.emplace_back(execuleUrl);

    desc.emplace(callbacks.size(), "cal: 进行加密计算");
    callbacks.emplace_back(cal);

    desc.emplace(callbacks.size(), "save: 进行数据的文件保存");
    callbacks.emplace_back(save);
}

void showHandler()
{
    for(const auto&amp; iter : desc)
    {
        std::cout << iter.first << "t" << iter.second << std::endl;
    }
}

int handlerSize()
{
    return callbacks.size();
}


//main.cpp
#include "task.hpp"

#define PROCESS_NUM 5

// 父进程给子进程派发任务

int waitCommind(int waitFd, bool &amp;quit)
{
    uint32_t command = 0;
    ssize_t s = read(waitFd, &amp;command, sizeof(command));
    if (s == 0)
    {
        quit = true;
        return -1;
    }

    assert(s == sizeof(uint32_t));
    return command;
}

void sendAndWakeUp(pid_t who, int fd, uint32_t command)
{
    write(fd, &amp;command, sizeof(command));
    std::cout << "main process: call process " << who << "execute" << desc[command] << "through" << fd << std::endl;
}

int main()
{
    Load();

    // 简单线程池
    // 进程pid 及 文件描述符
    std::vector<std::pair<pid_t, int>> slots;

    // 创建子进程
    for (size_t i = 0; i < PROCESS_NUM; i++)
    {
        // 1、以读写方式打开文件
        int fd[2] = {0};
        int n = pipe(fd);
        assert(n == 0);

        pid_t id = fork();
        if (id < 0)
        {
            std::cerr << "fork" << std::endl;
        }
        else if (id == 0)
        {
            // child
            // 关闭写端
            close(fd[1]);
            while (true)
            {
                bool quit = false;
                int command = waitCommind(fd[0], quit);
                if (quit == true)
                {
                    break;
                }

                if (command >= 0 &amp;&amp; command < handlerSize())
                {
                    callbacks[command];
                }
                else
                {
                    std::cout << "非法command" << std::endl;
                }
            }
            exit(1);
        }
        else
        {
            // father
            // 关闭读端
            close(fd[0]);
            slots.emplace_back(id, fd[1]);
        }
    }

    // 派发任务
    size_t count = 0;
    srand(time(0));
    while (true)
    {
        int command = rand() % handlerSize();

        int choice = rand() % slots.size();

        sendAndWakeUp(slots[choice].first, slots[choice].second, command);
        sleep(1);
        count++;

        if (count == 10)
        {
            break;
        }
    }

    // 关闭文件描述符
    for (const auto &amp;slot : slots)
    {
        close(slot.second);
    }

    // 回收子进程
    for (const auto &amp;slot : slots)
    {
        waitpid(slot.first, nullptr, 0);
    }

    return 0;
}

3、命名管道

命名管道与匿名管道类似,不过命名管道可以让不具有亲缘关系的进程通信

首先也是要让不同的进程看到同一份资源,双方进程就可以通过管道文件的路径看到同一份资源

管道文件首先是一个文件,它是有名字的可以被打开,但是不会将内存数据进行刷新到磁盘

mkfifo命名可以创建一个管道文件

我们通过echo命令hello通过管道文件发送过去,echo命令进入阻塞状态

这时通过管道文件就可以读取资源了

这样就完成了echocat的通信

删除管道文件可以使用unlink或者rm

 接下来代码方式创建管道文件

mkfifo可以创建管道文件

成功返回0,失败返回-1

 接下来还是举一个例子演示命名管道进程间通信

server创建管道文件,从client读取资源,结束通信后,client关闭管道文件并且删除管道文件

client获取管道文件,进行正常通信,向server发送数据

//comm.hpp
#pragma once

#include <iostream>
#include <string>
#include <cassert>
#include <ctime>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>

#define MODE 0666
#define SIZE 128
#define PROCESS_NUM 3

std::string ipcPath = "./fifo.ipc";



//log.hpp
#include <iostream>
#include <string>
#include <ctime>

#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3

const std::string msg[] = {
    "Debug",
    "Notice",
    "Warning",
    "Error"};

std::ostream &amp;Log(std::string message, int level)
{
    std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
    return std::cout;
}


//server.cpp
#include "log.hpp"
#include "comm.hpp"

static void GetMessage(int fd)
{
    char buffer[SIZE];
    while (true)
    {
        ssize_t s = read(fd, buffer, sizeof(buffer));
        if (s > 0)
        {
            buffer[s] = '';
            std::cout << "client call" << buffer << std::endl;
        }
        else if (s == 0)
        {
            std::cerr << "[ " << getpid() << " ]"
                      << "read end of file server quit" << std::endl;
            break;
        }
        else
        {
            std::cerr << "read" << std::endl;
            break;
        }
    }
}

int main()
{
    // 1、创建管道文件
    if (mkfifo(ipcPath.c_str(), MODE) < 0)
    {
        std::cerr << "mkfifo" << std::endl;
        exit(1);
    }

    Log("创建管道文件成功", Debug) << "step 1" << std::endl;

    // 2、正常文件操作
    int fd = open(ipcPath.c_str(), O_RDONLY);
    if (fd < 0)
    {
        std::cerr << "open" << std::endl;
    }

    Log("打开文件成功", Debug) << "step 2" << std::endl;

    // 3、正常通信代码
    // 使用进程池

    Log("进程间开始通信", Debug) << "step 3" << std::endl;

    for (size_t i = 0; i < PROCESS_NUM; i++)
    {
        pid_t id = fork();
        if (id < 0)
        {
            std::cerr << "fork" << std::endl;
        }
        else if (id == 0)
        {
            GetMessage(fd);
            exit(1);
        }
    }

    Log("进程间通信完成", Debug) << "step 4" << std::endl;

    // 4、回收子进程

    for (size_t i = 0; i < PROCESS_NUM; i++)
    {
        waitpid(-1, nullptr, 0);
    }

    Log("等待子进程成功", Debug) << "step 5" << std::endl;

    // 5、关闭文件
    close(fd);

    Log("关闭文件成功", Debug) << "step 6" << std::endl;

    // 5、删除文件
    unlink(ipcPath.c_str());
    Log("删除管道文件成功", Debug) << "step 7" << std::endl;

    return 0;
}


//clinet.cpp
#include "comm.hpp"

int main()
{
    //1、获取管道文件
    int fd = open(ipcPath.c_str(), O_WRONLY);
    if(fd < 0)
    {
        std::cerr << "open" << std::endl;
        exit(1);
    }

    //2、IPC过程
    std::string buffer;
    while(true)
    {
        std::cout << "Please Enter Mseeage Line:> ";
        std::getline(std::cin, buffer);
        if(buffer == "quit")
        {
            break;
        }
        write(fd, buffer.c_str(), buffer.size());
    }

    //3、关闭管道文件,server自动停止读取
    close(fd);
    return 0;
}

命名管道多个进程竞争读取,没有任何问题发送数据大小是小于4096字节就是原子

4、管道通信的特点

1、管道具有通过让进程间协同,提供了访问控制

2、管道提供的是面向流式的通信服务——面向节流——协议

3、管道是基于文件的,文件的声明周期是随进程的,管道的声明周期是随进程的

4、管道是单向通信的,就是半双工通信的一种特殊情况

管道的访问控制

1、写快,读慢,写满就不能再写了

2、写慢,读快,管道没有数据的时候,读必须等待

3、写关,读0,标识读到了文件结尾

4、读关,写继续,OS终止写进程

三、System V IPC

1、共享内存

还要说一下通信的前提:让不同的进程能够看到同一份资源共享内存也是同样道理

不过它是直接向OS申请一块空间然后映射到进程的共享区,这样就使得进程间通信的效率提升,因为不用使用系统调用多次拷贝资源了,进程可以直接访问那块共享内存资源

1)申请空间

2)建立映射(多个进程映射同一块共享内存)

3)通信

4)去掉映射

5)释放空间


共享内存的提供者是操作系统操作系统也要管理共享内存,通过先描述组织

共享内存 = 共享内存块 + 对应的共享内存的内核数据结构

我们可以使用shmget函数申请共享内存

它的返回值shmid是共享内存的用户标识符,类似于文件描述符fd

它的最后一个参数有两个选项IPC_CREAT and IPC_EXCL 

一般两者是组合使用的,单独使用IPC_EXCL是没有意义的

单独使用IPC_CREAT:如果创建共享内存,底层已经存在,那么就获取它,不存在就创建它

两者组合使用:如果底层存在就创建它,并且返回新创建的shmid,如果底层存在,出错返回

接下来就是shmget的第一个参数key

key可以保证与我们进程通信的进程就是要通信的进程,并且能够看到我创建的共享内存

我们可以使用ftok函数来创建keyftok是一种加密算法,使用同样的算法规则,传入的参数相同就能够形成唯一值,ftok第一个参数pathname最好设定我们有访问权限路径

shmid vs key

只有创建的时候使用key,大部分情况用户访问共享内存,都是使用的shmid

当我们的程序运行结束,我们的共享内存,还存在

System V IPC资源的生命周期内核

解决办法

1、手动删除

ipcs -m可以获取共享内存相关属性

使用命令 ipcrm -m + shmid  可以删除共享内存 

2、代码删除

shmctl + 选项IPC_RMID可以删除共享内存 

共享内存链接方法

使用shmat可以将共享内存与进程建立映射,这个函数类似于malloc返回值void*可以强制转换char*当作数组来使用共享内存

共享内存去关联方法

去除关联方式是使用shmdt

注意:创建共享内存的大小最好是页(4096bytes)的整数

如果你申请4097个字节操作系统会给你申请4096 * 2bytes,剩下的4095字节浪费

总结

shmget返回值是用户层标识符

key是内核层面标定共享内存的标识符

想要自己挂接共享内存等对共享内存的操作要使用shmid,同时共享内存的生命周期是随内核

shmat:挂接共享内存

shmdt:去关联

shmctl :删除共享内存

共享内存是在堆栈之间的共享区,它是属于用户的

用户空间:不用经过系统调用就可以访问的空间

管道的通信方式是文件,它是在内核空间中,它是内核的一种特定的数据结构,是操作系统维护的在[3G, 4G]的内核空间中,用户访问需要使用系统调用

下面看一个简单例子

//comm.hpp
#pragma once

#include <iostream>
#include <string>
#include <cassert>
#include <cstring>
#include "../named_pipe/log.hpp"
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <fcntl.h>

#define FIFO_NAME "./fifo"
#define PROJ_ID 0x66
#define SHM_SIZE 4096
#define PATH_NAME "/home/ww"

class Init
{
public:
    Init()
    {
        umask(0);
        int n = mkfifo(FIFO_NAME, 0666);
        assert(n == 0);
        Log("创建管道文件成功", Notice) << "n";
    }

    ~Init()
    {
        unlink(FIFO_NAME);
        Log("删除管道文件成功", Notice) << "n";
    }
};

#define READ O_RDONLY
#define WRITE O_WRONLY

int OpenFIFO(std::string pathname, int falgs)
{
    int fd = open(pathname.c_str(), falgs);
    assert(fd >= 0);
    return fd;
}

void Wait(int fd)
{
    Log("等待中", Notice) << "n";
    uint32_t tmp = 0;
    ssize_t s = read(fd, &amp;tmp, sizeof(uint32_t));
    assert(s == sizeof(uint32_t));
}

void Signal(int fd)
{
    uint32_t tmp = 0;
    ssize_t s = write(fd, &amp;tmp, sizeof(tmp));
    assert(s == sizeof(uint32_t));
    Log("唤醒中", Notice) << "n";
}

void CloseFIFO(int fd)
{
    close(fd);
}


//server.cpp
#include "comm.hpp"

Init init; 

int main()
{
    // 1、生成相同的key
    key_t key = ftok(PATH_NAME, PROJ_ID);

    Log("生成key成功", Notice) << "key: " << key << std::endl;

    // 2、申请共享内存
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid < 0)
    {
        Log("申请共享内存失败", Error) << "shmid: " << shmid << std::endl;
        exit(2);
    }

    Log("申请共享内存成功", Notice) << "shmid: " << shmid << std::endl;
    // 3、共享内存与进程建立映射

    char *adder = (char *)shmat(shmid, nullptr, 0);

    Log("链接共享内存成功", Notice) << "shmAdder: " << adder << std::endl;

    // 4、正常通信
    Log("开始通信", Notice) << std::endl;
    // 共享内存是没有访问控制的,使用管道进行管道控制

    int fd = OpenFIFO(FIFO_NAME, READ);
    while (true)
    {
        Wait(fd);
        printf("%sn", adder);
        if (strcmp(adder, "quit") == 0)
            break;
    }

    // 5、取消映射

    int ret = shmdt(adder);
    if (ret < 0)
    {
        Log("共享内存去关联失败", Error) << std::endl;
    }

    // 6、删除共享内存

    ret = shmctl(shmid, IPC_RMID, nullptr);
    if (ret < 0)
    {
        Log("删除共享内存失败", Error) << std::endl;
        exit(3);
    }
    return 0;
}



//client.cpp
#include "comm.hpp"

int main()
{
    // 1、生成相同的key
    key_t key = ftok(PATH_NAME, PROJ_ID);
    Log("生成key成功", Notice) << "key: " << key << std::endl;

    // 2、获取共享内存
    int shmid = shmget(key, SHM_SIZE, 0);
    if (shmid < 0)
    {
        Log("获取共享内存失败", Error) << "shmid: " << shmid << std::endl;
        exit(2);
    }

    // 3、建立映射
    char *adder = (char *)shmat(shmid, nullptr, 0);

    Log("链接共享内存成功", Notice) << "shmAdder: " << adder << std::endl;

    // 4、通信
    Log("开始通信", Notice) << std::endl;

    int fd = OpenFIFO(FIFO_NAME, WRITE);
    while (true)
    {
        ssize_t s = read(0, adder, SHM_SIZE - 1);
        if (s > 0)
        {
            adder[s - 1] = 0;
            Signal(fd);
            if (strcmp(adder, "quit") == 0)
                break;
        }
    }

    // 5、取消映射
    int ret = shmdt(adder);
    if (ret < 0)
    {
        Log("取消共享内存失败", Error) << std::endl;
    }

    return 0;
}

2、进程互斥

为了让进程间通信,让不同的进程之间看到同一份资源,之前所说的所有通信方式,本质都是优先解决一个问题,让不同的进程看到同一份资源

这样就会带来一些时序问题,造成数据不一致问题

1、多个进程看到的公共的一份资源叫做临界资源

2、把自己的进程访问临界资源的代码叫做临界区

3、为了更好保护临界区,可以让在任何时刻都只能有一个进程进入临界区——互斥

4、原子性:要么不做,要么做完,没有中间状态


总结

以上就是今天要讲的内容本文仅仅简单介绍了进程间通信的管道和共享内存

原文地址:https://blog.csdn.net/m0_62179366/article/details/128345053

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_28838.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注