如何在Linux中实现进程间通信

致前行路上的人:

        要努力,但不要着急,繁花锦簇,硕果累累都需要过程!

目录

1.进程间通信介绍

1.1进程间通信的目的

1.2进程间通信发展

1.3进程间通信分类

1.4进程间通信的本质

2.管道

2.1什么是管道

2.2管道与进程的关系

2.3什么是匿名管道

2.4进程与管道通信的特点

 2.5实例代码

2.6进程间通信读写策略

2.7管道的特点

2.8命名管道

2.9实例代码:

3.system V共享内存

3.1共享内存的概念

3.2共享内存的原理

3.3申请共享内存的函数

3.4查看共享内存

 3.5控制共享内存的函数

3.6共享内存和进程关联的函数

3.7共享内存和进程去关联的函数

3.8实例代码

3.9共享内存的特点

4.system V消息队列

4.system V信号量 

5.IPC资源组织方式

1.进程间通信介绍

1.1进程间通信的目的

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

1.2进程间通信发展

管道——>基于文件系统
System V进程间通信——>聚焦在本地通信
POSIX进程间通信——>跨主机通信

1.3进程间通信分类

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

1.4进程间通信的本质

要通信的进程必须看到同一份公共资源,所以要求操作系统必须直接或间接的方式给进程提供“内存空间”,而不同进程间通信的分类是操作系统中不同模块提供的公共资源进行分类的,因此不论是哪一种通信,前提必须是要提供一份公共资源!

2.管道

2.1什么是管道

管道是一种进程间通信的形式,是基于文件系统给进程提供的一份公共资源。

2.2管道与进程的关系

磁盘上的文件被打开,创建struct file结构体对象,父进程struct files_struct中保存打开文件的文件描述符,然后让父进程指向该文件,fork创建子进程,因为进程间具有独立性,所以子进程继承父进程的进程管理结构也指向该文件,每个被打开的文件对象中都有一块自己的内核缓冲区,而这个内核缓冲区就是一份公共资源,一个进程在这块区域写入数据,一个进程在这块区域读数据,这样就实现了进程间的通信了!

 注:把实现进程间通信的这种文件就被称为管道文件

2.3什么是匿名管道

不同进程间通信的管道文件并不是磁盘上的文件,而是内存级文件,因为如果说是磁盘上的文件,每次进程间通信都需要进行访问外设,效率就会很慢,所以操作系统为了提高效率,就将该文件创建在了内存,而在内存上创建的该文件是通过文件描述符来进行寻找的,而不是通过文件名进行标识的,所以把这种创建在内存级的文件就被称为是匿名管道!

2.4进程与管道通信的特点

 2.5实例代码

创建管道文件:

 参数
pipefd:文件描述符数组,其中pipefd[0]表示读端, pipefd[1]表示写端
返回值:成功返回0,失败返回错误代码

1.子进程写入,父进程读取

#include<iostream>
#include<cassert>
#include<cstring>
#include<cstdio>
#include<sys/wait.h>
#include<sys/types.h>
#include<unistd.h>
using namespace std;
//父进程读取。子进程写入
int main()
{
    //第一步:创建文件,打开读写端
    int fd[2];
    int n = pipe(fd);
    assert(n == 0);
    //第二步:创建子进程
    pid_t id = fork();
    assert(id >= 0);
    //子进程:
    if(id == 0)
    {
        close(fd[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(fd[1],buffer,strlen(buffer));
            sleep(1);
        }
        _exit(0);
    }
    //父进程:
    close(fd[1]);
    //父进程通信代码:
    while(true)
    {
        char buffer[1024];
        //读取成功,返回读取字符的个数
        ssize_t s = read(fd[0],buffer,sizeof(buffer)-1);
        if(s > 0) buffer[s] = 0;
        cout << "# " << buffer << "|my pid:"<< getpid() <<endl;
    }
    int status = 0;
    n = waitpid(id,&status,0);
    assert(n == id);

    return 0;
}

2.6进程间通信读写策略

1.读慢,写快,缓冲区空间写满之后,写端不再写入

2.读块,写慢,读端读取数据之后,下一次读取为阻塞式等待,等待写端写入数据

3.向缓冲区写入一次数据之后,写端关闭,读端读取一次数据之后,下一次读取到0

4.读关闭,写端向缓冲区写入数据,操作系统会给写端发信号,终止写端

2.7管道的特点

1.管道的声明周期随进程,进程退出,管道释放!

2.只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创
建,然后该进程调用fork,此后父、子进程之间就可应用该管道!

3.管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道!

4.内核会对管道操作进行同步与互斥,是对共享资源进行保护的方案!

2.8命名管道

概念:

命名管道和匿名管道一样也是一种内存级的管道文件,向该管道文件中写入数据的时候不需要刷新到磁盘,不同的是命名管道是具有文件名的,在Linux操作系统中文件是具有唯一性的,可以通过路径加文件名的方式唯一标识,因此两个没有关系的进程可以通过文件名打开同一份文件,也是能够看到同一份资源,因此一个进程向该文件中写入数据,一个进程向该文件中读取数据,此时就完成了进程间通信了!

创建命名管道

命名管道可以从命令行上创建,命令行方法是使用下面这个命令:

mkfifo filename

命名管道也可以从程序里创建,相关函数有:

 创建成功返回0,失败返回-1

删除管道文件:

 删除成功返回0,失败返回-1

2.9实例代码:

server.cc从管道文件中读取,client.cc向管道文件中写入:

comm.hpp:

#pragma once

#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<string>
#include<sys/stat.h>
#include<cstring>
#include<cassert>
#include<errno.h>
#include<fcntl.h>
#include<stdlib.h>
using namespace std;
#define NAME_PIPE "mypipe"

//创建管道文件
bool createFifo(const string& path)
{
    umask(0);
    int n = mkfifo(path.c_str(),0666);
    if(n == 0)
    {
        return true;
    }
    else
    {
        cout << "errno" << errno << "error:" << strerror(errno) << endl;
        return false;
    }
}
void removeFifo(const string& path)
{
    int n = unlink(path.c_str());
    assert(n == 0);
}

sever.cc

#include"comm.hpp"

int main()
{
    bool ret = createFifo(NAME_PIPE);
    assert(ret);

    int rfd = open(NAME_PIPE,O_RDONLY);
    if(rfd == -1)   exit(-1);
    //read:
    char buffer[1024];
    while(true)
    {
        ssize_t s = read(rfd,buffer,sizeof(buffer)-1);
        if(s > 0)
        {
            cout << "client->sever#:" << buffer << endl;
        }
        else if(s == 0)
        {
            cout << "client quit!" << endl;
            break;
        }
        else
        {
            perror("read fail:");
        }
    }
    removeFifo(NAME_PIPE);
    return 0;
}

client.cc:

#include"comm.hpp"


int main()
{
    int wfd = open(NAME_PIPE,O_WRONLY);
    if(wfd == -1)   exit(-1);
    //write:
    char buffer[1024];
    while(true)
    {
        cout << "Please say:"<< endl;
        fgets(buffer,sizeof(buffer),stdin);
        buffer[strlen(buffer)-1] = 0;
        ssize_t n = write(wfd,buffer,strlen(buffer));
        assert(n == strlen(buffer));
    }
    return 0;
}

运行结果:

3.system V共享内存

3.1共享内存的概念

通过让不同的进程,看到同一个内存块,把这个内存块称为共享内存

3.2共享内存的原理

如图所示:

 不同的进程通过进程地址空间,再通过页表映射到内存的同一块区域

 共享内存是一种通信方式,所有想通信的进程,都可以调用对应的函数接口,申请一块相同的空间实现进程间通信

3.3申请共享内存的函数

shmget函数

功能:用来创建共享内存

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

参数
key:这个共享内存段名字

获取key:调用ftok函数


pathname:路径名  proj_id:一个整型

能够让不同的进程申请同一块内存空间,在底层内核数据结构中是通过key来实现的,key被shmget设置进入共享内存相关属性,用来标识该共享内存在内核中的唯一性

size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
 

IPC_CREAT:如果不存在,则进行创建,如果存在,则进行获取

IPC_EXCL:无法单独使用,使用的时候和IPC_CREAT一起使用,IPC_CREAT | IPC_EXCL,如果不存在就创建,如果存在则会报错

返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

注:共享内存也是操作系统申请的空间,所以就需要被管理,而管理的本质是先描述,再组织,所以一定存在结构体描述共享内存的相关属性,申请共享内存,除了申请空间外还包含保存共享内存的相关属性的字段

3.4查看共享内存

共享内存的属性:

 共享内存的属性是随OS的,不是随进程的:

指令删除共享内存:ipcrm -m + shmid

 3.5控制共享内存的函数

shmctl函数

控制共享内存的选项: 

 功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的相关属性的数据结构
 


返回值:成功返回0;失败返回-1

3.6共享内存和进程关联的函数

shmat函数

 功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个字节;失败返回-1

说明:

shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr -(shmaddr % SHMLBA) shmflg=SHM_RDONLY,表示连接操作用来只读共享内存

3.7共享内存和进程去关联的函数

shmdt函数

 功能:将共享内存段与当前进程脱离
原型
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段

3.8实例代码

//comm.hpp
#ifndef __COMM_HPP_
#define __COMM_HPP_

#include<iostream>
#include<cstring>
#include<cerrno>
#include<cstdlib>
#include<cstdio>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
using namespace std;
#define PATHNAME "."
#define PROJ_ID 0x66
#define SIZE 4096
key_t getKey()
{
    key_t key = ftok(PATHNAME,PROJ_ID);
    if(key == -1)
    {
         cerr << errno << ":" << strerror(errno) << endl;
         exit(1);
    }
    return key;
}
int getShmHelper(key_t key,int flags)
{
    int shmid = shmget(key,SIZE,flags);
    if(shmid == -1)
    {
        cerr << errno << ":" << strerror(errno) << endl;
        exit(2);
    }
    return shmid;
}
int createShm(key_t key)
{
    return getShmHelper(key,IPC_CREAT | IPC_EXCL | 0600); //0600 创建共享内存权限
}
int getShm(key_t key)
{
    return getShmHelper(key,IPC_CREAT);
}
void delShm(int shmid)
{
    int key = shmctl(shmid,IPC_RMID,nullptr);
    if(key == -1)
    {
        cerr << errno << ":" << strerror(errno) << endl;
        exit(3);
    }
}
void* attachShm(int shmid)
{
    void* mem = shmat(shmid,nullptr,0);
    if((long long)mem == -1L)
    {
        cerr << errno << ":" << strerror(errno) << endl;
        exit(4);
    }
    return mem;
}
void detachShm(void* start)
{
    if(shmdt(start) == -1)
    {
        cerr << errno << ":" << strerror(errno) << endl;
    }
}
#endif
//client.cc
#include"comm.hpp"

int main()
{
    key_t key = getKey();
    printf("0x%x\n",key);
    int shmid = getShm(key);
    printf("%d\n",shmid);
    char* start = (char*)attachShm(shmid);
    printf("attach sucess,address start:%p\n",start);
    const char*message = "hello server:我是另一个进程,正在给你发信息";
    pid_t id = getpid();
    int cnt = 1;
    char buffer[1024];
    while(true)
    {
        // snprintf(buffer,sizeof(buffer),"%s[pid:%d],%d",message,id,cnt++);
        // memcpy(start,buffer,strlen(buffer)+1);
        snprintf(start,SIZE,"%s[pid:%d],消息编号:%d",message,id,cnt++);
        sleep(1);
    }
    //去关联
    detachShm(start);
    
    return 0;
}
//server.cc
#include"comm.hpp"

int main()
{
    key_t key = getKey();
    printf("0x%x\n",key);
    int shmid = createShm(key);
    printf("%d\n",shmid);
    //关联成功:挂接成功
    char* start = (char*)attachShm(shmid);
    printf("attach sucess,address start:%p\n",start);
    //使用:
    while(true)
    {
        printf("client say:%s\n",start);
        sleep(1);
    }
    //去关联
    detachShm(start);
    //删除共享内存
    delShm(shmid);
    return 0;
}

效果演示:

 3.9共享内存的特点

优点:所有进程通信,速度是最快的,能大大减少拷贝的次数

关于共享内存数据拷贝的一道面试题:

题目:考虑共享内存,键盘输入,键盘输出,共享内存有几次数据拷贝?

共享内存的缺点:不会进行同步和互斥的操作,不对数据进行保护

 共享内存的大小:

共享内存的大小,一般建议是4kb的整数倍,系统分配共享内存是以4kb为单位的,4kb是内存划分内存块的基本单位,内核分配内存是采取向上取整的原则,申请小于4kb大小的内存,操作系统也会给你4kb,但是你只能使用自己申请的大小空间

4.system V消息队列

消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法

每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值特性方面
IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核

如图所示:

4.system V信号量 

信号量概念:

在了解什么是信号量之前我们来看看,进程要通信需要什么?根据上面管道和共享内存的学习,我们知道不同的进程想实现通信,两个进程必须看到同一份公共资源,一个进程在这个公共资源中写入数据,一个进程从公共资源中读取数据就能实现进程间通信,但是这种方式在实现进程间通信存在隐含的问题,就是不同的进程都能看到这份公共资源,这也就造成公共资源中的数据可以被任何进程修改,使得公共资源不安全的问题,所以为了解决这个问题就需要把公共资源保护起来,如何将这部分公共资源保护起来呢?操作系统实现了两种机制,就是同步互斥

互斥:是指一个进程访问公共资源的数据的时候,另一个进程不能够访问,这也就解决了一个进程正在向公共资源写数据的时候被另一个进程读取,造成数据不一致的问题。

注:将保护起来的公共资源被称为临界资源,使用公共资源的代码称之为临界区,不是使用公共资源的代码称为非临界区

了解上面这些之后,下面举一个电影院买票的例子来理解信号量,假设电影院有100个座位,所以卖票的时候最多只能卖100张票,为了避免电影院多卖票的问题,可以定义一个count变量保存电影院总共的票数,每次卖出一张票之后就将count--,当count==0的时候,就不会再卖票了,这种售票的机制联想到进程申请资源,可以将信号量类比为电影院卖票的个数count,当我们向内存申请公共资源的时候就将信号量--,释放资源的时候就将信号量++,所以所有进程在访问公共资源的时候,必须先申请公共资源,通过这样的方式就可以将公共资源保护起来,但是此时又引入了新的问题,就是信号量本身就是公共资源,那信号量该如何保护自己的安全呢?关于信号量如何保护自己的安全,介绍一个新的概念就是原子性。

什么是原子性?

所谓的原子性就是两中状态,要么做,要么不做。

而信号量就具有这种特性,所以不会有多个进程同时访问同一个信号量的问题,进而也就保护了信号量。

了解了上面这些之后就能得出一个结论,信号量本质是一个计数器,通常用来表示公共资源中,资源数量有多少的问题

注:上面说的预定资源信号量--,释放资源信号量++的两种操作被称为PV操作

当信号量为1的时候,将这个信号称为二元信号量,此时就具备互斥功能。

5.IPC资源组织方式

共享内存:

 消息队列: 

信号量:

这三种进程间通信的接口方式都非常类似,目的是操作系统在对这三种通信的方式公共资源管理的时候就可以通过一个结构体指针数组进行管理,数组的每一个下标存放每种通信方式公共资源属性的第一个字段的地址,然后进而维护和管理这三种通信方式申请的公共资源

猜你喜欢

转载自blog.csdn.net/qq_65307907/article/details/128749005
今日推荐