【1++的Linux】之进程间通信(共享内存)

👍作者主页:进击的1++

🤩 专栏链接:【1++的Linux】

我们在前面的文章中提到过,进程间的通信本质都是先看到同一块资源,然后通过这同一块资源进行通信,并且是单向的通信,只能一端发,一端进行读,共享内存也是基于这样的原理而进行的通信,与管道有异曲同工之处,管道是基于文件,拿到同一个文件的文件描述符而进行的通信,需要调用对文件的读写操作函数,因此要经过内核。而共享内存是不会的,其是内存级的通信,因此它的效率非常高。

ps:共享内存因为数据不需要在客户机和服务器端之间复制,数据直接写到内存,不用若干次数据拷贝,所以这是最快的一种IPC。

那么是什么是共享内存呢?

我们其实早已经和它见过面了!!!

我们的共享内存就在共享区中。

我们通过OS申请一块物理内存,作为共享内存,进程间是独立的,有自己的页表,进程地址空间。两进程通过页表将这块物理内存映射到各自的进程地址空间,此时他们就能够看到同一块资源啦!

共享内存的提供者是OS,共享内存不止有一块,此时就需要将他们进行管理,怎么管理呢?六字真言:先描述,后组织!!!

那么共享内存实质是什么呢?----共享内存块+对应的内核数据结构。

共享内存的建立:

shmget()函数

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

它的参数都是指什么呢?

key : 通信的双方要保证看到的是同一块共享内存,,它是几不重要,只要它在系统里唯一就行。相同的key就可以看到同一块空间了。

size : 共享内存的大小

shmflg :标志位 IPC_CREAT 创建共享内存:若存在则获取,不存在则创建 IPC_CREAT | IPC_EXCL 创建共享内存:若存在,报错返回,若不存在,创建。

IPC_EXCL单独使用没有意义。

返回值是一个整数 ,其类似于文件描述符一样,是共享内存用户层的标识符。

那么key 值怎么获取,才能使系统中唯一的呢?

key_t ftok(const char *pathname, int proj_id) 我们调用ftok函数去生成一个唯一的key。
pathname:指定的文件,此文件必须存在且可存取
proj_id:计划代号(project ID)

ftok的典型实现是调用stat函数,然后组合以下三个值:

① pathname所在的文件系统的信息(stat结构的st_dev成员)。

② 该文件在本文件系统内的索引节点号(stat结构的st_ino成员)。

③ proj_id的低序8位(不能为0)。

上述三个值的组合产生一个32位键。

创建出共享内存后,我们需要将其挂接到我们需要进行通信的地址空间上,我们用shmat()来实现。

void *shmat(int shm_id, const void *shm_addr, int shmflg);

第一个参数,shm_id是由shmget()函数返回的共享内存标识。

第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。

第三个参数,shm_flg是一组标志位,通常为0。

调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回

挂接完成后我们就可以进行通信了。

若向结束通信,我们可以选择让进程和共享内存分离

我们使用shmdt() 函数实现。

int shmdt(const void *shmaddr);

参数shmaddr是shmat()函数返回的地址指针,调用成功时返回0,失败时返回-1.

该函数只是让进程和其分离,并不会删除共享内存。

删除共享内存我们用shmctl()函数。

int shmctl(int shm_id, int command, struct shmid_ds *buf);

第一个参数,shm_id是shmget()函数返回的共享内存标识符。

第二个参数,command是要采取的操作,它可以取下面的三个值 :

IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。

IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值 IPC_RMID:删除共享内存段

第三个参数,buf是一个结构指针,它指向共享内存模式和访问权限的结构。

出来使用函数删除共享内存,我们还可以用命令手动去删除:

ipcs -m 用来查询存在的共享内存

ipcrm -m+shmid 可以删除对应的共享内存

共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取。所以我们通常需要用其他的机制来同步对共享内存的访问

下面我们利用管道来实现一个同步的共享内存:

cpp 复制代码
#pragma once
#include<iostream>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<cassert>
#include<unistd.h>
#include<cstring>
#include<sys/stat.h>
#include<fcntl.h>
#include"LOG.hpp"
#define IPC_PATH "/home/hyp"
#define PROJ_id 0x66  //8位
#define SHM_SIZE 4096 //最好是页的整数倍
#define Fifo_Name "./fifo"

class Init
{
    public:
    Init()
    {
        umask(0);
         int n=mkfifo(Fifo_Name,0666);//创建管道
         if(n==-1)
         {
            perror("mkfifo");
            exit(1);
         }
    }

    ~Init()
    {
        unlink(Fifo_Name);
    }


};


int OPen_fifo(std::string fifo_name,std::string flag)
{
    int fd=-1;
    if(flag=="READ")
    {
        fd=open(fifo_name.c_str(),O_RDONLY);

    }
    else if(flag=="WRITE")
    {
        fd=open(fifo_name.c_str(),O_WRONLY);
    }
    else{
        exit(2);
    }
    assert(fd!=-1);
    return fd;
}
void Sendmessage(int fd)
{
    int commd=0;
    int s=write(fd,&commd,sizeof commd);
    log("发送中.....",DEBUG)<<std::endl;
    assert(s!=-1);
    
}

void Wait(int fd)
{
    log("等待中.....",DEBUG)<<std::endl;
    int commd=1;
    int s=read(fd,&commd,sizeof(commd));
    assert(s!=-1);
}

//日志打印

cpp 复制代码
#pragma once
#include<iostream>
#include<string>
#ifndef _LOG_H_
#define _LOG_H_

#define DEBUG 0
#define NOTICE 1
#define WARNING 2
#define ERROR 3

std::string mes[4]={
    "DEBUG","NOTICE","WARNING","ERROR"
};

std::ostream& log(const std::string& message,int level)
{
   std::cout<<"|"<<mes[level]<<"|"<<message<<"|";
   return std::cout;
}

#endif

//发送端

cpp 复制代码
#include"comm.hpp"
#include"LOG.hpp"

int main()
{
    key_t k=ftok(IPC_PATH,PROJ_id);
    if(k==-1)
    {
        perror("ftok");
        exit(1);
    }
    log("creat key done",DEBUG)<<" client# k:"<<k<<std::endl;

    int shmid=shmget(k,SHM_SIZE,0);
    assert(shmid>0);
    log("creat shm done",DEBUG)<<std::endl;
    //将创建出的共享内存挂接到自己的地址空间中
    char* shmaddr=(char*)shmat(shmid,nullptr,0);
    if(shmaddr==nullptr)
    {
        perror("shmat");
        exit(1);
    }
    log("attach shm success",DEBUG)<<std::endl;

    //使用
   int fd= OPen_fifo(Fifo_Name,"WRITE");
    while(true)
    {
        //sleep(1);
        //char puts[1024];
        //fgets(shmaddr,10,stdin);
        //Sendmessage(shmaddr);
        Sendmessage(fd);
        int s=read(0,shmaddr,SHM_SIZE-1);
        if(s>0)
        {
            shmaddr[s-1]='\0';
           if(strcmp(shmaddr,"quit")==0)
            {
                break;
            }
        }

    }
    close(fd);

     int n=shmdt(shmaddr);
    if(n==-1)
    {
        perror("shmdt");
        exit(1);
    }
    log("detach shm success",DEBUG)<<std::endl;


    return 0;
}

//收端

cpp 复制代码
#include"comm.hpp"
#include"LOG.hpp"

Init init;
int main()
{
    key_t k=ftok(IPC_PATH,PROJ_id);
    if(k==-1)
    {
        perror("ftok");
        exit(1);
    }
    log("creat key done",DEBUG)<<" server# k:"<<k<<std::endl;

    int shmid=shmget(k,SHM_SIZE,IPC_CREAT|IPC_EXCL|0666);//通信的发起者
    assert(shmid>0);
    log("creat shm done",DEBUG)<<std::endl;
    //将创建出的共享内存挂接到自己的地址空间中
    char* shmaddr=(char*)shmat(shmid,nullptr,0);
    if(shmaddr==nullptr)
    {
        perror("shmat");
        exit(1);
    }
    log("attach shm success",DEBUG)<<std::endl;
    //使用

    int fd=OPen_fifo(Fifo_Name,"READ");
    while(true)
    {
        Wait(fd);
        printf("%s\n",shmaddr);
        if(strcmp(shmaddr,"quit")==0) break;
       
    }

    int x=shmctl(shmid,IPC_RMID,0);
    assert(x!=-1);
    log("delete shm success",DEBUG)<<std::endl;

    

    return 0;
}

为了让进程之间能够通信,我们让其能够看到同一份资源,但看到同一份资源也会带来一些时序问题,从而造成数据不一致的问题。

我们上述的代码中是用加入管道的方式从而保证其同步性使得:只有一个进程写完后另一个进程才能够去读。信号量也是解决同步机制的一种方法。

我们将多个进程看到的公共的一份资源称为临界资源。

把进程访问临界资源的代码称为临界区

多个执行流运行时互相干扰,主要是我们不加保护的访问了临界资源(在非临界区是没有影响的),为了更好的进行临界区的保护,我们让多执行流在任何时刻都只有一个进程能够进入临界区----我们将其称为互斥。

原子性:要么不做,要么做完,没有中间状态。

关于信号量我们在后面会有更加详细的解读。

相关推荐
醉颜凉25 分钟前
银河麒麟桌面操作系统V10 SP1:取消安装应用的安全授权认证
运维·安全·操作系统·国产化·麒麟·kylin os·安全授权认证
C++忠实粉丝1 小时前
Linux环境基础开发工具使用(2)
linux·运维·服务器
康熙38bdc2 小时前
Linux 环境变量
linux·运维·服务器
存储服务专家StorageExpert2 小时前
DELL SC compellent存储的四种访问方式
运维·服务器·存储维护·emc存储
hakesashou2 小时前
python如何比较字符串
linux·开发语言·python
Ljubim.te3 小时前
Linux基于CentOS学习【进程状态】【进程优先级】【调度与切换】【进程挂起】【进程饥饿】
linux·学习·centos
cooldream20093 小时前
Linux性能调优技巧
linux
大G哥3 小时前
记一次K8S 环境应用nginx stable-alpine 解析内部域名失败排查思路
运维·nginx·云原生·容器·kubernetes
醉颜凉3 小时前
银河麒麟桌面操作系统修改默认Shell为Bash
运维·服务器·开发语言·bash·kylin·国产化·银河麒麟操作系统
QMCY_jason4 小时前
Ubuntu 安装RUST
linux·ubuntu·rust