进程间通信

进程间通信

  • 进程间通信
    • 概念
    • 目的
    • 方式
  • 管道
    • 概念
    • 匿名管道
      • 实例代码
      • 特点
      • 进程池
    • 命名管道
      • 概念
      • 创建命名管道
  • system v共享内存
    • 概念
    • 代码实现
      • 总结

进程间通信

概念

在之前的学习中,我们知道进程是具有独立性的,现在却需要让进程之间进行通信,代价肯定是比较大的,而且为什么要进行通信呢?这就是通信的目的;通信该如何完成呢?

既然进程需要将数据传递给双方,肯定需要一块内存空间用来存放数据,这块空间不能由进程其中一方提供,因为进程是具有独立性的;进行通信时,除了进程双方,操作系统也参与进来,所以需要操作系统直接或间接给进程进程双方提供内存空间,让进程双方都能“看到”这一资源;不同的通信,所需要的空间也不同

目的

  1. 数据传输:一个进程需要将自己的数据发送给另一个进程
  2. 资源共享:多个进程之间共享同样的资源
  3. 通知事件:一个进程需要向另一个或一组进程发送消息,通知
  4. 进程控制:有些进程希望完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

这里介绍一个简单的场景:cat file|grep 'hello'该指令是,将文件file的内容打印到显示器上,不过呢,先需要经过grep进行过滤,才会将内容进行打印;两个进程之间通过|完成了某种意义上的联系,这里也就是通信

方式

System V:
可以使进程跨主机进行通信

POSIX IPC
将进程聚焦在本地进行通信

管道:

  1. 匿名管道
  2. 命名管道

前两种较为复杂,这里就学习以管道的方式进行通信

管道

概念

管道是unix中最古老的进程间通信的方式;将一个进程连接到另一个进程的一个数据流称作一个管道

父进程通过调用管道特定的系统调用,以读方式和写方式打开一个内存级文件,并通过创建子进程的方式,被字进程继承之后,关闭各自的读写端,进而形成一个通信信道

匿名管道

在上一章节中学习到,当在磁盘上打开文件时,操作系统会生成对应的struct file进行管理;通过管道进行通信时,也存在文件,称作管道文件,属于内存级文件,没有名称,称作匿名管道

图解:
进程间通信_第1张图片

父进程通过文件描述符表,映射到对应的文件结构体;子进程继承父进程的文件描述表,指向同一个结构体;父进程可以通过结构体将内容写到磁盘中,子进程再到磁盘上进行读,不过这样做,效率太低,毕竟是通信;所以父进程所打开的是管道文件,也就是在内存中;结构体提供了父子进程都能够使用的内存空间(缓冲区)

内存空间已经提供,接下来就是父子进程的通信过程
图解:

进程间通信_第2张图片

父进程之所以需要以读写两中方式打开管道文件,是因为如果只以一种方式打开文件的话,子进程也就继承父进程打开文件的方式,并且两者之间不能进行通信

进程间通信_第3张图片
进程间通信_第4张图片

匿名管道目前用来进行父子进程之间的通信

实例代码

先介绍创建管道的函数
int pipe(int pipefd[2]);
数组中第一个元素表示读端
pipefd[0] refers to the read end of the pipe
数组中第二个元素表示写端
pipefd[0] refers to the read end of the pipe

返回值
On success, zero is returned. On error, -1 is returned, and errno is set appropriately

代码演示

using namespace std;
int main()
{
    int fds[2];
    int n=pipe(fds);
    assert(n==0);
    cout<<"fds[0]"<<fds[0]<<endl;
    cout<<"fds[1]"<<fds[1]<<endl;
    return 0;
}

进程间通信_第5张图片

由结果可知,读写端分别对应文件描述符表下标的3,4

makefile

mypipe:mypipe.cpp
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f mypipe

mypipe.cpp
头文件

#include
#include
#include
#include
#include
#include
#include
#include

子进程每隔五秒写一次数据

int main()
{
    //父进程以读写打开文件
    int fds[2];
    int n=pipe(fds);
    assert(n==0);

    //创建子进程
    pid_t id=fork();
    assert(id>=0);
    //子进程
    if(id==0)
    {
        //子进程进行写入,关闭读端
        close(fds[0]);
        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));
            cout<<"count: "<< cnt <<endl;
            sleep(5);
        }
        //关闭写端
        close(fds[1]);
        cout<<"子进程关闭写端"<<endl;
        exit(0);
    }

    //父进程关闭写端
    close(fds[1]);
    //父进程通信代码
    while(true)
    {
        char buffer[1024];
        cout<<"*************************"<<endl;
        ssize_t s=read(fds[0],buffer,sizeof(buffer)-1);
        cout<<"#########################"<<endl;
        if(s>0)
        {
            buffer[s]=0;
        }
        cout<<"Get Message# "<<buffer<<"|my pid"<<getpid()<<endl;
    }
    //回收进程
    n=waitpid(id,nullptr,0);
    assert(n==id);
    return 0;
}

进程间通信_第6张图片

此时如果管道中没有数据,读端,默认会发生堵塞

写端一直写,读端休眠1000秒

int main()
{
    //父进程以读写打开文件
    int fds[2];
    int n=pipe(fds);
    assert(n==0);

    //创建子进程
    pid_t id=fork();
    assert(id>=0);
    //子进程
    if(id==0)
    {
        //子进程进行写入,关闭读端
        close(fds[0]);
        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));
            cout<<"count: "<< cnt <<endl;
        }
        //关闭写端
        close(fds[1]);
        cout<<"子进程关闭写端"<<endl;
        exit(0);
    }

    //父进程关闭写端
    close(fds[1]);
    //父进程通信代码
    while(true)
    {
        sleep(1000);
        char buffer[1024];
        cout<<"*************************"<<endl;
        ssize_t s=read(fds[0],buffer,sizeof(buffer)-1);
        cout<<"#########################"<<endl;
        if(s>0)
        {
            buffer[s]=0;
        }
        cout<<"Get Message# "<<buffer<<"|my pid"<<getpid()<<endl;
    }
    
    //回收进程
    n=waitpid(id,nullptr,0);
    assert(n==id);
    return 0;
}

进程间通信_第7张图片

写端将内存空间写满时,再进行写入会发生堵塞,需要等待读端进行读取

子进程写一次直接退出,并且关闭自己的写端

int main()
{
    //父进程以读写打开文件
    int fds[2];
    int n=pipe(fds);
    assert(n==0);
    //创建子进程
    pid_t id=fork();
    assert(id>=0);
    //子进程
    if(id==0)
    {
        //子进程进行写入,关闭读端
        close(fds[0]);
        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));
            cout<<"count: "<<cnt<<endl;
            break;
        }
        //关闭写端
        close(fds[1]);
        cout<<"子进程关闭写端"<<endl;
        exit(0);
    }
    //父进程关闭写端
    close(fds[1]);
    //父进程通信代码
    while(true)
    {
        char buffer[1024];
        ssize_t s=read(fds[0],buffer,sizeof(buffer-1));
        if(s>0)
        {
            buffer[s]=0;
            cout<<"Get Message"<<buffer<<"|my pid"<<getpid()<<endl;
        }
        else if(s==0)
        {
            //读取到文件末尾
            cout<<"read:"<<s<<endl;
            break;
        }
    }
    int status=0;
    n=waitpid(id,&status,0);
    assert(n==id);
    cout<<"pid->"<<n<<" "<<(status&0x7F)<<endl;
    return 0;
}

进程间通信_第8张图片

父进程进行读取,将内容读取完之后,直接退出进程

子进程一直写,父进程读取一次直接退出

int main()
{
    //父进程以读写打开文件
    int fds[2];
    int n=pipe(fds);
    assert(n==0);
    //创建子进程
    pid_t id=fork();
    assert(id>=0);
    //子进程
    if(id==0)
    {
        //子进程进行写入,关闭读端
        close(fds[0]);
        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));
            cout<<"count: "<<cnt<<endl;
        }
        //关闭写端
        close(fds[1]);
        cout<<"子进程关闭写端"<<endl;
        exit(0);
    }
    //父进程关闭写端
    close(fds[1]);
    //父进程通信代码
    while(true)
    {
        char buffer[1024];
        ssize_t s=read(fds[0],buffer,sizeof(buffer-1));
        if(s>0)
        {
            buffer[s]=0;
            cout<<"Get Message"<<buffer<<"|my pid"<<getpid()<<endl;
        }
        else if(s==0)
        {
            //读取到文件末尾
            cout<<"read:"<<s<<endl;
            break;
        }
        break;
    }
    close(fds[0]);
    cout<<"父进程关闭读端"<<endl;
    int status=0;
    n=waitpid(id,&status,0);
    assert(n==id);
    cout<<"pid->"<<n<<" "<<(status&0x7F)<<endl;
    return 0;
}

进程间通信_第9张图片

操作系统会直接终止写段,杀死进程

特点

  1. 只能用于具有公共祖先的进程之间的进程;通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可使用该管道
  2. 管道提供流式服务
  3. 进程退出,管道释放,管道生命周期随进程
  4. 内核会对管道操作进行同步和互斥
  5. 管道是半双工,数据只能向一个方向流动;如果需要双方通信,需要建立两个管道

进程池

父进程通过管道创建多个子进程:向每个管道中都写入不同的任务,每个子进程都读入到相应的任务并执行

图解:
进程间通信_第10张图片

命名管道

概念

匿名管道应用的一个限制就是只能在具有公共祖先的进程间通信;如果想要在不相关的进程之间交换数据,就可以使用命名管道,命名管道是一种特殊类型的文件

假如进程a指向一个名为named_pipe的文件(保存在磁盘上),此时又有一个进程b也要指向文件named_pipe;由于两个进程指向同一个文件,则操作系统便只会创建一个对应的文件结构体struct file供进程使用;进程a此时要向文件内写入,进程b要向文件中读取,此情此景是不是和上面匿名管道一致呢?

不同的是这次的进程没有任何关系,通过打开指定文件(路径+文件名),给两进程提供一份共同的资源,也称作命名管道
图解:

进程间通信_第11张图片

创建命名管道

先介绍创建命名管道的函数
int mkfifo(const char *pathname, mode_t mode);

返回值:
On success mkfifo() returns 0. In the case of an error, -1 is returned

创建两个进程,进行通信
student.cpp

学生对老师说话

#include"test.hpp"
int main()
{
    cout<<"student begin: "<<endl;
    int wfd=open(NAME_PIPE,O_WRONLY);
    cout<<"student end: "<<endl;
    if(wfd<0)
    {
        exit(1);
    }
    char buffer[1024];
    while(true)
    {
        cout<<"Please say: ";
        fgets(buffer,sizeof(buffer),stdin);
        if(strlen(buffer)>0)
        {
            buffer[strlen(buffer)-1]=0;
        }
        ssize_t s=write(wfd,buffer,strlen(buffer));
        assert(s==strlen(buffer));
        (void)s;
    }
    close(wfd);
    return 0;
}

teacher.cpp
老师读取学生说的话

#include"test.hpp"
int main()
{
    bool r=creatfifo(NAME_PIPE);
    assert(r==true);
    (void)r;
    cout<<"teacher begin: "<<endl;
    int rfd=open(NAME_PIPE,O_RDONLY);
    cout<<"teacher end: "<<endl;
    if(rfd<0)
    {
        exit(1);
    }
    char buffer[1024];
    while(true)
    {
        ssize_t s=read(rfd,buffer,sizeof(buffer)-1);
        if(s>0)
        {
            buffer[s]=0;
            cout<<"student->teacher: "<<buffer<<endl;
        }
        else if(s==0)
        {
            cout<<"teacher quit"<<endl;
            break;
        }
        else
        {
            cout<<"errno"<<strerror(errno)<<endl;
            break;
        }
    }
    close(rfd);
    removefifo(NAME_PIPE);
    return 0;
}

test.hpp

#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define NAME_PIPE "/tmp/mypipe.named"
bool creatfifo(const string &path)
{
    int n=mkfifo(path.c_str(),0600);
    if(n==0)
    {
        return true;
    }
    else 
    {
        cout<<"errno: "<<errno<<strerror(errno)<<endl;
        return false;
    }
}

void removefifo(const string&path)
{
    int n=unlink(path.c_str());
    assert(n==0);
    (void)n;
}

判断语句assertif的区别,前者是明确结果进行判断,后者是不清楚结果进行判断
这里解释一下(void)n的作用,首先在debug版本下,该语句和上一条判断会被执行;在release版本下,判断语句assert(n==0)是不会被执行的,当执行下一条语句是,程序会报错因为变量n产生之后并没有被使用,所以(void)n的作用就是使变量被使用一次

在这里插入图片描述

简单的命名管道就完成了!!!

system v共享内存

概念

共享内存的概念:通过让不同的进程看到同一个内存块的方式称作共享内存

上面两种管道实现进程间通信都是基于文件完成的,共享内存是基于内存完成的,原理解释如下:
假设存在两个进程,分别通过自己的进程空间在页表中映射到内存的不同区域
图解:

进程间通信_第12张图片

如果要让这两个进程进行通信,首先操作系统需要在内存中提供一份资源,有了资源,还需要让这两个进程都能看到,所以将创建好的内存资源通过两页表映射到进程中
图解:

进程间通信_第13张图片

在将来如果不想进行通信可以取消进程和内存的关系也称去关联,然后再去释放共享内存

上面只是假设两个进程进行通信,其实系统中只要是想进行通信的进程都可以是共享内存,毕竟这只是一个方式,还有操作系统中一定是存在着许多个共享内存的

代码实现

首先,创建一块共享内存

int shmget(key_t key, size_t size, int shmflg);

  1. shmglg:本质就是宏,存在两个选项:IPC_CREAT表示如果共享内存不存在,则创建,如果已经存在则进行获取;IPC_EXCL需要和第一个宏一起使用,IPC_CREAT|IPC_EXCL如果共享内存不存在,进行创建,如果存在,返回错误
  2. size_t size:共享内存的大小
  3. key_t key:此参数是为了确保进程可以看到同一内存
    key_t ftok(const char *pathname, int proj_id);:进行通过路径名称和项目id便可以到内存中找到同一区域,返回key标识这一内存,也就做到了让不同的内存看到同一资源
  4. 返回值On success, a valid shared memory identifier is returned. On errir, -1 is returned, and errno is set to indicate the error.:创建成功,返回内存标识

返回值和key都是标识内存的,那么有什么区别呢???
操作系统中存在着许多的共享内存,肯定不能是杂乱无章的,一定需要进行管理,方法还是:先描述,再组织;所以共享内存其实是由两部分组成的:物理内存块和共享内存的相关属性也就是结构体struct shm;在创建共享内存时,为了保证唯一性使用key标识,也就是将其写入结构体中,当其他进程便可根据这一标识找到共享内存;总之,key标识内存时内核级别的,返回值标识内存是用户级的,都是标识,只是面对的对象不同罢了

进程间通信_第14张图片

代码实现创建共享内存

key_t getKey()
{
    key_t k=ftok(PATHNAME,PROJ_ID);
    if(k<0)
    {
        cerr<<errno<<" "<<strerror(errno)<<endl;
        exit(1);
    }
    return k;
}

int getShmhelper(key_t k,int flags)
{
    int shmid=shmget(k,MAX_SIZE,flags);
    if(shmid<0)
    {
        cerr<<errno<<" "<<strerrno(errno)<<endl;
        exit(2);
    }
    return shmid;
}

int getShm(key_t k)
{
    return getShmhelper(k,IPC_CREAT);
}

int creatShm(key_t k)
{
    return getShmhelper(k,IPC_CREAT|IPC_EXCL|0600);
}

查看生成的共享内存 指令ipcs -m

进程间通信_第15张图片

一般而言,当进程执行完毕比那会退出,生命周期也就结束;这里很奇怪,当第二次生成共享内存时,进程报错,并且报错原因还是文件已经存在,由此可知:共享内存的生命周期是随操作系统的,不是随进程的

这里就引入了一个函数来结束共享内存的生命
int shmctl(int shmid, int cmd, struct shmid_ds *buf)

  1. shmid:标识共享内存
  2. cmd:也就宏,这里只介绍一个选项IPC_RMID将共享内存杀掉
  3. 共享内存的内核结构体
    进程间通信_第16张图片

包含共享内存的许多属性,包括标识共享内存的
key也在其中

代码实现 shmctl

void delShm(int shmid)
{
    if(shmctl(shmid,IPC_RMID,nullptr)==-1)
    {
        cerr<<errno<<" "<<strerror(errno)<<endl;
    }
}

进程间通信_第17张图片

共享内存在进程结束之后便也被删除了

共享内存创建之后,接下来就是将进程与共享内存建立联系
void *shmat(int shmid, const void *shmaddr, int shmflg);

  1. shmid:标识共享内存
  2. shmaddr:指定共享内存通过页表映射到进程空间的某一区域,一般设置为空
  3. shmflg:设置进程的读写权限,一般设置为空
  4. 返回值:共享内存通过页表映射到进程空间某一区域的起始地址
void*attachShm(int shmid)
{
    void*mem=shmat(shmid,nullptr,0);
    if((long long)mem==-1L)
    {
        cerr<<"shmat: "<<errno<<" "<<strerror(errno)<<endl;
        exit(3);
    }
    
    return mem;
}

进程与共享内存建立联系之后,可以进行查看

进程间通信_第18张图片

当前存在一个进程与共享内存建立联系

建立联系之后,紧接着就是进行通信
代码实现两进程实现通信
client.cpp

int main()
{
    key_t k=getKey();
    printf("key: %x\n",k);
    //获取共享内存
    int shmid=getShm(k);
    printf("shmid: %d\n",shmid);
    //建立联系
    char*start=(char*)attachShm(shmid);
    printf("attach success,address start: %p\n",start);

    const char*message="hello server,我是另一个进程,咱俩正在通信";
    pid_t id=getpid();
    int cnt=0;
    //通信
    while(true)
    {
        snprintf(start,MAX_SIZE,"%s[pid:%d][消息编号:%d]",message,id,cnt++);
    }
    //去关联
    detachShm(start);
    return 0;
}

server.cpp

int main()
{
    key_t k=getKey();
    printf("key:%x\n",k);
    //创建共享内存
    int shmid=creatShm(k);
    printf("shmid: %d\n",shmid);
    //建立联系
    char*start=(char*)attachShm(shmid);
    printf("attach success,address start: %p\n",start);
    //通信
    while(true)
    {
        printf("client say: %s\n",start);
    }
    //去关联
    detachShm(start);
    //删除共享内存
    delShm(shmid);
    return 0;
}

完成通信之后,先将两进程去关联,再删除共享内存

int shmdt(const void *shmaddr);

  1. shmaddr是共享内存映射到进程空间地址的起始地址
  2. 返回值:失败返回-1
void detachShm(void* start)
{
    if(shmdt(start)==-1)
    {
        cerr<<"shmdt: "<<errno<<" "<<strerror(errno)<<endl;
    }
}

总结

共享内存的优点:
在所有进程间通信中,速度是最快的,能够大大地减少拷贝次数

对比:
管道
进程间通信_第19张图片

共享内存

进程间通信_第20张图片

共享内存的缺点:对数据没有包含操作

你可能感兴趣的:(Linux,unix,linux,网络)