进程间通信

🌟🌟hello,各位读者大大们你们好呀🌟🌟
🚀🚀系列专栏:【Linux的学习】
📝📝本篇内容:进程间通信的目的;匿名管道;命名管道;共享内存
⬆⬆⬆⬆上一篇:C++的多态
💖💖作者简介:轩情吖,请多多指教(> •̀֊•́ ) ̖́-

1.进程间通信的目的

①数据传输:一个进程需要将它的数据发送给另一个进程
②资源共享:多个进程之间共享同样的资源
③通知事件:一个进程需要向另一个或一组进程发送消息,通知它发生了某种事情
④进程控制:有些进程希望完全控制另一个进程的执行(如debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

2.管道

①进程是具有独立性的,这无疑增加了通信成本
②要让两个不同的进程进行通信,前提条件是先让两个进程看到同一份“资源”(OS直接或间接提供)
③任何进程通信手段
a.让不同的进程看到同一份资源
b.让一方写入,一方读取,完成通信过程,至于通信目的和后续工作,要结合具体场景
接下来用一幅图来展示一下管道(匿名管道)是如何工作的
在这里插入图片描述
创建子进程的时候,fork子进程,只会复制进程相关的数据结构对象,不会复制父进程曾经打开的文件对象
这就是为什么fork之后,父子进程都printf,cout,都会向同一个显示器终端打印数据的原因

3.创建匿名管道

函数原型:int pipe([int fd[2]]);
功能:创建一个无名管道
参数:fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端
返回值:成功返回0,失败返回-1

4.编写代码

#include <stdio.h>
#include <unistd.h>
#include <iostream>
#include <cerrno>
#include <cassert>
#include <string.h>
using namespace std;
// 父进程写入,子进程读取
int main()
{
    // 1.创建匿名管道
    int fd[2] = {0};
    int ret = pipe(fd);
    if (ret < 0)
    {
        cout << errno << ":" << strerror(errno) << endl;
    }
    // 2.创建子进程
    int n = fork();
    assert(n>=0);
    if (n == 0)
    {
        // 子进程
        // 3.关闭不必要的fd
        close(fd[1]);
        // 4.开始通信
        while (1)
        {
            char buff[128];
            int n=read(fd[0], buff, 128);
            if(n<0)
            {
        cout << errno << ":" << strerror(errno) << endl;
            }
            if(n==0)
            {
                cout<<"I have already read the end of file"<<endl;
            }
            buff[n]='\0';
            cout<<"child:";
            cout<<buff<<endl;
            sleep(1);
        }

        close(fd[0]);
        exit(0);
    }
    // 父进程
    // 3.关闭不必要的fd
    close(fd[0]);
    // 4.开始通信
    int message=0;
    while (1)
    {
        char buff[128];
        cout<<"parent:I am the parent process,I am preparing for write to the child"<<endl;
        snprintf(buff,128,"The parent process  writes message to my me:%d\n", message++);
        
        int n = write(fd[1], buff, strlen(buff));
        if (n < 0)
        {
            cout << errno << ":" << strerror(errno) << endl;
        }
        sleep(1);
    }

    close(fd[1]);
    return 0;
}

在这里插入图片描述

特点:
①单向通信(管道是半双工的一种特殊情况)
②管道的本质是文件,因为fd的生命周期随进程,管道的生命周期是随进程的
③管道通信,通常用来进行具有“血缘”关系的进程,进行进程间通信。常用于父子通信——pipe打开管道,并不清楚管道名字,因此称为匿名管道
④在管道通信中,写入的次数和读取的次数,不是严格匹配的——字节流
在这里插入图片描述

在这里插入图片描述
⑤具有一定的协同能力,让read和write能够按照一定的步骤进行通信——自带同步机制
四种场景:
a.如果我们的read读取完毕了所有的管道数据,如果对方不发,我就只能等待
在这里插入图片描述

对代码稍作修改
在这里插入图片描述
b.如果我们的write端将管道写满了,我们不能继续写
在这里插入图片描述

在这里插入图片描述
③如果我关闭了写端,读取完毕管道数据,再读,就会read返回0,表明读到了文件结尾

#include <stdio.h>
#include <unistd.h>
#include <iostream>
#include <cerrno>
#include <cassert>
#include <string.h>
using namespace std;
// 父进程写入,子进程读取
int main()
{
    // 1.创建匿名管道
    int fd[2] = {0};
    int ret = pipe(fd);
    if (ret < 0)
    {
        cout << errno << ":" << strerror(errno) << endl;
    }
    // 2.创建子进程
    int n = fork();
    assert(n >= 0);
    if (n == 0)
    {
        // 子进程
        // 3.关闭不必要的fd
        close(fd[1]);
        // 4.开始通信
        while (1)
        {
            char buff[128];
            // cout<<"child:我在等待父进程发送的消息"<<endl;
            int n = read(fd[0], buff, 127);
            cout << "child:我等到了父进程发送的消息" << endl;
            if (n < 0)
            {
                cout << errno << ":" << strerror(errno) << endl;
                break;
            }
            if (n == 0)
            {
                cout << "I have already read the end of file" << endl;
                break;
            }
            buff[n] = '\0';
            cout << "child:";
            cout << buff << endl;
            sleep(1);
        }

        close(fd[0]);
        exit(0);
    }
    // 父进程
    // 3.关闭不必要的fd
    close(fd[0]);
    // 4.开始通信
    int message = 0;
    int count = 5;
    while (count--)
    {
        char buff[128];
        cout << "parent:I am the parent process,I am preparing for write to the child" << endl;
        snprintf(buff, 128, "The parent process  writes message to my me:%d\n", message++);

        int n = write(fd[1], buff, strlen(buff));
        if (n < 0)
        {
            cout << errno << ":" << strerror(errno) << endl;
            break;
        }
        // sleep(5);
    }

    close(fd[1]);
    return 0;
}

在这里插入图片描述

④写端一直写,读端关闭,则没有意义,OS不会维护无意义,低效率或浪费资源的事情。OS会杀掉一直在写入的进程!OS会通过信号来终止进程13)SIGPIPE

5.命名管道

如果我们想在不相关的进程之间交换数据,可以使用FIFO文件
命令:mkfifo filename
函数:int mkfifo(const char*filename,mode_t mode);
在这里插入图片描述

//header.h
#include <iostream>
#include <stdio.h>
#include <cassert>
#include <cerrno>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <cstdlib>
#include "fcntl.h"
using namespace std;
#define filename "fifo"
#define  MAX 128
__mode_t mode=0666;
//server.cpp
#include "header.h"
int main()
{
    // 1.创建命名管道
    umask(0);
    int ret = mkfifo(filename, mode);
    if (ret < 0)
    {
        cout << errno << ":" << strerror(errno) << endl;
        exit(1);
    }
    // 2.打开命名管道文件
    int fd = open(filename, O_RDONLY);
    assert(ret != -1);
    // 3.准备通信
    while (1)
    {
        cout << "server:I have already prepared for receiving the client message" << endl;
        char buf[MAX] = {0};
        int n = read(fd, buf, sizeof(buf) - 1);
        if (n < 0)
        {
            cout << errno << ":" << strerror(errno) << endl;
            break;
        }
        else if (n == 0)
        {
            cout << "读到文件尾" << endl;
            break;
        }
        else
        {
            buf[n]='\0';
            cout<<"server:I have received the  client message"<<endl;
            cout<<buf<<endl;
        }
        sleep(1);
    }
    unlink(filename);//删除命令管道

    return 0;
}
//client.cpp
#include "header.h"
int main()
{
    // 1.打开命名管道
    int fd = open(filename, O_WRONLY | O_TRUNC);
    cout<<"client:I have already opened the namefile"<<endl;
    assert(fd != -1);
    // 2.开始通信
    int count = 0;
    while (1)
    {
        char buf[MAX] = {0};
        sprintf(buf, "the client send message is %d", count++);
        cout<<"client:I have already prepared for sending message to the server"<<endl; 
        int ret = write(fd, buf, strlen(buf));
        if (ret <= 0)
        {
            cout << errno << ":" << strerror(errno) << endl;
            break;
        }
        sleep(1);
    }

    return 0;
}

在这里插入图片描述
在这里插入图片描述
和匿名管道的特性是一样的

6.命名管道的打开规则

如果当前打开操作是为读而打开FIFO时:
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时:
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
在这里插入图片描述
可以看到我们的读端fifoserver程序开始运行时并没有反应,直到client也开始运行,这代表着读端阻塞到client为写而打开fifo
命名管道总结:使两个毫不相关的进程看到同一个文件靠的是文件的唯一性,路径,让不同的进程通过文件路径+文件名看到同一个文件,并打开,就是看到了同一个资源

7.system V共享内存

在这里插入图片描述
共享内存就是在内存中找一块空间开辟出来,然后将共享内存映射到共享它的进程地址空间
在任意时刻,可能有多个共享内存被用来通信,因此系统为了管理共享内存,构建对应的描述共享内存的结构体对象
共享内存=共享内存的内核数据结构+真正开辟的内存空间
在这里插入图片描述

首先我们要使用ftok函数创建key值来区分不同的共享内存,当进程A创建共享内存后,进程B通过对应的key值来找到对应的共享内存

8.函数(共享内存)

1.ftok

在这里插入图片描述
参数:
pathname:路径字符串
proj_id:项目id
返回值为一个共享内存段的名字(后面使用),它是通过上面的路径和项目id进行一个算法,来计算出的一个值(整数)

2.shmget

在这里插入图片描述
功能:用来创建共享内存
参数:key:这个共享内存的名字
size:共享内存的大小
shmflg:权限标志
常用的权限标志有:IPC_CREAT和IPC_EXCL
单独使用IPC_CREAT:创建一个共享内存,如果共享内存不存在,就创建,如果已经存在,获取已经存在的共享内存并返回
IPC_EXCL不能单独使用,一般都要配合IPC_CREAT
IPC_CREAT|IPC_EXCL:创建一个共享内存,如果共享内存不存在,就创建之,如果已经存在,则立马出错返回——如果创建成功,对应的shm一定是最新的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

3.shmct

在这里插入图片描述

功能:控制共享内存
参数:shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作,如下(常用)
IPC_STAT:有权限的情况下,可以使buf中存在共享内存相关属性并查看
IPC_RMID:删除共享内存
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构

4.shmat

在这里插入图片描述
功能:将共享内存段连接到进程地址空间
参数:shmid:共享内存标识
shmaddr:指定链接的地址
shmflg:通常直接用0,既可以读也可以写

5.shmdt

在这里插入图片描述
功能:将共享内存段与当前进程脱离
参数:shmaddr:又shamt返回的指针
返回值:成功0,失败-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段

9.代码演示

//server.cpp
#include "header.hpp"
int main()
{
    // 1.获取key值
    key_t key = Getkey();
    // 2.创建共享内存
    int shmid = Createshm(key);
    // 3.和共享内存关联
    char *start = Attachshm(shmid);
    // 4.通信

    int count = 30;
    while (count--)
    {
        cout <<"client->server:" <<start << endl;
        sleep(1);
    }
    // 5.脱离关联
    Awayshm(start);
    // 6.删除共享内存
    Deleteshm(shmid);
    return 0;
}
//client.cpp
#include "header.hpp"
int main()
{
//1.获取key值
key_t key=Getkey();
//2.获取共享内存
int shmid=Getshm(key);
//3.和共享内存关联
char* start=Attachshm(shmid);
//4.通信
    char c = 'A';
    while (c <= 'Z')
    {
        start[c - 'A'] = c;
        c++;
        start[c - 'A'] = '\0';
        sleep(1);
    }


//5.脱离关联
Awayshm(start);
    return 0;
}
//header.hpp
#include <stdio.h>
#include <cerrno>
#include <string.h>
#include <iostream>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>
using namespace std;
#define pathname "."
#define project_id 1
#define SIZE 4096

// 获取key值
key_t Getkey()
{
    return ftok(pathname, 1);
}

// 创建共享内存
int Createshm(key_t k)
{
    umask(0);
    int n = shmget(k, SIZE, IPC_CREAT | IPC_EXCL|0666);
    if (n < 0)
    {
        cout << errno << ":" << strerror(errno) << endl;
        exit(-1);
    }
    return n;
}

// 获取共享内存
int Getshm(key_t k)
{
    int n = shmget(k, SIZE, IPC_CREAT);
    if (n < 0)
    {
        cout << errno << ":" << strerror(errno) << endl;
        exit(-1);
    }
    return n;
}

//删除共享内存
void Deleteshm(int shmid)
{
shmctl(shmid,IPC_RMID,nullptr);
}


//连接共享内存
char* Attachshm(int shmid)
{
return (char*)shmat(shmid,nullptr,0);
}


//脱离共享内存
void Awayshm(char * addr)
{
shmdt(addr);
}

在这里插入图片描述

在这里插入图片描述

10.指令

①ipcs
在这里插入图片描述
查看System V标准通信信息
②ipcs -m

查看共享内存信息
③ipcrm -m shmid
删除特定共享内存

11.补充细节(共享内存)

在这里插入图片描述
perms指的是权限
nattch是连接进程的数量
①共享内存的大小是以PAGE(4KB)为单位
②一旦共享内存映射到进程的地址空间,该共享内存就直接被所对应的进程直接看到了
③因为共享内存的这种特性,可以让进程通信的时候,减少拷贝次数,所以共享内存是所有进程间通信中速度最快的
④共享内存没有任何保护机制(同步互斥)->由于管道通过系统接口通信,共享内存直接通信
⑤同步互斥:
我们把大家都能看到的资源:公共资源
a.互斥:任何一个时刻,都只允许一个执行流在进行共享资源的访问-加锁
b.临界资源:我们把任意时刻,都只允许一个执行流在进行访问的资源叫做临界资源
c.临界区:临界资源是要通过代码访问的,凡是访问临界资源的代码,叫做临界区
d.原子性:要么不做,要么做完,只有两种确定状态的属性
⑥信号量/信号灯
本质就是一个计数器。描述资源数量的计数器
任何一个执行流想要访问临界资源中的一个子资源时,不能直接访问
a.需要申请信号量才可以,先申请信号量资源,相等于(count–),只要我申请信号量成功,我就一定能在未来拿到一个子资源(p操作)
b.进入自己的临界区,访问对应的临界资源
c.释放信号量资源,相等于(count++),只要将计数器增加,就表示将我们对应的资源进行了归还(v操作)
进程通过执行代码来申请信号量,所有的进程都得先看到信号量,因为我们想让不同的进程看到同一个信号量,所以信号量也被归为了进程间通信
如果计数器为1时,称为二元信号量,有互斥功能
相等于信号量也是共享资源,因此要必须保证自己的操作++,–是原子性的
⑦理解IPC
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
以上是system V中的三种通信方式,我们只讲了共享内存,其他两种暂时了解即可,但是重点在于看他们的struct xxx_ds中的第一个成员,struct ipc_perm shm_perm,三个通信方式都是一样的,其实在内核中,所有的ipc资源统一以数组方式进行管理
在这里插入图片描述
可以看成类似于C++的多态

🌸🌸进程间通信的知识大概就讲到这里啦,博主后续会继续更新更多Linux的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

轩情吖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值