Linux进程通信 ---匿名/命名管道 --- 共享内存

文章目录

  • 通信的概念
  • 管道
    • 管道的系统调用
    • 匿名管道
  • 命名管道
  • SyStem V共享内存
    • 共享内存的接口
    • ftok --- 创建key值
    • shmget --- 创建共享内存
    • 命令行的操作:
    • shmat --- 将共享内存映射到进程地址空间
    • shmctl --- 控制共享内存 --- 获取属性/设置属性/删除共享内存
    • shmdt --- 取消进程与共享内存的关联
    • 操作示例
  • 总结

通信的概念

进程之间的数据传输,资源共享,发送通知,进程控制就属于进程间的通信

数据传输:

一个进程将其数据发送给另一个进程

资源共享:

多个进程之间共享同样的资源

通知事件:

一个进程向另一个进程发送消息也可以是向一组进程发送消息

进程控制:

一个进程控制另一个进程的执行

目前通信的主要标准分类为:

POSIX — 让通信可以跨主机

System V — 聚焦在本地通信

基于文件的通信方式为:管道

通信的本质:

因为进程具有独立性,所以通信并不是进程之间的直接交互,而是由操作系统直接或间接的给通信双方的进程提供内存空间,内存空间里的数据就是公共资源,而通信的前提就是需要通信的进程都必须看到同一份公共资源。不同的通信种类就是操作系统中不同的模板提供的。由上述即可得知,通信并不是低成本的工作。

管道

管道是基于文件系统的通信方式:从一个进程连接到另一个进程的数据流称为一个管道

每个文件都会有一个属于自己的内核缓冲区,这个区域就属于是进程之间的公共资源。因此进程间的通信就可通过这个内核缓冲区去实现。而这种基于文件系统的通信就是管道通信,这个文件就称为管道文件。管道文件时内存级的文件,不需要进行IO。一般而言管道只能用来进行单向数据通信

管道的特征:

  1. 管道的生命周期随进程,进程退出则管道释放
  2. 只要是具有血缘关系的进程就可以进行管道通信
  3. 管道是面向字节流的(网络)
  4. 单向通信
  5. 管道具有互斥与同步的机制

管道的系统调用

int pipe(int pipefd[2]); —

创建匿名管道

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

参数为输出型参数,需要将两个文件描述符(分别对应进程读和写)传入这个参数

创建成功后,数组保存两个文件描述符:pipfd[0] — 读;pipfd[1] — 写

Linux进程通信 ---匿名/命名管道 --- 共享内存_第1张图片

int mkfifo(const char *filename,mode_t mode); —

创建以权限p开头的命名管道文件

参数1为文件名(C类型的字符串),参数2为文件的权限

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

Linux进程通信 ---匿名/命名管道 --- 共享内存_第2张图片

int unlink(const char* path); —

删除管道文件

删除成功返回0.

image-20230703151936548

匿名管道

通过父进程创建子进程去继承文件地址的方式,使得两进程看到同一个内存级文件,此时的内存级文件没有名称就称为匿名管道

创建匿名管道的步骤:

父进程创建出管道,分别以读和写的方式打开文件(为了让子进程继承,方便后期选择对于文件的单向数据通信)

Linux进程通信 ---匿名/命名管道 --- 共享内存_第3张图片

父进程创建出子进程

Linux进程通信 ---匿名/命名管道 --- 共享内存_第4张图片命名管道

父进程关闭读,子进程关闭写(根据需求可自行关闭读/写),必须一个负责读一个负责写

Linux进程通信 ---匿名/命名管道 --- 共享内存_第5张图片

匿名管道的读写特征:

  1. 当写的速度比读的速度慢时,会导致管道中没有数据,默认会将读的进程阻塞,等待管道有数据
  2. 当读的速度比写的速度慢时,会导致管道空间写满,默认会将写的进程阻塞等待读端读取
  3. 当写关闭后,读端会将数据读完,可由用户自行设置读完后的操作
  4. 当读关闭后,写就失去了意义,操作系统会发信号终止写的进程
int main(){
    //1、创建管道文件,打开读写端
    int fds[2];
    int n = pipe(fds);
    assert(n == 0);

    //2、创建子进程
    pid_t id = fork();
    assert(id >= 0);

    //3、根据需求删除进程的读或写,确保单向传输
    //父进程读,子进程写
    if(id == 0){
        //子进程
        //子进程关闭读
        close(fds[0]);
        int count = 0;
        while(1){
            char buff[1024];
            //写入字符串
            snprintf(buff, sizeof(buff), "I am child -> parent: %d\n", ++count);
            write(fds[1], buff, strlen(buff));
            //每隔一秒写一次
            sleep(1);
        }

        exit(0);
    }

    //父进程
    //父进程关闭写
    close(fds[1]);

    //父进程读
    while(1){
        char buff[1024];
        //在管道文件中读取到父进程的buff
        ssize_t i = read(fds[0], buff, sizeof(buff) - 1);
        //将独到的字符串最后加上\0
        if(i > 0)
            buff[i] = 0;
        cout << "parent: " << buff << endl;
    }

    //等待
    n = waitpid(id, nullptr, 0);
    assert(id == n);

    return 0;
}

命名管道

创建一个以权限p开头的文件,进程可以往文件里写入也可以从文件里读取,实现通信

命名管道文件根据名称具有唯一性,进程可以通过路径 + 文件名 找到命名管道文件,从而实现通信

管道的读端在打开文件后不会继续往后运行,直至管道的写端也打开文件后才会继续运行

命名管道的通信需要两个不相干的进程来实现,如下代码示例

name_pipe.hpp

#pragma once

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

using namespace std;

#define NAME_PIPE "./MyPipe"//宏定义管道文件名字与路径

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

//删除管道文件
void RemoveFifo(const string& path){
    assert(unlink(path.c_str()) == 0);
}

Read.cc

#include"name_pipe.hpp"

using namespace std;

//读取
int main(){
    //创建文件
    //读端负责创建管道文件
    bool flags = CreatFilePipe(NAME_PIPE);
    assert(flags);

	//创建成功后,打开管道文件
    int fd = open(NAME_PIPE, O_RDONLY);
    if(fd < 0) exit(1);

    char buff[1024];
    while(1){
    	//读取管道文件中的数据
        ssize_t s = read(fd, buff, sizeof(buff) - 1);
        //读取成功
        if(s > 0){
            buff[s] = 0;
            std::cout << "Read: " << buff << endl;
        }
        //没有读到数据
        else if(s == 0){
            std::cout << "stop" << endl;
            break;
        }
        //读取出错
        else
            break;
    }

    close(fd);
	
	//删除管道文件
    RemoveFifo(NAME_PIPE);

    return 0;
}

Write.cc

#include"name_pipe.hpp"

using namespace std;

//写入
int main(){
	//因为读端已经有创建除了管道文件,写端只需要打开即可
    int fd = open(NAME_PIPE, O_WRONLY);
    if(fd < 0) exit(1);
    
    string str;
    while(1){
    	//往管道文件里写入数据
        getline(cin, str);
        assert(write(fd, str.c_str(), strlen(str.c_str())) == strlen(str.c_str()));
        
    }

    close(fd);

    return 0;
}

Linux进程通信 ---匿名/命名管道 --- 共享内存_第6张图片

当写端写入数据后,读端就会立即读取。命名管道与匿名管道最大区别就在于, 匿名管道是用于有血缘关系的进程之间,而命名管道适用于毫不相干的进程之间

SyStem V共享内存

操作系统会申请一块内存,然后将这块内存映射到对应进程的进程地址空间,这块内存就是共享内存。进程之间可以通过访问这块内存从而实现通信

步骤:

1、操作系统创建内存

2、将内存映射到进程地址空间

3、取消进程和内存的映射关系,释放内存

Linux进程通信 ---匿名/命名管道 --- 共享内存_第7张图片

共享内存是一种通信方式,所有需要通信的进程都可以使用,并且在操作系统中存在着大量的共享内存。也就是说通过让不同进程看到同一块内存的方式就叫做共享内存

一般共享内存的大小为4KB的整数倍

每一个共享内存都有一个key值标识唯一性,而每一个共享内存都由属于自己的id,进程间为了保证访问的是同个内存就必须通过key值去创建或者获取共享内存

共享内存 = 物理内存块 + 共享内存的相关属性

操作系统在创建共享内存时,同时会创建一个结构体对象(struct shm{})出来里面包含了所有共享内存的属性,而对共享内存的管理就变成了对结构体对象的管理。因为操作系统中有很多的共享内存,所以这些共享内存都会有对应的结构体对象,而操作系统会将这些对象组织起来统一管理。

每个共享内存结构体对象里都有唯一且不同的key值,这就能保证共享内存的唯一性,进程之间也就可以用过key值找到对应的共享内存。shmid也是共享内存的标识符,是基于用户层面的。

共享内存的生命周期是随着操作系统的,并不是跟随进程的,只要不关闭操作系统,即使进程退出,共享内存还是会存在的。除非使用接口将其删除

共享内存的特点:

所有的进程通信最快的方式,能大大的减少数据的拷贝次数。

没有同步互斥,不对数据进行保护

共享内存只有在当前映射连接数为0时才会被删除释放

共享内存的接口

ftok — 创建key值

为了能够使进程间能够访问的是同一块内存,则需要一个唯一的key值标识。也就是shmget接口的第一个参数。这个key值也需要使用接口创建出来

#include 
#include 

key_t ftok(const char *pathname, int proj_id);

参数是可以自定义的,只要进程是使用由相同的两个参数创建出来的key值去创建共享内存,那么它们访问的内存就一定是同一块内存。

相同的参数创建出来的key值是一样的

#define PATHNAME "."
#define PROJ_ID 22

//创建key值
key_t getKey(){
    key_t keynum = ftok(PATHNAME, PROJ_ID);
    //key值小于0则创建失败
    if(keynum < 0){
        //将错误打印到标准错误
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(1);
    }

    return keynum;
}

shmget — 创建共享内存

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

参数一为保证进程访问的是同一块内存的唯一性的标识。

参数二为空间大小

参数三为创建内存的选项,IPC_CREAT(如果创建的内存不存在则创建,如果存在则获取已存在的内存)、IPC_EXCL(无法单独使用,需要搭配IPC_CREAT使用。代表如果不存在则创建,存在则报错返回。意义代表着创建出来的内存一定是新的内存)

返回值:成功创建会返回内存的标识符,不兼容文件的标识符。失败则返回-1。

//因为shmget是有不同的选项的,因此可以分为只创建和获取不存在则创建两种
//不同类型创建共享内存
int GetOrCreat(key_t keynum, int flags){
    int shmid = shmget(keynum, 1024, flags);
    if(shmid < 0){
        //将错误打印到标准错误
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(2);
    }

    return shmid;
}

//只创建不获取,存在则报错
//因为共享内存也是文件,因此创建时要加上文件的权限
int CreatrSHM(key_t keynum){
    return GetOrCreat(keynum, IPC_CREAT | IPC_EXCL | 0666);
}

//创建获取共享内存,不存在则创建
int GetSHM(key_t keynum){
    return GetOrCreat(keynum, IPC_CREAT);
}

命令行的操作:

ipcs -m — 查看共享内存

ipcrm -m XXX(共享内存的id) — 删除共享内存

shmat — 将共享内存映射到进程地址空间

进程间想要访问共享内存就必须把共享内存映射到自己的地址空间中。

#include 
#include 

void *shmat(int shmid, const void *shmaddr, int shmflg);

int shmdt(const void *shmaddr);

参数一 共享内存的id

参数二 进程地址空间的指定区域,因为这个不关心所以可以设为空

参数三 映射的选项,默认为0即可

失败返回-1

//映射共享内存
void* AttchSHM(int shmid){
    void* mem = shmat(shmid, nullptr, 0);
    if((long long)mem == (long long)-1){
        //将错误打印到标准错误
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(4);
    }

    return mem;
}

shmctl — 控制共享内存 — 获取属性/设置属性/删除共享内存

一般而言这个接口常用于删除共享内存

#include 
#include 

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数一为 共享内存的id

参数二为 控制的选项 — IPC_STAT(获取属性)、IPC_SET(设置属性)、IPC_RMID(删除)

参数三为 放置属性容器,删除共享内存时设为nullptr

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

//删除共享内存
void DeleteSHM(int shmid){
    if(shmctl(shmid, IPC_RMID, nullptr) == -1){
        //将错误打印到标准错误
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(3);
    }
}

shmdt — 取消进程与共享内存的关联

#include 
#include 

void *shmat(int shmid, const void *shmaddr, int shmflg);

int shmdt(const void *shmaddr);

参数和shmat一样

//取消关联
void DeleteProcSHM(void* start){
    if(shmdt(start) == -1){
         //将错误打印到标准错误
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(5);
    }
}

操作示例

comm.hpp

#ifndef _COMM_HPP_
#define _COMM_HPP_

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

#define PATHNAME "."
#define PROJ_ID 22

//创建key值
key_t getKey(){
    key_t keynum = ftok(PATHNAME, PROJ_ID);
    if(keynum < 0){
        //将错误打印到标准错误
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(1);
    }

    return keynum;
}

//不同类型创建共享内存
int GetOrCreat(key_t keynum, int flags){
    int shmid = shmget(keynum, 1024, flags);
    if(shmid < 0){
        //将错误打印到标准错误
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(2);
    }

    return shmid;
}

//只创建不获取,存在则报错
int CreatrSHM(key_t keynum){
    return GetOrCreat(keynum, IPC_CREAT | IPC_EXCL | 0666);
}

//创建获取共享内存,不存在则创建
int GetSHM(key_t keynum){
    return GetOrCreat(keynum, IPC_CREAT);
}

//删除共享内存
void DeleteSHM(int shmid){
    if(shmctl(shmid, IPC_RMID, nullptr) == -1){
        //将错误打印到标准错误
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(3);
    }
}

//映射共享内存
void* AttchSHM(int shmid){
    void* mem = shmat(shmid, nullptr, 0);
    if((long long)mem == (long long)-1){
        //将错误打印到标准错误
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(4);
    }

    return mem;
}

//取消关联
void DeleteProcSHM(void* start){
    if(shmdt(start) == -1){
         //将错误打印到标准错误
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(5);
    }
}

#endif

Rshm.cc

#include"comm.hpp"

int main(){
    key_t keynum = getKey();

    int shmid = GetSHM(keynum);
    std::cout << shmid << std::endl;

    char* mem = (char*)AttchSHM(shmid);
    while(1){
        printf("好的收到:%s\n", mem);
        sleep(1);
    }

    DeleteProcSHM(mem);

    DeleteSHM(shmid);

    return 0;
}

Wshm.cc

#include"comm.hpp"

int main(){
    key_t keynum = getKey();

    int shmid = CreatrSHM(keynum);
    std::cout << shmid << std::endl;

    char* mem = (char*)AttchSHM(shmid);
    int cnt = 1;
    while(1){
        snprintf(mem, 1024, "发送数据了,请接收: %d", cnt++);
        sleep(1);
    }

    DeleteProcSHM(mem);
    return 0;
}

需要注意运行的先后顺序,必须要先运行只创建内存的进程

总结

进程间通信的最主要步骤就是让进程们能够访问的到同一块空间,不同的通信方式的本质就是访问的空间不同

你可能感兴趣的:(Linux,linux,服务器,数据库)