进程间通信(管道,共享内存)包含原理剖析

通信的本质

因为进程具有独立性,我们要进行通信的成本一定不低,我们要先让不同的进程看到同一份资源,之后再进行通信。所以,通信的本质是:1.操作系统直接或间接给通信双方的进程提供内存空间2.要通信的进程,必须看到同一份资源!不同的通信类型的本质就是:上面所说的资源是OS中哪一个模块提供的(比如公共资源是文件系统提供的那么就是管道通信)。

匿名管道

工作原理

每个进程都有自己的task_struct结构体,其中又有一个指针,指向自己的文件描述符表,文件描述符表中又保存了所有文件描述符,通过文件描述符就可以对文件进行读写操作。有了这些前置知识,我们直接先上原理图(省去了task_struct)
第一步:在父进程中调用pipe函数,该函数会生成一个管道文件,并开启读端和写端,将读写操作符通过传入的参数进行返回。
第二步:创建子进程,子进程会继承父进程的文件描述符表,同样可以对管道文件有读写操作。
第三步:分别关闭父进程的读端和子进程的写端,使得形成只能父进程往里写,子进程从中读的半双匿名管道

代码实现

#include<iostream>
#include<unistd.h>
#include<cassert>
#include<cstdlib>
#include<cstring>
using namespace std;


int main() {
//第一步,创建管道文件,打开读写端
    int pipefd[2];
    int n = pipe(pipefd);
    assert(n == 0);
//第二步,fork
    int fd = fork();
    assert(fd >= 0);
    if (fd == 0) {
        //子进程进行写入
        int cnt = 0;
        close(pipefd[0]);//关闭读
        char buffer[1024];
        const char* s = "你好我是子进程,我正在写入";
        while (true) {
            snprintf(buffer, sizeof(buffer) - 1, "pit->%d:%s\n", getpid(), s);
            write(pipefd[1], buffer, strlen(buffer));
            cout << "count: " << ++cnt << endl;

            sleep(2);
        }
        exit(0);
    }
    close(pipefd[1]);
    char buffer[1024];

    while (true) {
        ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
        if (s > 0) { //读成功
            cout << "get message: " << buffer << endl;
        }
    }
    return 0;
}

读写规则

当没有数据可读时
        O_NONBLOCK disable(阻塞调用):read调用阻塞,即进程暂停执行,一直等到有数据来为止
        O_NONBLOCK enable(非阻塞调用):read调用返回-1errno值为EAGAIN
当管道满的时候
        O_NONBLOCK disable(阻塞调用):
write调用阻塞,直到有进程读走数据。
        O_NONBLOCK enable(非阻塞调用):write调用返回-1errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭write操作会产生SIGPIPE信号,进而导致write进程退出

管道特性

1.管道的生命进程随进程
2.管道可以用来进行具有血缘关系的进程之间通信,常用于父与子
3.管道是面向字节流的
4.管道是半双工通信(单向通信)
5.互斥与同步机制--对共享资源进行保护的方案

命名管道

工作原理

由于匿名管道的使用有一个局限,只能应用于具有血缘关系的进程间通信。我们想在不相关进程之间交换数据,就可以使用FIFO文件来做这项工作,它经常被称为命名管道。那么命名管道又是如何让不同的进程,看到同一份资源的?答案是让不同的进程打开指定名称(路径+文件名)的同一个文件,这就是命名管道的工作原理

代码实现

命名管道代码思路更加简单,只需让通信的进程通过mkfifo函数创建一个管道文件,后续进行正常的读写就可以了。

//创建fifo文件的代码
bool creatFifo(const string& s) { //s是文件路径+文件名
    umask(0);
    int n = mkfifo(s.c_str(), 0600); //给定文件名、权限
    if (n == 0)return true;
    cout << "errno:" << errno << " string er:" << strerror(errno) << endl;
    return false;
}

void removeFifo(string& s) { //s是文件路径+文件名
    int n = unlink(s.c_str());
    assert(n == 0);
    (void)n;
}

共享内存

工作原理

通过让不同进程,看到同一个内存块的方式就是共享内存。
我们直接上图,步骤先后顺序在图中有标号

认识接口

notice:未提及的参数如想了解可自行从手册查找,在此篇文章中不重要。

#include<sys/types.h>
#include<sys/ipc.h>

ket_t ftok(const char*pathname,int projectid)

函数功能:通信的进程通过相同的位置和进程id即可获取相同的key值
返回值:用于后续创建共享内存
pathname:路径
projectid:进程id

#include<sys/ipc.h>
#include<sys/shm.h>

int shmget(ket_t key,size_t size,int shmflg)

函数功能:申请一块共享内存的空间
返回值:是数组下标,但是跟文件系统是两套,只需知道后续操作用它作标识符即可
key:通过ftok函数获取的key值
size:共享内存的大小
shmflag:位图,如:IPC_CREAT表示不存在则创建,存在则获取;IPC_EXCL无法单独使用,可以IPC_CREAT|IPC_EXCL表示不存在则创建,存在就返回错误,可以保证给用户的一定是一个新的shm

//shmid是shmget的返回值,如果将cmd设置为IPC_RMID表示释放共享内存
int shmctl(int shmid,int cmd,shmid_ds *buf)

//关联操作,返回值为地址,后续直接一端向该地址写,另一端读,即可完成通信
void* shmat(int shmid,const void*shmaddr,int shmflg)

//去关联操作,shmaddr为关联操作获取的地址,该函数失败返回-1
int shmdt(const void*shmaddr)

代码实现

//file name: comm.hpp
#pragma once
#include<iostream>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<cerrno>
#include<cstring>
#include<cstdbool>
#include<unistd.h>

#define PATH_NAME "."
#define PROH_ID 0x66
using namespace std;

//通过ftok函数获取key
int getKey() {
    key_t key = ftok(PATH_NAME, PROH_ID);
    if (key < 0) {
        cerr << errno << ":" << strerror(errno) << endl;
        exit(1);
    }
    return key;
}
//创建共享内存
int creatShm(key_t key) {
    int shmid = shmget(key, 1024, IPC_CREAT | IPC_EXCL | 0600);
    if (shmid < 0) {
        cerr << errno << ":" << strerror(errno) << endl;
        exit(2);
    }
    return shmid;
}

int getShm(key_t key) {
    int shmid = shmget(key, 1024, IPC_CREAT);
    if (shmid < 0) {
        cerr << errno << ":" << strerror(errno) << endl;
        exit(2);
    }
    return shmid;
}
//关联
void* attchShm(int shmid) {
    void* ad = shmat(shmid, nullptr, 0);
    if ((long long)ad < 0) {
        cerr << errno << ":" << strerror(errno) << endl;
        exit(3);
    }

    return ad;
}
//去关联
void detchShm(void* ad) {
    int n = shmdt(ad);
    if (n < 0) {
        cerr << errno << ":" << strerror(errno) << endl;
        exit(4);
    }
}
//删除共享内存
void desShm(int shmid) {
    if (shmctl(shmid, IPC_RMID, nullptr) == -1) {
        cerr << errno << ":" << strerror(errno) << endl;
        exit(5);
    }
}

这个文件主要是对上面接口进行封装,以便服务端和客户端进行使用

#include"comm.hpp"


int main() {
    key_t key = getKey();
    cout << "获取key成功" << endl;

    int shmid = creatShm(key);
    cout << "创建共享内存成功" << endl;

    void* ad = attchShm(shmid);
    cout << "进程和共享内存挂接成功" << endl;

///发消息
    char s[1024] = "我是client我在发送消息";
    int cnt = 1;
    while (true) {

        sprintf(s, "%s:pid[%d]cnt[%d]\n", "我是client我在发送消息", getpid(),
                cnt++);
        memcpy(ad, s, sizeof(s));
        sleep(2);
    }

    detchShm(ad);
    cout << "去关联成功" << endl;


    return 0;
}

该文件为客户端的代码,就是按照共享内存的原理,通过事先规定好的路径和projectid获取到key值、通过key值创建共享内存、关联、向共享内存中发送数据、去关联、删除共享内存

#include"comm.hpp"
int main() {
    key_t key = getKey();
    cout << "获取key成功" << endl;

    int shmid = getShm(key);
    cout << "创建共享内存成功" << endl;

    void* ad = attchShm(shmid);
    cout << "进程和共享内存挂接成功" << endl;

///收消息
    while (true) {
        printf("%s", ad);
        sleep(2);
    }

    detchShm(ad);
    cout << "去关联成功" << endl;

    desShm(shmid);
    cout << "删除共享内存成功" << endl;

    return 0;
}

这个文件即为服务器的代码,通过与客户端相同的路径和projectid获取到key值、通过key值创建共享内存、关联、在共享内存中读取数据、去关联、删除共享内存
注意:运行代码时,应先运行客户端创建好共享内存,再运行服务器。

其他进程间通信方法

除了上述两种方法之外,进程间通信的方法还有,信号量、信号、socket用于不同主机间进程通信等。其中信号量在生产者消费者模型精讲-CSDN博客中有讲解和使用,另外的的方法由于篇幅问题,先不进行讲解。如有需要可以评论区留言。

  • 13
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值