【Linux】进程间通信——命名管道和共享内存

目录

[命名管道(named pipe)](#命名管道(named pipe))

命令行中使用

代码中使用

[共享内存(shared memory)](#共享内存(shared memory))

shmget

ipcs命令

shmctl

shmat/shmdt

简单通信


命名管道(named pipe)

之前我们说了匿名管道,但是匿名管道有一个特点就是只能具有血缘关系的进程间可以进行通信,那如果我想让两个毫无关系的进程进行通信,该怎么办呢?

还是不要忘记进程间通信的本质,就是让两个进程看到同一份资源,如果两个进程毫无关系,那么我们可以让它们看到某个路径下的一个文件,这样它们就看到同一份资源了。并且,因为这类文件是唯一路径下的唯一文件,它是可以确定的,所以叫命名文件,创建的管道也就叫命名管道
不同的进程以不同的方式打开同一个文件,那么这两个进程的文件描述符表中的指针指向不同的文件结构体对象,但是不同的文件结构体对象指向的文件的缓冲区是一样的,所以,两个进程可以看到相同的内容。

命令行中使用

我们可以首先在命令行中用一下管道文件,先查一下手册

man mkfifo

这个命令的基本使用就是mkfifo + 文件名

我们也可以看到这个文件的类型是p,表示pipe,管道文件

创建完管道文件后就可以用两个进程 (命令就是进程),一个往管道文件中写,一个往管道文件中读(可以echo向文件中写,cat从文件中读)

现象就是一个往管道文件中写,如果没人读,那么进程就会阻塞等待;同理,一个往管道文件中读,如果没人写,那么进程也会阻塞等待。

代码中使用

上面是在命令行上进行的操作,如果我们要用代码来操作就需要用相关的接口

man 3 mkfifo

第一个参数就是路径加文件名,第二个参数就是要创建的文件的权限
其实我们的任务就是创建一个管道文件 ,然后用文件操作(open、close、read、write)的方式向管道文件中写和读

我们可以创建一个服务器端和一个客户端,把客户输入的内容通过命名管道传给服务器,创建下面几个文件:

Comm.hpp中放共同的用得到的代码,PipeClient.cc放客户端的代码,PipeServer.cc放服务器端的代码,我们就来非常简单的模拟实现一下

基本代码如下,代码的效果就是先开服务器端,再开客户端,客户端写什么,服务器端都可以收到

cpp 复制代码
//Comm.hpp
#pragma once
#include<iostream>
#include<string>
#include<cstring>
#include<cerrno>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
using namespace std;

#define PATH "./fifo"
#define Mode 0666
class Fifo
{
    public:
    Fifo(const char*path)
    :_path(path)
    {
        umask(0);
        int n=mkfifo(_path.c_str(),Mode);
        if(n==0)
        {
            cout<<"touch fifo success"<<endl;
        }
        else
        {
            cout<<"fail to fifo errno is "<<errno<<" strerror is "<<strerror(errno)<<endl;
        }
    }
    ~Fifo()
    {
        int n=unlink(PATH);
        if(n==0)
        {
            cout<<"fifo remove"<<endl;
        }
        else
        {
            cout<<"fail to remove fifo errno is "<<errno<<" strerror is "<<strerror(errno)<<endl;
        }
    }
    private:
    string _path;
};
//PipeServe.cc
#include"Comm.hpp"

int main()
{
    Fifo fifo(PATH);
    int rfd=open(PATH,O_RDONLY);//如果要是读先open,没人写,就要阻塞等待在这
    if(rfd<0)
    {
            cout<<"fail to read open fifo errno is "<<errno<<" strerror is "<<strerror(errno)<<endl;
            return 1;
    }
    cout<<"read open success"<<endl;
    char buffer[1024]={0};
    while(1)
    {
        ssize_t n=read(rfd,buffer,sizeof(buffer)-1);
        if(n>0)
        {
            buffer[n]=0;
            cout<<"client say : "<<buffer<<endl;
        }
        else if(n==0)
        {
            cout<<"client quit,me too"<<endl;
            break;
        }
        else 
        {
            cout<<"fail to read errno is "<<errno<<" strerror is "<<strerror(errno)<<endl;
            break;
        }
        
    }
close(rfd);
    return 0;
}
//PipeClient.cc
#include"Comm.hpp"

int main()
{
    int wfd=open(PATH,O_WRONLY);
    if(wfd<0)
    {
            cout<<"fail to write open fifo errno is "<<errno<<" strerror is "<<strerror(errno)<<endl;
            return 1;
    }
    string inbuffer;
    while(1)
    {
        cout<<"please input your message"<<endl;
        getline(cin,inbuffer);
        if(inbuffer=="quit")break;
        int n=write(wfd,inbuffer.c_str(),inbuffer.size());
        if(n<0)
        {
            cout<<"fail to write errno is "<<errno<<" strerror is "<<strerror(errno)<<endl;
            break;
        }
    }
     close(wfd);
    return 0;
}

共享内存(shared memory)

之前说的无论是匿名管道还是命名管道都是管道,它们都是基于文件实现的,下面我们要说的是system V版本下的进程间通信的方式:共享内存,消息队列,信号量

下面我们先说什么是共享内存
在谈论进程间通信时永远不要忘掉进程间通信的本质:让不同的进程看到同一份资源。

所以,共享内存就是在物理内存中开辟一段空间,然后将这块空间通过页表映射到各个进程的地址空间(虚拟内存)的共享区中,这样,不同的进程就可以看到同样的一份资源了。
有了上面的理论基础我们就知道了我们需要学习什么样的系统调用,有申请共享内存的,有把共享内存进行挂接到地址空间的,有去挂接的,也有删除共享内存的。当然这只是我们粗略的去描述,具体有啥我们下面来看:

shmget

首先要介绍的就是申请共享内存的系统调用

man shmget

它如果成功了就返回共享内存的一个标识符,这个跟下面的key不同,这个标识符是给人看的,是我们通过这个标识符唯一确定一个共享内存

它有三个参数,第一个 是在内核中唯一标识一个共享内存的key值,就是内核通过这个值唯一确定一个共享内存,因为内核中存在多个共享内存这是正常的。具体说这个值是怎么创建的呢,当然我们可以自己给一个值,但是我们给的值容易过于简单,容易重复,所以就提供了一个接口

man ftok

这个函数有两个参数,其实我们可以随便给一个路径,一个proj_id,只要它们是唯一的即可,这样就可以生成唯一的key值,不同的进程间只要传入相同的路径和proj_id,它们就能获得相同的key值,找到同一块共享内存。

第二个参数就是你要创建的共享内存的大小,单位是字节

第三个参数是一些选项,你可以设置,基本的选项有:

IPC_CREAT:共享内存不存在就创建,如果存在就直接获取

IPC_EXCL:不能单独使用,没意义

IPC_CREAT | IPC_EXCL:共享内存不存在就创建,存在就出错返回

但是在这些选项后面还有按位或上权限,一般给0666

我们简单的写一个使用它们的代码

cpp 复制代码
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include<cstring>
#include<cerrno>
using namespace std;
#define PATH "/home/user100"
#define proj_id 111
#define SIZE 4096
int main()
{
    key_t key=ftok(PATH,proj_id);
    if(key<0)
    {
        cout<<"ftok fail errno is "<<errno<<" error string is "<<strerror(errno)<<endl;
        return 1;
    }
    int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);
    if(shmid<0)
    {
        cout<<"shmid fail errno is "<<errno<<" error string is "<<strerror(errno)<<endl;
        return 2;
    }
    cout<<"make shm success"<<endl;
    return 0;
}

然后就运行,发现,第一次运行成功,第二次运行失败

因为共享内存在程序结束后并不会删除,也就是说,共享内存的生命周期随内核,我们关掉xShell再开启也不会消失,因为云服务器是一直运行着的,除非我们重启系统共享内存才会消失

ipcs命令

我们可以通过命令和代码删除共享内存

图中的perm其实就是我们代码中设置的权限(permission)

nattch是这个共享内存挂接(attach)到了几个进程中

并且我们要说明的是内核中共享内存的大小是以4kb为基本单位的,也就是说如果我们给4097个字节,它也是会申请8kb的大小的,只不过会显示4097个字节

ipcs可以查看目前都有哪些消息队列,共享内存和信号量,shmid为1的共享内存就是我们刚才创建的,添加不同的选项可以分别查看

-q:显示消息队列的信息。(massage queue)

-m:显示共享内存的信息。(shared memory)

-s:显示信号量的信息。(semaphore array)

ipcrm -m shmid 就是删除共享内存,要是删消息队列和信号量也是以此类推

shmctl

下面就是用代码删除了,我们需要用到下面的接口

man shmctl

这个其实是对共享内存进行控制,当然了,控制也包括删除

第一个参数就是shmid,就是shmget的返回值

第二个参数是一些选项,不过我们删除要用到的选项是IPC_RMID

第三个参数是你传一个这个类型的结构体指针过去,然后它把共享内存的信息给你传回来,我们一般给个nullptr就行了

我们简单的用一下就行了

cpp 复制代码
 int ret=shmctl(shmid,IPC_RMID,nullptr);
    if(ret<0)
    {
        cout<<"remove shm fail errno is "<<errno<<" error string is "<<strerror(errno)<<endl;
        return 3;
    }
    cout<<"remove shm success,shmid is "<<shmid<<endl;

shmat/shmdt

上面我们讲了共享内存的创建和删除,下面我们就要说它该如何挂接和去挂接到进程的地址空间中呢?

man shmat(attach)挂接

man shmdt(detach)去关联

shmat第二个参数是一个地址,就是你想挂接到地址空间的哪里,但是我们一般给nullptr,就让OS随便找合适的空间即可

第三个仍然是一些选项,我们一般给0即可

返回值就是,挂接到的地址空间的首地址

shmdt参数就是shmat的返回值

我们简单来用一下

cpp 复制代码
void *addr = shmat(shmid, nullptr, 0);
    if ((long long)addr == -1)
    {
        cout << "shmat fail errno is " << errno << " error string is " << strerror(errno) << endl;
    }
    cout << "shmat success" << endl;
    sleep(5);
    int ret1 = shmdt(addr);
    if (ret1 < 0)
    {
        cout << "shmdt fail errno is " << errno << " error string is " << strerror(errno) << endl;
    }
    cout << "shmdt success" << endl;
    sleep(2);

简单通信

上面是一些基础的使用,下面我们就像命名管道写代码完成两个进程间简单的通信,我们还是创建那么几个文件

我们不得不说,共享内存是最快的进程间通信的方式,因为一个进程只需要把数据写入到它的地址空间中(其实就是写到了物理内存中),另一个进程就可以看到,这是它的优点,但是共享内存是不提供进程间协同的机制的,就是你写你的我读我的,这就会导致你可能还没写完我就读了,这就导致信息丢失 。但是我们知道管道是提供的,于是呢,我们可以利用管道的特点(写端不写,读端阻塞等待)实现共享内存的同步机制,基本代码如下:

cpp 复制代码
//server.cc
#include "Comm.hpp"
#include "pipe.hpp"
int main()
{
    key_t key = Getkey();//获取key
    int shmid = ServerGetshm(key);//创建共享内存
    void *addr = Attach(shmid);//挂接
    int *address = (int *)addr;
    Fifo fifo;//创建管道
    int rfd = Serveropenfifo();//以读方式打开文件
    for (int i = 0; i < 10; i++)
    {
        wait(rfd);//写端不写,就阻塞在这里
        int *tmp = address;
        cout << "Server get ";
        while (*(tmp) != 0)
        {
            cout << *(tmp);
            tmp++;
        }
        cout << endl;
    }
    Detach(addr);//去挂接
    removeshm(shmid);//删除共享内存
    return 0;
}
//client.cc
#include "Comm.hpp"
#include"pipe.hpp"
int main()
{
    key_t key = Getkey();//获取key
    int shmid = ClientGetshm(key);//获取共享内存
    void *addr = Attach(shmid);//挂接
    int *address = (int *)addr;
   int wfd= clientopenfifo();//以写方式打开文件
    for (int i = 0; i < 10; i++)
    {
        *(address + i) = i + 1;
        wakeup(wfd);//写完了一部分完整的数据就通知唤醒读端
        sleep(1);
    }
    Detach(addr);//去挂接
    return 0;
}
//comm.hpp
#pragma once
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <cstring>
#include <unistd.h>
#include <cerrno>
using namespace std;
#define PATH "/home/user100"
#define proj_id 111
#define SIZE 4096

key_t Getkey()
{
    key_t key = ftok(PATH, proj_id);
    if (key < 0)
    {
        cout << "ftok fail errno is " << errno << " error string is " << strerror(errno) << endl;
        return -1;
    }
    return key;
}

int ServerGetshm(key_t key)
{
    int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid < 0)
    {
        cout << "Server Get shm fail errno is " << errno << " error string is " << strerror(errno) << endl;
        return -1;
    }
    cout << "make shm success" << endl;
    return shmid;
}
int ClientGetshm(key_t key)
{
    int shmid = shmget(key, SIZE, IPC_CREAT | 0666);
    if (shmid > 0)
        cout << "Client Get shm success" << endl;
    return shmid;
}

void *Attach(int shmid)
{
    void *addr = shmat(shmid, nullptr, 0);
    if ((long long)addr == -1)
    {
        cout << "shmat fail errno is " << errno << " error string is " << strerror(errno) << endl;
        return nullptr;
    }
    cout << "shmat success" << endl;
    return addr;
}

int Detach(void *addr)
{
    int ret1 = shmdt(addr);
    if (ret1 < 0)
    {
        cout << "shmdt fail errno is " << errno << " error string is " << strerror(errno) << endl;
        return -1;
    }
    cout << "shmdt success" << endl;
    return 0;
}

int removeshm(int shmid)
{
    int ret = shmctl(shmid, IPC_RMID, nullptr);
    if (ret < 0)
    {
        cout << "remove shm fail errno is " << errno << " error string is " << strerror(errno) << endl;
        return -1;
    }
    cout << "remove shm success,shmid is " << shmid << endl;
    return 0;
}
//pipe.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
using namespace std;

#define Pipepath "./fifo"
#define Mode 0666
class Fifo
{
public:
    Fifo(const char *path = Pipepath)
        : _path(path)
    {
        umask(0);
        int n = mkfifo(_path.c_str(), Mode);
        if (n == 0)
        {
            cout << "touch fifo success" << endl;
        }
        else
        {
            cout << "fail to fifo errno is " << errno << " strerror is " << strerror(errno) << endl;
        }
    }
    ~Fifo()
    {
        int n = unlink(Pipepath);
        if (n == 0)
        {
            cout << "fifo remove" << endl;
        }
        else
        {
            cout << "fail to remove fifo errno is " << errno << " strerror is " << strerror(errno) << endl;
        }
    }

private:
    string _path;
};

int Serveropenfifo()
{
    int rfd = open(Pipepath, O_RDONLY);
    if (rfd < 0)
    {
        cout << "fail to ropen fifo errno is " << errno << " strerror is " << strerror(errno) << endl;
        return 1;
    }
    return rfd;
}
int clientopenfifo()
{
    int wfd = open(Pipepath, O_WRONLY);
    if (wfd < 0)
    {
        cout << "fail to wopen fifo errno is " << errno << " strerror is " << strerror(errno) << endl;
        return 1;
    }
    return wfd;
}

void wakeup(int wfd)
{
    char ch='A';
    write(wfd,&ch,sizeof(ch));
}
void wait(int rfd)
{
    char buffer[10]={0};
    read(rfd,buffer,sizeof(buffer));
}
相关推荐
川石课堂软件测试10 分钟前
性能测试|docker容器下搭建JMeter+Grafana+Influxdb监控可视化平台
运维·javascript·深度学习·jmeter·docker·容器·grafana
龙哥说跨境18 分钟前
如何利用指纹浏览器爬虫绕过Cloudflare的防护?
服务器·网络·python·网络爬虫
pk_xz1234562 小时前
Shell 脚本中变量和字符串的入门介绍
linux·运维·服务器
小珑也要变强2 小时前
Linux之sed命令详解
linux·运维·服务器
海绵波波1072 小时前
Webserver(4.3)TCP通信实现
服务器·网络·tcp/ip
九河云4 小时前
AWS账号注册费用详解:新用户是否需要付费?
服务器·云计算·aws
Lary_Rock4 小时前
RK3576 LINUX RKNN SDK 测试
linux·运维·服务器
幺零九零零5 小时前
【计算机网络】TCP协议面试常考(一)
服务器·tcp/ip·计算机网络
云飞云共享云桌面6 小时前
8位机械工程师如何共享一台图形工作站算力?
linux·服务器·网络
Peter_chq6 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端