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来标明对应是什么类型!!

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

相关推荐
算法与编程之美20 分钟前
文件的写入与读取
linux·运维·服务器
xianwu54337 分钟前
反向代理模块
linux·开发语言·网络·git
Amelio_Ming1 小时前
Permissions 0755 for ‘/etc/ssh/ssh_host_rsa_key‘ are too open.问题解决
linux·运维·ssh
Ven%2 小时前
centos查看硬盘资源使用情况命令大全
linux·运维·centos
TeYiToKu2 小时前
笔记整理—linux驱动开发部分(9)framebuffer驱动框架
linux·c语言·arm开发·驱动开发·笔记·嵌入式硬件·arm
dsywws3 小时前
Linux学习笔记之时间日期和查找和解压缩指令
linux·笔记·学习
yeyuningzi3 小时前
Debian 12环境里部署nginx步骤记录
linux·运维·服务器
上辈子杀猪这辈子学IT3 小时前
【Zookeeper集群搭建】安装zookeeper、zookeeper集群配置、zookeeper启动与关闭、zookeeper的shell命令操作
linux·hadoop·zookeeper·centos·debian
minihuabei3 小时前
linux centos 安装redis
linux·redis·centos
lldhsds4 小时前
书生大模型实战营第四期-入门岛-1. Linux前置基础
linux