Bootstrap

Linux:进程间通信

1.什么是进程通信?

进程具有独立性,进程要通信-信息数据交互,就会打破进程独立性的特征,成本不会低。为了同时保证进程的独立性完成数据通信,操作系统需要直接或间接给通信双方的进程提供“内存空间”。要通信的进程,必须看到同一份公共资源!不同类型的资源是由操作系统中不同的模块所提供的。

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

2.为什么要进行进程通信

有时候我们是需要多进程协同的!

cat file | grep "hello" cat将file中内容输出到显示器,grep将数据源(cat中的内容)按照标准输入关键字"hello"搜索。这就是一个进程间协同完成的任务。

3.进程间通信的发展

发展产生的两套标准:

POSIX     —让通信过程可以跨主机
System V —聚焦在本地通信(共享内存、消息队列、信号量——进程通信的不同模块)

1.管道-基于文件系统

管道是进程间通信最古老的方式,我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。管道是一种内存级文件,它不关心文件在磁盘什么路径下,它只需要创建struct file对象创建它的内存缓冲区,不需要访问磁盘这样的外设,完成内存级数据交互。
怎么让:父进程打开一个文件,fork创建子进程继承父进程文件描述符表中的内容就会得到文件地址,这个通过文件地址看到的文件,没有文件名称所以称作匿名管道。匿名管道目前只能用来进行父子进程之间互相通信。 

1.1让不同的进程看到同一份资源:

管道是父进程通过调用管道系统调用以读方式和写方式打开一个内存级文件并通过fork创造子进程的方式被子进程继承下去之后各自再关闭对应的读写端进而形成一条通信信道,这条通信信道是基于文件的。

为什么要让父进程以读写方式打开文件呢?—只读或只写那么子进程就只会继承只读或只写,要让子进程以读写方式打开,然后关闭不需要的文件描述符选择特定的通信方式即可。

为什么要创建子进程?—形成进程间通信;让子进程继承父进程的读写文件。

1.2实现父进程进行读取子进程写入的单向管道--匿名管道

#include <unistd.h>
int pipe(int pipefd[2]);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
//write和read接口的使用:此时将数据读取或者写入管道实际上是对操作系统文件处理,所以需要调用系统接口
//管道通信代码:
#include <iostream>
#include<cassert>//c/c++混编的时候加C
#include<unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<string>
#include<cstring>
#include <stdio.h>
using namespace std;

//实现父进程进行读取子进程写入的单向管道
int main()
{
    //第一步:创建管道文件,打开读写端
    int fds[2];
    int n = pipe(fds);
    assert(n==0);
    //第二步:fork创建子进程
    pid_t id = fork();
    assert(id>=0);
    if(id==0) 
    {//子进程进行写入
        close(fds[0]);//关闭读取
        // string msg = "hello,i am child";
        const char *s = "我是子进程,我正在给你发消息";
        int cnt = 0;
        while(true)
        {
            cnt++;
            char buffer[1024];//缓冲区-只有子进程能看到
            snprintf(buffer,sizeof buffer,"child->parent say:%s[%d][%d]",s,cnt,getpid());
            write(fds[1],buffer,strlen(buffer));//向文件写入不需要写入\0
            sleep(1);//每隔一秒向管道/缓冲区中写一次
        }
        exit(0);
    }
    //父进程进行读取
    close(fds[1]);//关闭写入
    while(true)
    {
        char buffer[1024];
        //ssize_t长整型
        ssize_t s = read(fds[0],buffer,sizeof buffer -1);//从文件读出来字符串,空留一个\0的位置
        if(s > 0) buffer[s] = 0;//从文件读出的字符串是没有\0结尾的
        cout <<"Get Message"<<buffer<<" | Mypid:"<<getpid()<<endl;
    }//细节:父进程并没有sleep
    n = waitpid(id,nullptr,0);
    assert(n==id);
    //0,1,2->3,4
    // cout<<"fds[0]:"<<fds[0]<<endl;//读
    // cout<<"fds[1]:"<<fds[1]<<endl;//写
    return 0;
}


运行程序:子进程成功每隔1秒给父进程发一次消息

打开另一个shell同时查看,此时确实有父子两个进程:


注意:
读快、写慢:写端阻塞等待;如果管道中没有了数据,读端在读,默认会直接阻塞当前正在读取的进程!
读慢、写快:如果管道文件写端一直写入但是读端不读的话,缓冲区就会被写满;写端写满的时候再写会阻塞等对方进行读取。管道中的内容在读端每次读取后清空。

写关闭、读到0:
读关闭、写端终止:读端关闭,仍旧写的话就是浪费资源。操作系统通过发送信号的方式杀掉写端,进程异常退出,通过获取进程退出码可以验证(13 SIGPIPE)。

管道的特征:

1.管道的生命周期随进程(管道基于文件,和文件的特征相似)

2.管道可以用来进行具有血缘关系进程之间进行通信,常用于与父子通信。

3.管道是面向字节流的(不关心数据类型只按照字节读取数据)。

4.半双工通信--任何一个时刻只允许一方向另一方发送数据;单向通信是其中一种。

5. 互斥与同步机制---对共享资源进行保护的方案。

1.3基于匿名管道的进程池设计

#include<iostream>
#include<unistd.h>
#include<cstdlib>
#include<cassert>
#include<vector>
#include<string>
#include<ctime>
#include<sys/wait.h>
#include<sys/types.h>
#define PROCESS_NUM 5
#define MakeSeed() srand((unsigned)time(nullptr)^getpid()^0x171237^rand()%1234)

//函数指针类型
typedef void(*func_t)();
void download()
{
    std::cout<<getpid()<<":下载任务\n"<<std::endl;
    sleep(1);
}
void ioTask()
{
    std::cout<< getpid()<< ":IO任务\n" << std::endl;
    sleep(1);
}
void flushTask()
{
    std::cout<<getpid()<<":刷新任务\n"<<std::endl;
    sleep(1);
}
void loadtaskFunc(std::vector<func_t> *out)
{
    assert(out);
    out->push_back(download);
    out->push_back(ioTask);
    out->push_back(flushTask);
}


//下面的代码是一个多进程程序

class subEp//Endpoint(一个子进程和它对应的管道组合)
{
public:
    subEp(pid_t subId,int writeFd)
    :subId_(subId),writeFd_(writeFd)
    {
        char nameBuffer[1024];
        snprintf(nameBuffer,sizeof nameBuffer,"process-%d[pid(%d)-fd(%d)]",num++,subId_,writeFd_);//构建一个独有的name
    }
public:
    static int num;
    std::string name_;//该组合的属性
    pid_t subId_;//子进程的pid
    int writeFd_;//该写管道的文件描述符
};
int subEp::num = 0;

int recvTask(int readFd)
{
    int code = 0;
    ssize_t s = read(readFd,&code,sizeof code);//读四个字节读到code中
    if(s==4) return code;//读到任务码  
    else if(s<=0) return -1;//没有读到内容
    else return 0;
    assert(s == sizeof(int));
    return code;
}

void sendTask(const subEp &process,int taskNum)
{
    std::cout << "send task num :"<<taskNum << " send to "<<process.name_<<std::endl;
    int n = write(process.writeFd_,&taskNum,sizeof(taskNum));//将任务编号写入写端
    assert(n == sizeof(int));
    (void)n;
}

void createSubProcess(std::vector<subEp> *subs,std::vector<func_t>& funcMap)
{
    std::vector<int> deleteFd;
    for(int i=0;i<PROCESS_NUM;i++)
    {
        int fds[2];
        int n = pipe(fds);
        assert(n==0);
        (void)n;
        //bug???
        pid_t id = fork();//子进程不仅会继承新管道父进程的文件描述符,还会继承以前父进程写入兄弟子进程的文件描述符
        if(id == 0)//fork在子进程中返回0
        {
            for(int i=0;i<deleteFd.size();i++) close(deleteFd[i]);
            //子进程,进行处理任务
            close(fds[1]);
            while(true)
            {
                //1.获取命令码,如果没有发送,子进程就阻塞等待
                int commandCode = recvTask(fds[0]);//子进程从读端拿到任务码
                //2.完成任务
                if(commandCode >= 0 && commandCode < funcMap.size()) funcMap[commandCode]();//根据命令表调用
                else if(commandCode==-1) break;//终止子进程
                else std::cout<<"sub recv code error!"<<std::endl;
            }
            exit(0);//子进程退出
        }
        close(fds[0]);//关闭父进程读端
        subEp sub(id,fds[1]);//给每个sub对象存入子进程id和父进程写端文件描述符
        subs->push_back(sub);
        deleteFd.push_back(fds[1]);//
    }
}

void loadBlanceControl(std::vector<subEp> subs,std::vector<func_t> funcMap,int count)
{
    int processnum = subs.size();//管道-子进程对个数
    int tasknum = funcMap.size();//任务个数
    int forever = (count==0) ? true:false;//count为0永远为true
    while(true)
    {
        //a.先选择一个子进程->std::vector<subEp> -> index,避免一直是一个子进程执行;让子进程进行负载均衡
        int subIdx = rand() % processnum;
        //b.选择一个任务   ->std::vector<funcMap> -> index
        int taskIdx = rand() % tasknum;
        //c.把任务发送给选择的进程
        sendTask(subs[subIdx],taskIdx);
        sleep(1);
        if(!forever)
        {
            count--;
            if(count==0) break;
        }
    }
    //写端退出,读端读到0退出
    for(int i=0;i<processnum;i++) close(subs[i].writeFd_);
}

void waitProcess(std::vector<subEp> processes)
{
    for(int i=0;i<processes.size();i++)
    {
        waitpid(processes[i].subId_,nullptr,0);
        std::cout<<"wait sub process success ..."<<processes[i].subId_<<std::endl;
    }
}

int main()
{
    MakeSeed();
    //1.建立子进程并建立子进程通信的信道
    //[子进程id,wfd]加载方法表
    std::vector<func_t> funcMap;//创建该对象用于组织所有任务函数
    loadtaskFunc(&funcMap);//给funcMap赋值
    std::vector<subEp> subs;//先描述再组织,创建该对象用于组织每对管道和子进程
    createSubProcess(&subs,funcMap);//将多个任务和多对管道子进程传送
    //2.走到这里的只能是父进程,控制子进程
    int taskCnt = 3;//设置任务个数为20
    loadBlanceControl(subs,funcMap,taskCnt);//负载均衡的向子进程发送命令码
    //3.回收子进程信息
    waitProcess(subs);
}//fork()

 2.命名管道 

两个没有血缘关系、毫不相干的进程之间。

命名管道是如何做到让不同的进程看到了同一份资源呢?

可以让不同的进程打开指定名称(路径+文件名->具有唯一性)的同一个文件;在磁盘中创建一个文件,该文件在Linux下具有唯一性,所以命名管道可以通过名字来具有唯一性;匿名管道其实就是malloc的struct file对象,它的地址也具有唯一性。


必须读端和写端都将文件都open时,进程才会将代码继续向下执行。

2.1client.cc从命名管道文件读取,将内容打印到屏幕上:

#include"comm.hpp"

int main()
{
    int wfd = open(NAMED_PIPE,O_WRONLY);
    if(wfd<0) exit(1);
    //write
    char buffer[1024];
    while(true)
    {
        std::cout<<"Please Says# ";
        //fgets是C语言接口,会自动加\0
        fgets(buffer,sizeof(buffer),stdin);//把数据从特定的流读到缓冲区,按行;从键盘读入,会多按一次回车
        if(strlen(buffer)) buffer[strlen(buffer)-1] = 0;
        size_t w = write(wfd,buffer,strlen(buffer));
        assert(n==strlen(buffer));
        (void)n;
    }
    close(wfd);
    return 0;
}
//将通信过程变成文件的读写!!!

2.2server.cc向命名管道写入,写入流从stdin获取

#include"comm.hpp"
int main()
{
    bool ret = createFifo(NAMED_PIPE);
    assert(ret);
    (void)ret;
    int rfd = open(NAMED_PIPE,O_RDONLY);
    if(rfd<0) exit(1);
    //read
    char buffer[1024];
    while(true)
    {
        ssize_t s = read(rfd,buffer,sizeof(buffer)-1);
        if(s>0)
        {
            buffer[s] = 0;
            std::cout<<"client->server:"<<buffer<<std::endl;//将Buffer中的内容输出到屏幕
        }
        else if(s==0)//对方的写文件描述符关闭
        {
            std::cout<<"client quit,me too!"<<std::endl;
            break;
        }
        else
        {
            std::cout<<"err string:"<<strerror(errno)<<std::endl;
            break;
        }
    }
    close(rfd);
    removefifo(NAMED_PIPE);
    return 0;
}

2.3comm.hpp头文件,定义创建命名管道的方法和移除命名管道的方法

#pragma once
#include<iostream>
#include<string>
#include<cstring>
#include<sys/types.h>
#include<sys/stat.h>
#include<cassert>
#include<cerrno>
#include<unistd.h>
#include<fcntl.h>
#define NAMED_PIPE "/home/bfr/linux1.15/name_pipe/mypipe.115"
bool createFifo(const std::string &path)
{
    umask(0);
    int n = mkfifo(path.c_str(),0600);//路径,创建权限-只允许文件拥有者通信
    if(n==0) return true;
    else
    {
        std::cout<<"errno:"<<errno<<" err string:"<<strerror(errno)<<std::endl;
        return false;
    }
}
void removefifo(const std::string &path)
{
    int n = unlink(path.c_str());
    assert(n == 0);//unlink删除文件成功执行
    //意料之中使用assert,意料之外if;assert仅在debug中有效,release里面就没有了
    (void)n;//以防warning:定义一个变量但是没有使用
}

 

;