Linux——进程间通信

目录

一进程间通信的介绍

1进程为什么要通信

2进程如何通信

3进程的常见方式

二进程间通信的方式

1匿名管道

1.1原理

1.2实现匿名管道

完整代码

2管道的4种情况和5种特征

3进程池

完整代码

4命名管道

4.1原理

4.2实现命名管道

5共享内存

5.1原理

5.2代码实现

5.3优化

6.消息队列(了解)

6.1原理

6.2接口

​编辑7信号量

7.1五大概念渗透

7.2信号量理解

7.3信号量操作

三OS管理进程间通信


一进程间通信的介绍

1进程为什么要通信

不同进程之间需要某种协同,所以如何协同的前提条件:通信------通信的数据是有类别的:通知就绪的:单纯的要给我传递数据的;控制相关信息的数据的......

而事实上,进程之间具有独立性(进程=内核数据结构+代码和数据)通信的选择??

2进程如何通信

a.可以直接让进程之间进行进行通信,但成本可能会高一点

b.通信的前提:让不同的进程,看到同一份**(OS)资源(同一块内存)**

而OS要这样做:

1.一定是某个进程需要进行通信的需求,让OS创建一份资源;

2.OS要如何知道要进行通信了------用户知道:所以OS要提供系统调用来满足用户;

OS创建资源的不同,意味着有很多的系统调用,也就是说:进程间通信有不同的种类!!

3进程的常见方式

管道 匿名管道pipe 命名管道
System V IPC System V 消息队列 System V 共享内存 System V 信号量
POSIX IPC 消息队列 共享内存 信号量
互斥量 条件变量 读写锁

二进程间通信的方式

1匿名管道

1.1原理

理解OS内部进程与文件之间的关系:

进程对文件进行操作:

进程要在files结构体 在2号下标之后(0,1,2默认是打开的)添加指向对文件进行read或者write的方法的指针,指向对应的file结构体去执行方法:不同方法的file结构体只是里面的对文件的操作(如read或者write)不同,其他的如文件内容与属性,缓冲区...这些资源是每个file结构体所共享的,不用在单独去再创建一份出来;

我们要实现父进程read,子进程write:父进程创建出子进程,把父进程的task_struct和files结构体里面的内容拷贝一份给子进程,但关于file结构体就不用再拷贝了??

前面我们不是说进程之间具有独立性吗?

因为files结构体所指向的struct file以及后面的文件的属性,内容,这些都是文件系统相关的,关我什么事!

把父进程的files里的4号下标给关了,子进程的files里的3号下标给关了:一个只读,一个只写,就完成了父进程与子进程的单向通信!

正因为这种通信实现简单,才有了管道这一概念的出现!

a.为什么父子进程会向同一个显示器终端打印数据

1.父进程会默认打开三个流:stdin,stdout,stderr;在创建子进程后,子进程通过继承也打开;

2.显示器也是文件,父子进程在往显示器文件里写数据,数据就会在同一个缓冲区里,通过刷新显示到显示器上

b.子进程主动ciose(0,1,2),会影响父进程继续使用显示器文件吗?

在struct file中会包含着引用计数变量:子进程关闭只是在此基础上进行--;只有减到0时file才会被释放:所以子进程的close不影响父进程的使用

c.既然后面父进程要关闭files中4号下标(关闭文件的写端),先关闭好不好?

如果这样做的话:子进程继承下来的files里的4号也是关闭的,但我们是想让它来写操作!!

1.2实现匿名管道

先来了解创建匿名管道的系统调用:

1.父进程创建管道:

cpp 复制代码
int main()
{
    // 1. 创建管道
    int pipefd[2];
    int n = pipe(pipefd); // 输出型参数,rfd, wfd
    //n不为0创建失败
    if (n != 0)
    {
        std::cerr << "errno: " << errno << ": "
                  << "errstring : " << strerror(errno) << std::endl;
        return 1;
    }
    //...
}

2.父进程fork出子进程

cpp 复制代码
    // pipefd[0]->0->r(嘴巴 - 读)  pipefd[1]->1->w(笔->写)
    //2.创建子进程
    pid_t id = fork();
    if (id == 0)
    {
        std::cout << "子进程关闭不需要的fd了, 准备发消息了" << std::endl;
        sleep(1);
        // 子进程 --- write

        //关闭不需要的fd
        close(pipefd[0]);
        SubProcessWrite(pipefd[1]);//实现子进程写如管道的函数
        close(pipefd[1]);//写完进行关闭
        exit(0);
    }

3.父进程关闭fd[pipe[0]],子进程关闭fd[pipe[1]]:

cpp 复制代码
 std::cout << "父进程关闭不需要的fd了, 准备收消息了" << std::endl;
    sleep(1);
    // 父进程 --- read
    // 3. 关闭不需要的fd
    close(pipefd[1]);
    FatherProcessRead(pipefd[0]);//从管道读的函数实现
    std::cout << "5s, father close rfd" << std::endl;
    sleep(5);
    close(pipefd[0]);
    //等待子进程退出
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if (rid > 0)
    {
        std::cout << "wait child process done, exit sig: " << (status & 0x7f) << std::endl;
        std::cout << "wait child process done, exit code(ign): " << ((status >> 8) & 0xFF) << std::endl;
    }

既然父进程在后面要关闭pipe[0]:在最开始的时候为什么不先把它关掉呢?

为了让子进程继承~

可以不关吗? -> 可以:但建议关,防止误写~

完整代码

cpp 复制代码
#include <iostream>
#include <string>
#include <cerrno>  // errno.h
#include <cstring> // string.h
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

const int size = 1024;

std::string getOtherMessage()
{
    static int cnt = 0;
    std::string messageid = std::to_string(cnt); // stoi -> string -> int
    cnt++;
    pid_t self_id = getpid();
    std::string stringpid = std::to_string(self_id);

    std::string message = "messageid: ";
    message += messageid;
    message += " my pid is : ";
    message += stringpid;

    return message;
}

// 子进程进行写入
void SubProcessWrite(int wfd)
{
    int pipesize = 0;
    std::string message = "father, I am your son prcess!";
    int n = 10;
    while (n--)
    {
        sleep(1);
        std::cerr << "+++++++++++++++++++++++++++++++++" << std::endl;
        std::string info = message + getOtherMessage(); // 子进程发给父进程的消息
        write(wfd, info.c_str(), info.size());          // 不需要写入\0
    }

    std::cout << "child quit ..." << std::endl;
}

// 父进程进行读取
void FatherProcessRead(int rfd)
{
    char inbuffer[size]; //变长数组->c99标准
    while (true)
    {
        sleep(1);
        // std::cout << "-------------------------------------------" << std::endl;
        ssize_t n = read(rfd, inbuffer, sizeof(inbuffer) - 1);//保留一个位置填'\0'
        if (n > 0)
        {
            inbuffer[n] = 0; // == '\0'
            std::cout << inbuffer << std::endl;
        }
        else if (n == 0)
        {
            // 如果read的返回值是0,表示写端直接关闭了,我们读到了文件的结尾
            std::cout << "client quit, father get return val: " << n << " father quit too!" << std::endl;
            break;
        }
        else if (n < 0)
        {
            std::cerr << "read error" << std::endl;
            break;
        }
    }
}

int main()
{
    // 1. 创建管道
    int pipefd[2];
    int n = pipe(pipefd); // 输出型参数,rfd, wfd
    if (n != 0)
    {
        std::cerr << "errno: " << errno << ": "
                  << "errstring : " << strerror(errno) << std::endl;
        return 1;
    }
    // pipefd[0]->0->r(嘴巴 - 读)  pipefd[1]->1->w(笔->写)

    // 2. 创建子进程
    pid_t id = fork();
    if (id == 0)
    {
        std::cout << "子进程关闭不需要的fd了, 准备发消息了" << std::endl;
        sleep(1);
        // 子进程 --- write
        // 3. 关闭不需要的fd
        close(pipefd[0]);

        // if(fork() > 0) exit(0);

        SubProcessWrite(pipefd[1]);
        close(pipefd[1]);
        exit(0);
    }

    std::cout << "父进程关闭不需要的fd了, 准备收消息了" << std::endl;
    sleep(1);
    // 父进程 --- read
    // 3. 关闭不需要的fd
    close(pipefd[1]);
    FatherProcessRead(pipefd[0]);
    std::cout << "5s, father close rfd" << std::endl;
    sleep(5);
    close(pipefd[0]);
    //等待子进程退出
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if (rid > 0)
    {
        std::cout << "wait child process done, exit sig: " << (status & 0x7f) << std::endl;
        std::cout << "wait child process done, exit code(ign): " << ((status >> 8) & 0xFF) << std::endl;
    }
    return 0;
}

以上是用一个管道实现简单的单向通信,如果要实现双向通信可以选择用两个管道来实现

2管道的4种情况和5种特征

特征:a.匿名管道:只用来进行有血缘关系的进程之间的通信,如:父子进程

1.让父进程一直读,子进程写完后休眠5s:

cpp 复制代码
// 子进程进行写入
void SubProcessWrite(int wfd)
{
    std::string message = "father, I am your son prcess!";
    while (true)
    {
        std::cerr << "+++++++++++++++++++++++++++++++++" << std::endl;
        std::string info = message + getOtherMessage(); 
        write(wfd, info.c_str(), info.size());          

        sleep(5);//休眠5s
    }
}

// 父进程进行读取
void FatherProcessRead(int rfd)
{
    char inbuffer[size]; 
    while (true)
    {
        //sleep(1);//永远在读
        ssize_t n = read(rfd, inbuffer, sizeof(inbuffer) - 1); 
        if (n > 0)
        {
            inbuffer[n] = 0; // == '\0'
            std::cout << inbuffer << std::endl;
        }
        else if (n == 0)
        {
            std::cout << "client quit, father get return val: " << n << " father quit too!" << std::endl;
            break;
        }
        else if (n < 0)
        {
            std::cerr << "read error" << std::endl;
            break;
        }
    }
}

刚开始:父进程读到子进程的第一条消息: (之后父进程阻塞)

过了5s之后才读到第二条消息:(继续阻塞...)

进程之间进行通信,可能会让不同进程(并发)进行访问的:带来数据不一致问题(子进程本来要写入整体的"abc1234":写到abc的时候就被父进程读走了)

但在上面我们所演示的现象是:子进程写一条,父进程读一条;

因为管道本质上也是文件,子进程休眠的过程,父进程在阻塞着,等待子进程的写入

我们可以得出:

b.管道内部:自带进程之间同步的机制(同步:多执行流执行代码时,有明显的顺序性)

也看到了第一种情况:

1.如果管道内部为空&&读的fd没有关闭:读取条件不具备读进程进行阻塞
2.父进程不读了,子进程不断进行写入:

cpp 复制代码
// 子进程进行写入
void SubProcessWrite(int wfd)
{
    int pipesize = 0;
    std::string message = "father, I am your son prcess!";
    while (true)
    {
        char ch='A';
        write(wfd,&ch,1);
        pipesize++;
        std::cout<<"pipesize:"<<pipesize<<std::endl;
    }
}

// 父进程进行读取
void FatherProcessRead(int rfd)
{
    char inbuffer[size]; 
    while (true)
    {
        sleep(500);//不读了
        ssize_t n = read(rfd, inbuffer, sizeof(inbuffer) - 1); 
        if (n > 0)
        {
            inbuffer[n] = 0; // == '\0'
            std::cout << inbuffer << std::endl;
        }
        else if (n == 0)
        {
            std::cout << "client quit, father get return val: " << n << " father quit too!" << std::endl;
            break;
        }
        else if (n < 0)
        {
            std::cerr << "read error" << std::endl;
            break;
        }
    }
}

结果:子进程写到65535(=65KB)时,子进程阻塞:管道被写满了

2.管道被写满&&读的fd不去读且不关闭,管道被写满,写进程会被阻塞
3. 父进程一直读,子进程写一条信息后就退出了:

cpp 复制代码
// 子进程进行写入
void SubProcessWrite(int wfd)
{
    int pipesize = 0;
    std::string message = "father, I am your son prcess!";
    while (true)
    {
        char ch='A';
        write(wfd,&ch,1);
        break; // 关闭写端
    }
    std::cout<<"child quit..."<<std::endl;
}

// 父进程进行读取
void FatherProcessRead(int rfd)
{
    char inbuffer[size];
    while (true)
    {
        // 一直读
        ssize_t n = read(rfd, inbuffer, sizeof(inbuffer) - 1);
        if (n > 0)
        {
            inbuffer[n] = 0; // == '\0'
            std::cout << inbuffer << std::endl;
        }
        std::cout << "get return val:" << n << std::endl;
    }
}

结果:父进程会死循环地进行读取

3管道一直在读&&写端关闭了wfd,读端会读到返回值0,表示读到了文件结尾
4.父进程读端关闭,子进程一直写:

cpp 复制代码
// 子进程进行写入
void SubProcessWrite(int wfd)
{
    int pipesize = 0;
    std::string message = "father, I am your son prcess!";

    // 一直写
    while (true)
    {
        sleep(1);
        std::string info = message + getOtherMessage(); // 子进程发给父进程的消息
        write(wfd, info.c_str(), info.size());          // 不需要写入\0
        sleep(1);
    }

    std::cout << "child quit..." << std::endl;
}

// 父进程进行读取
void FatherProcessRead(int rfd)
{
    char inbuffer[size];
    while (true)
    {
        ssize_t n = read(rfd, inbuffer, sizeof(inbuffer) - 1);
        if (n > 0)
        {
            inbuffer[n] = 0; // == '\0'
            std::cout << "father read:" << inbuffer << std::endl;
        }
        else if (n == 0)
        {
            std::cout << "client quit, father get return val: " << n << " father quit too!" << std::endl;
            break;
        }
        else if (n < 0)
        {
            std::cerr << "read error" << std::endl;
            break;
        }

        sleep(1);
        break; // 读端关闭
    }
}

我们在实现的时候让子进程进行写入,目的就在这:便于观察现象

当父进程关闭读端时,OS认为这个管道是broken pipe(没有了读端,往管道里写入就是浪费时间),对写的进程发送SIGPIPE(13)信号来终止掉进程:所以子进程的exit sig为13
4.rfd直接关闭,写端一直写入,写端进程会被操作系统使用13号信号杀掉,相当于进程异常
进程退出后,管道也就被OS释放了:

c管道(文件)的生命周期是随进程的

d管道在通信时,是面向字节流的(读取与写入不是一一匹配的)

e管道的通信方式,是一种特殊的半双工模式(双方聊天你一句我一句的交谈)

在管道里:只要每次写入的信息不超过4096个字节,写进去的数据就是安全的(原子的)

管道在指令中的符号:| 代表的就是匿名管道:同时连接(启动)多个进程;

他们是:兄弟关系,对应的父进程是:~bash

3进程池

主逻辑:实现父进程给子进程派发任务,让子进程去执行相应的任务即可
1.创建一个Channel对象

cpp 复制代码
class Channel
{
public:
    Channel(const int wfd, const string &ChannelName, const pid_t ChannelId)
        : _wfd(wfd), _ChannelName(ChannelName), _ChannelId(ChannelId)
    {}

    ~Channel()
    {
    }
    //关闭读端
    void CloseWfd()
    {
        close(_wfd);
    }
    //等待子进程
    void WaitChild()
    {
        pid_t id=waitpid(_ChannelId,nullptr,0);
        if(id>0)
        {
            cout<<"wait seccess"<<endl;
        }
    }
    //获取private变量
    int Getwfd() {return _wfd;}
    string GetChannelName() {return _ChannelName;}
    pid_t GetChannelId() {return _ChannelId;}

private:
    int _wfd;
    string _ChannelName;
    //管道对应子进程的Id
    pid_t _ChannelId;
};

2.任务表的初始化,挑选任务(父进程)与执行任务(子进程):

cpp 复制代码
#define TaskNum 3
typedef void(*task_t)();

task_t Task[TaskNum];



void Print()
{
    cout<<"I an a print task"<<endl;
}
void DownLoad()
{
    cout<<"I an a download task"<<endl;
}

void Fllush()
{
    cout<<"I an a Fllush task"<<endl;
}

void InitTask()
{
    srand(time(nullptr));
    Task[0]=Print;
    Task[1]=DownLoad;
    Task[2]=Fllush;
}
//父进程随机选择任务
int SlectTask()
{
    int tasknum=rand()%TaskNum;
    return tasknum;
}
//执行任务
void ExcuteTask(int Num)
{
    if(Num<0||Num>TaskNum-1)
    {
        cout<<"excute task fail"<<endl;
        return;
    }
    else
    {
        Task[Num]();
    }
}

3创建信道(管道)与子进程

cpp 复制代码
//读到父子进程发来的任务并执行
void work()
{
    while(true)
    {
        int Read_Task=0;
        ssize_t n=read(0,&Read_Task,sizeof(Read_Task));
        if(n==sizeof(int))
        {
            cout<<"child will excute task"<<endl;
            ExcuteTask(Read_Task);
            cout<<"-----------------------"<<endl;
        }
        else if(n==0)
        {
            cout<<"child quie"<<endl;
            break;
        }
    }   
}

void CreateChannelAndSub(int num, vector<Channel> * channels, task_t task)//work的重命名
{
    for(int i=0;i<num;i++)
    {
        int pipefd[2]={0};
        int n=pipe(pipefd);
        if(n<0) exit(-1);
        pid_t id=fork();
        if(id==0)
        {
            //child->读
            close(pipefd[1]);
            //关闭不同管道的读端(后面创建的子进程会有不同管道的读端要进行循环关闭)
            if(!channels->empty())
            {
                for(int i=0;i<(*channels).size();i++)
                {
                    (*channels)[i].CloseWfd();
                }
            }

            //回调函数
            //将读端重写到标准输入端,task()不用传pipefd[0]啦
            dup2(pipefd[0],0);

            task();

            close(pipefd[0]);
            exit(-1);
        }
        //father->写与初始化
        string channelsName="channel"+to_string(i);
        //关闭写端
        close(pipefd[0]);
        channels->push_back(Channel(pipefd[1],channelsName,id));
    }
}

4父进程发送任务(往管道里写)

cpp 复制代码
int SlectPipe(int channel_size)
{
    //分配管道是依次的,保证每个子进程都有任务执行
    static int n=0;
    int channel=n;
    n++;
    n%=channel_size;
    return channel;
}

void SandTask(int Task_Num,int Pipe_Num,vector<Channel>& Channels)
{
    //写任务码
    write(Channels[Pipe_Num].Getwfd(),&Task_Num,sizeof(Task_Num));
}

void ctrlProcessOnce(vector<Channel>& channels)
{
    sleep(1);
    //选任务
    int Task_Index=SlectTask();
    //选管道
    int Pipe_Index=SlectPipe(channels.size());
    //发送任务
    SandTask(Task_Index,Pipe_Index,channels);
  
}

void ctrlProcess(vector<Channel>&channels, int time=-1)
{
    if(time==-1)
    {
        while(true)
        {
            ctrlProcessOnce(channels);
        }
    }
    else
    {
        while(time--)
        {
            ctrlProcessOnce(channels);
        }
    }
}

5回收管道与子进程

cpp 复制代码
void CleanUpChannel(vector<Channel>& channels)
{
   for(auto& Channel:channels)
   {
    Channel.CloseWfd();
    Channel.WaitChild();
   }
}

完整代码

cpp 复制代码
//TestPipePool.cc
#include <iostream>
#include<string>
#include<vector>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>

using namespace std;
#include"Task.hpp"

class Channel
{
public:
    Channel(const int wfd, const string &ChannelName, const pid_t ChannelId)
        : _wfd(wfd), _ChannelName(ChannelName), _ChannelId(ChannelId)
    {}

    ~Channel()
    {
    }

    void CloseWfd()
    {
        close(_wfd);
    }

    void WaitChild()
    {
        pid_t id=waitpid(_ChannelId,nullptr,0);
        if(id>0)
        {
            cout<<"wait seccess"<<endl;
        }
    }

    int Getwfd() {return _wfd;}
    string GetChannelName() {return _ChannelName;}
    pid_t GetChannelId() {return _ChannelId;}

private:
    int _wfd;
    string _ChannelName;
    pid_t _ChannelId;
};





void CreateChannelAndSub(int num, vector<Channel> * channels, task_t task)
{
    for(int i=0;i<num;i++)
    {
        int pipefd[2]={0};
        int n=pipe(pipefd);
        if(n<0) exit(-1);
        pid_t id=fork();
        if(id==0)
        {
            //child->读
            close(pipefd[1]);
            //关闭继承的读端
            if(!channels->empty())
            {
                for(int i=0;i<(*channels).size();i++)
                {
                    (*channels)[i].CloseWfd();
                }
            }

            //回调函数
            //将读端重写到标准输入端就不用传值调用task()了
            dup2(pipefd[0],0);

            task();
            close(pipefd[0]);
            exit(-1);
        }
        //father
        string channelsName="channel"+to_string(i);
        close(pipefd[0]);
        channels->push_back(Channel(pipefd[1],channelsName,id));
    }
}

int SlectPipe(int channel_size)
{
    static int n=0;
    int channel=n;
    n++;
    n%=channel_size;
    return channel;
}

void SandTask(int Task_Num,int Pipe_Num,vector<Channel>& Channels)
{
    //写任务码
    write(Channels[Pipe_Num].Getwfd(),&Task_Num,sizeof(Task_Num));
}

void ctrlProcessOnce(vector<Channel>& channels)
{
    sleep(1);
    //选任务
    int Task_Index=SlectTask();
    //选管道
    int Pipe_Index=SlectPipe(channels.size());
    //发送任务
    SandTask(Task_Index,Pipe_Index,channels);
  
}

void ctrlProcess(vector<Channel>&channels, int time=-1)
{
    if(time==-1)
    {
        while(true)
        {
            ctrlProcessOnce(channels);
        }
    }
    else
    {
        while(time--)
        {
            ctrlProcessOnce(channels);
        }
    }
}

void CleanUpChannel(vector<Channel>& channels)
{
   /* int num=channels.size()-1;
   while(num)
   {
    channels[num].CloseWfd();
    channels[num--].WaitChild();
   } */
   //注意多个读端的问题
   for(auto& Channel:channels)
   {
    Channel.CloseWfd();
    Channel.WaitChild();
   }
}


int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " processnum" << std::endl;
        return 1;
    }
    int num = stoi(argv[1]);
    //函数指针数组
    InitTask();
    vector<Channel> channels;

    // 1. 创建信道和子进程
    CreateChannelAndSub(num, &channels, work);

    // 2. 通过channel控制子进程
    ctrlProcess(channels, 5);

    // 3. 回收管道和子进程. a. 关闭所有的写端 b. 回收子进程
    CleanUpChannel(channels);
    return 0;
}

//Task.hpp 函数的声明与实现可以放到一起
#include<iostream>
using namespace std;
#include<time.h>
#define TaskNum 3
typedef void(*task_t)();

task_t Task[TaskNum];



void Print()
{
    cout<<"I an a print task"<<endl;
}
void DownLoad()
{
    cout<<"I an a download task"<<endl;
}

void Fllush()
{
    cout<<"I an a Fllush task"<<endl;
}

void InitTask()
{
    srand(time(nullptr));
    Task[0]=Print;
    Task[1]=DownLoad;
    Task[2]=Fllush;
}

int SlectTask()
{
    int tasknum=rand()%TaskNum;
    return tasknum;
}

void ExcuteTask(int Num)
{
    if(Num<0||Num>TaskNum-1)
    {
        cout<<"excute task fail"<<endl;
        return;
    }
    else
    {
        Task[Num]();
    }
}

void work()
{
    while(true)
    {
        int Read_Task=0;
        ssize_t n=read(0,&Read_Task,sizeof(Read_Task));
        if(n==sizeof(int))
        {
            cout<<"child will excute task"<<endl;
            ExcuteTask(Read_Task);
            cout<<"-----------------------"<<endl;
        }
        else if(n==0)
        {
            cout<<"child quie"<<endl;
            break;
        }
    }   
}

4命名管道

命名管道的原理与匿名管道的差不多:

只不过由父子进程 之间的通信变为两个毫不相干的进程之间进行通信

4.1原理

两个不相关进程之间的通信:一个读,一个写:在对应的files结构体添加r的fd,w的fd:分别指向不同的file结构体执行不同的方法(但属性集合与操作集是共享的前面说过),指向同一个缓冲区进行实现;但如果要满足两个进程的需求:缓冲区里的数据要进行多次的刷新(OS不做如任何浪费时间的事),就得把刷新的通道给关闭掉,两个进程的通信在缓冲区里完成

但两个毫不相关的进程,怎么保证打开了同一个文件呢?

每一个文件,都有文件路径(唯一性):而这个文件就得是特殊文件即:(命名)管道文件!

4.2实现命名管道

思路:创建一个Server(服务)端:负责读Client的内容,一个Client(客户):负责写消息
1.写一个namepipe类:类中成员:pipe路径,fd文件描述符,id表示身份;

将所有要实现的方法函数(创建pipe,读pipe,写pipe...)共同写在这个类中(优雅)

cpp 复制代码
//NamePipe.hpp
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <fcntl.h>
using namespace std;

#define PCreater 0
#define PUser 1
#define Read O_RDONLY
#define Write O_WRONLY
#define Size 1024

class NamePipe
{
public:
    NamePipe(int id)
        : _id(id)
    {
        // server要进行创建管道文件
        if (_id == PCreater)
        {
            int n = mkfifo(_path.c_str(), 0666);
            if (n != 0) exit(-1);
            cout<<"Creater Create Pipe..."<<endl;
        }
    }

    bool OpenFifoForWrite()
    {
        return OpenFifo(Write);
    }
    int WriteFifo(const string &in)
    {
        return write(_fd, in.c_str(), in.size());
    }

    bool OpenFifoForRead()
    {
        return OpenFifo(Read);
    }
    int ReadFifo(string *out)
    {
        char OutBuffer[Size];
        ssize_t n = read(_fd, OutBuffer, sizeof(OutBuffer) - 1);
        if (n > 0)
        {
            OutBuffer[n] = '\0';
            *out = OutBuffer;
        }
        return n;
    }

    ~NamePipe()
    {
        if (_id == PCreater)
        {
            int n = unlink(_path.c_str());
            if (n != 0)
                cout << "unlink fail:" << errno << ":" << strerror(errno) << endl;
            cout << "Clean Fifo Suecss..." << endl;
        }
        if (_fd != -1)
            close(_fd); // 关文件描述符
    }

private:
    bool OpenFifo(int flags)
    {
        _fd = open(_path.c_str(), flags);
        if (_fd == -1)
        {
            cout << "open fail" << errno << ":" << strerror(errno) << endl;
            return false;
        }
        return true;
    }
    const string _path = "./myfifo";
    int _id;      // 身份
    int _fd = -1; // 文件描述符
};

2.Server进行读pipe

cpp 复制代码
//Server.cc
#include "NamePipe.hpp"

int main()
{
    NamePipe server(PCreater);
    if (server.OpenFifoForRead())
    {
        cout << "Server Create Pipe..." << endl;
        sleep(3);
        while (true)
        {
            string message;
            int n = server.ReadFifo(&message);
            if (n > 0)
            {
                cout << "Client say: " << message << endl;
            }
            else if (n == 0)
            {
                cout << "Client quit,Server quit" << endl;
                break;
            }
            else
            {
                cout << "Read fail" << endl;
                break;
            }
        }
    }
    return 0;
}

3.Clinet端进行写pipe

cpp 复制代码
//Client.cc
#include "NamePipe.hpp"

int main()
{
    NamePipe client(PUser);
    // write
    while (client.OpenFifoForWrite())
    {
        string message;
        cout << "Please Write Word >" << " ";
        getline(cin, message);
        int n = client.WriteFifo(message);
        if (n == -1)
        {
            cout << "write fail" << endl;
            break;
        }
    }
    return 0;
}

5共享内存

除了使用管道进行通信外,还有别的设计者从0开始,重新设计出一套(本地)通信的方案:

System V IPC:共享内存,消息队列,信号量;但在后面有了网络后,这些方案就逐渐处于淘汰的边缘了:在本文中只讲一下共享内存的原理与使用

5.1原理

通信的本质是让不同的进程看到同一份资源:为了让不同的进程看到,OS在物理内存中开辟一段内存空间,通过不同进程的页表映射到对应的虚拟地址空间中的共享区中;进程通过地址空间也就能拿到共享内存的起始虚拟地址,也就能让不同的进程访问到同一块内存空间进行通信啦

理解:

1.以上讲的内容,要明白这些事情都是OS做的

2.OS不知道用户创建进程A,进程B是要什么时候进行通信的:所以务必要提供系统调用

3.AB,CD,EF...共享内存存在系统中是可以存在多份,供不同个数,不同进程进行同时通信!

4.这样说OS就要对共享内存进行管理------先描述,在组织;

说明:它不是一段简单的内存空间,也要有描述并管理共享内存的数据结构和匹配的算法

5.共享内存 = 内存空间(数据) + 共享内存的属性

5.2代码实现

先来认识一下系统调用:

shmget的第一个参数:key_t key:key_t ftok的返回值;

为什么要让用户自己生成一个key值?

共享内存被创建出来要有一个自己的标识符来表示:方便OS对后续的管理;

共享内存被进程A创建出来,进程B要进行通信,它怎么知道共享内存被创建出来了

这就要有一个规定(规则)来标识共享内存创建的标志:用户生成的key值

有了key值,进程B就能知道共享内存被创建出来,从而进行访问啦!!
shmget的第三个参数:int shmflg:有系统提供选项来组合使用:(身份不同,参数不同)

我们要知道:共享内存是不随进程的结束而自动释放的,一直存在直到系统重启;

关于生命周期:共享内存随内核的,文件随进程的:不是一样的!!

开辟好共享内存后,要进行挂接才能进行使用:

1.实现出共享内存的整个生命周期的过程(类封装)

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <cstring>
using namespace std;

#define SCreater 0
#define SUser 1
const int ShmSize = 4096;

class shm
{
public:
    // 获取key值
    key_t GetKey()
    {
        key_t key = ftok(_pathname, _proj_id);
        return key;
    }
    // 获取shm
    int GetShm(key_t key, size_t size, int shmflg)
    {
        if (_id == SCreater)
            cout << "Shm Creating..." << endl;
        else if (_id == SUser)
            cout << "Get Shm..." << endl;
        sleep(2);
        return shmget(key, size, shmflg);
    }
    // 获取shm地址
    void *GetShmadd()
    {
        return _shmadd;
    }

    // 将当前进程进行挂接到shm上
    void* AttachShm()
    {
        void* shmadd = shmat(_shmid, nullptr, 0);
        if (shmadd == nullptr)
        {
            perror("shmat");
        }
        if (_id == SUser)
            cout << "User" << " Attach Shm..." << endl;
        else if (_id == SCreater)
            cout << "Creater" << " Attach Shm..." << endl;
        sleep(2);
        return shmadd;
    }

    // 取消挂接
    void ShmDeleteTouch()
    {
        if (_shmadd == nullptr)
            return;
        shmdt(_shmadd);
        if (_id == SCreater)
            cout << "Creater Decth Shm..." << endl;
        else if (_id == SUser)
            cout << "User Decth Shm..." << endl;
        sleep(2);
    }

    // 初始化
    shm(int id)
        : _id(id)
    {
        _key = GetKey();
        if (_id == SCreater)
            _shmid = GetShm(_key, ShmSize, IPC_CREAT | IPC_EXCL | 0666); // 加了excl存在就报错
        else if (_id == SUser)
            _shmid = GetShm(_key, ShmSize, IPC_CREAT | 0666); // 第二个参数以4096*n来设置
        // 进行挂接
        _shmadd = AttachShm();
    }

    ~shm()
    {
        ShmDeleteTouch(); // 取消挂接
        if (_id == SCreater)
        {
            int n = shmctl(_shmid, IPC_RMID, nullptr);
            cout << "Shm Delete..." << endl;
            sleep(2);
        }
    }
    // 共享区清空并初始化
    void Zero()
    {
        if (_shmadd)
        {
            memset(_shmadd, 0, ShmSize);
        }
    }

private:
    const char *_pathname = "/root/Code/Shm"; // 当前路径
    int _proj_id = 0x66;                      // 随便设置
    key_t _key;                               // 用户设置
    int _shmid;                               // 系统提供
    int _id;                                  // 身份
    void *_shmadd = nullptr;                  // 共享内存地址
};

2.客户端负责写,服务端负责读:

cpp 复制代码
//Server.cc
int main()
{
   //读
   shm Server(SCreater);
   char * shmadd=(char*)Server.GetShmadd();
   while(true)
   {
      cout<<"Client say:"<<shmadd<<endl;
      sleep(1);
   }
   return 0;
}

//Client.cc
int main()
{
    shm Client(SUser);
    Client.Zero();//写之前先清空
    char *shmadd=(char*)Client.GetShmadd();
    char ch='A';
    while(ch!='G')
    {
        shmadd[ch-'A']=ch;
        ch++;
        sleep(2);
    }
    return 0;
}

执行代码:

我们会发现:

1.Server端在Client端退出后(不写了)还在继续读,而使用命名管道则不会这样

2.Server端在读的时候是不管Client端写没写完成就进行读取,而管道会继续阻塞等待

说明:1.共享内存不提供如何的保护机制;2.造成数据不一致问题

5.3优化

解决上面的问题,我们引入管道来解决:(用前面写好的命名管道代码)

cpp 复制代码
//Server.cc
#include "shm.hpp"
#include "NamePipe.hpp"
int main()
{
   //1.创建共享内存
   shm Shm(SCreater);
   char *shmaddr = (char *)Shm.GetShmadd();

   // 2. 创建管道(提供保护机制)
   NamePipe fifo(PCreater);
   fifo.OpenFifoForRead();

   while (true)
   {
      std::string tmp;//不重要
      int n=fifo.ReadFifo(&tmp);// 没有写入就阻塞

      if(n==0) break;//Client端没写数据了就结束read(只是方便当前Server最后的回收)

      std::cout << "Shm Memory : " << shmaddr << std::endl;
   }

   return 0;
}

//Client.cc
#include "shm.hpp"
#include "NamePipe.hpp"
int main()
{
    //1.创建共享内存
    shm Shm(SUser);
    //写数据前先清空
    Shm.Zero();
    char *shmaddr = (char *)Shm.GetShmadd();

    // 2.打开管道
    NamePipe fifo(PUser);
    fifo.OpenFifoForWrite();

    char ch = 'A';
    while (ch <= 'G')
    {
        shmaddr[ch - 'A'] = ch;
        std::cout << "Add " << ch++ << " Into Shm Memory" << std::endl;

        std::string tmp="wake up";//让Server read数据
        fifo.WriteFifo(tmp);
        sleep(1);
    }
    return 0;
}

总结:

1.创建共享内存时,共享内存不通过如何的保护机制:会产生数据不一致问题;

但我们创建好共享内存后,直接使用即可,不需要使用如何的系统调用(而管道需要调用read,write等系统调用来实现);这也说明:

2.共享内存是所有进程IPC中最快的:它大大减少了拷贝次数!!

6.消息队列(了解)

6.1原理

原理:一个进程进行接受,另一个发送有类型的数据块的方式:

进程A拿队列中B的数据块,进程B拿队列中A的数据块

6.2接口

因为与共享内存都是System V IPC的通信版本,设计接口时都是大差不差的:

在发送和收消息时要自定义结构体类型:

7信号量

7.1五大概念渗透

a.多个执行流(进程)能看到的一份资源------共享资源->共享内存

b.共享资源数据不一致问题;要进行保护:被保护的资源------临界资源

c.通过互斥(同步)的方式来保护临界资源;互斥------任何时刻只能有一个进程在访问资源

d.访问资源:(程序员)通过代码来访问:代码------访问共享资源 +不访问共享资源

e.所谓的对资源进行保护------本质上是对访问共享资源的代码进行保护(加锁)

f.共享资源------临界区;非共享资源------非临界区

7.2信号量理解

信号量(信号灯):保护临界资源(Code)

a.共享内存不整体使用时:

OS会划分为一个一个的数据块,进程要访问数据块之前要先申请信号量,申请完后这个数据块就只有一个进程能访问并进行使用:

而信号量根据块数的多少来确定初始值:有人来申请就进行--操作:它本质上相当于计数器

这就相当于电影院的购票机制:

有多少个座位,在购票系统里count就初始化多少:有人完成购票操作就继续count--;

我是在购票后还是到电影院坐下后,这个位置才是我的?------当然是在购票完成后,这个座位就是属于我的!!(我们国民都是有素质的群众)
在这里:电影院------共享内存;购票------申请数据块(临界资源)

进行申请(购票)的本质:对临界资源的预定机制

但我们最担心的还是申请的块数 > 总共资源的块数(100张电影票卖出去了102张)

这就需要来让执行流(购票系统)与资源(座位)进行一一对应(程序员实现)暂时不关心

b,共享资源整体使用时

比如:电影院的超级VIP,只有一个座位:购票完成后只有购票者有权利使用

这不就是说:对整体资源的使用,不就是说资源只有一个吗?

这个过程不就是我们在上面说的互斥方式吗?

这种信号量为1 or 0:我们称之为二元信号量

而上面的情况的信号量我们称之为多元信号量
信号量本质上是一个计数器:那我们可不可以创建一个全局变量:gcount来替代它呢?

不能!!

1.全局变量不能被所有进程看到(父子进程即使看到了,修改它也会发生写时拷贝)

2.gcount++,不是原子的(多线程重点叙述)

7.3信号量操作

信号量作为进程间通信的其中一种方式:和共享内存,消息队列一样:必须先让不同的进程看到同一个信号量(计数器);这也意味着:信号量本身也是共享资源

信号量是用来保护临时资源的安全的:这是不是你自己得是安全的啊!!

信号量--:安全性通过P操作来保证:信号量++:安全性通过V操作来保证

也可以说:PV操作共同来保证信号量是原子的!!(简单理解:原子的就是它只在意结果)

三OS管理进程间通信

由于共享内存,消息队列,信号量这三个都是基于System V版本说出现的三个进程间通信的方式,那么:这三个在OS内部是如何管理的呢?

在这三个方式删除(回收)的系统接口手册中,我们发现:这三个的结构体名字是一样的(会不会是巧合?);结构体里第一个储存都是IPC_Prem结构体:

我们往OS内部去探索发现:OS用一个kern_ipc_perm结构体数组来共同管理:

在ipc_id中,用一个重要的成员变量:struct kern_ipc_pern p[]->结构体柔性数组(可变长)

在这个数组里储存着shm,smg,sem的IPC_Prem结构体的地址!!

用指令删除shm,smg,sem用到的是shmid:这个shmdid对应的是kern_ipc_pern p 数组下标!

要访问类型就要先强转成对应的类型在用->进行使用其它成员~

但是怎么知道它是什么类型呢??

在结构体数组中有成员mode来标明对应是什么类型!!

以上便是进程间通信的所以内容:有问题在评论区指出,感激不尽!!

相关推荐
即将头秃的程序媛27 分钟前
centos 7.9安装tomcat,并实现开机自启
linux·运维·centos
fangeqin36 分钟前
ubuntu源码安装python3.13遇到Could not build the ssl module!解决方法
linux·python·ubuntu·openssl
爱奥尼欧2 小时前
【Linux 系统】基础IO——Linux中对文件的理解
linux·服务器·microsoft
超喜欢下雨天2 小时前
服务器安装 ros2时遇到底层库依赖冲突的问题
linux·运维·服务器·ros2
tan77º3 小时前
【Linux网络编程】网络基础
linux·服务器·网络
笑衬人心。4 小时前
Ubuntu 22.04 + MySQL 8 无密码登录问题与 root 密码重置指南
linux·mysql·ubuntu
chanalbert5 小时前
CentOS系统新手指导手册
linux·运维·centos
星宸追风6 小时前
Ubuntu更换Home目录所在硬盘的过程
linux·运维·ubuntu
热爱生活的猴子6 小时前
Poetry 在 Linux 和 Windows 系统中的安装步骤
linux·运维·windows
myloveasuka6 小时前
[Linux]内核如何对信号进行捕捉
linux·运维·服务器