Linux 进程间通信(IPC)

news2024/12/25 8:56:21

文章目录

  • 进程间通信介绍
    • 进程间通信的概念
    • 进程间通信的目的
    • 进程间通信的本质理解
    • 进程间通信分类
  • 管道
    • 匿名管道
      • 匿名管道的原理
    • pipe函数
      • 管道的特点
    • 命名管道
      • 命名管道的原理
      • 使用相关命令创建命名管道
      • 使用命名管道实现server端和client端通信
  • system V共享内存
    • 共享内存通信的基本原理
    • 共享内存的基本构成
    • 共享内存函数
      • 创建共享内存
      • 释放共享内存
      • 关联共享内存
      • 取消关联共享内存
      • 使用共享内存让shmServer和shmClien通信
      • 共享内存与管道的区别
    • 浅谈 system V信号量
      • 信号量的原理

进程间通信介绍

进程间通信的概念

进程间通信(ipc), 指不同进程之间的信息传输或交换。

进程间通信的目的

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

进程间通信的本质理解

我们知道进程间具有独立性,因为写时拷贝存在,父子进程之间向持续性的交换数据是不可能的。如果想让两个进程通信,必须先让不同的进程看到同一份资源作为数据交换的场所。

进程间通信分类

管道

  • 匿名管道
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

注意:针对以上分类,我们主要对管道和共享内存详细学习。

管道

什么是管道

  • 管道是Unix中最古老的进程间通信的形式,将数据单项传输,是一种单向通信的方式。
  • 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。

匿名管道

匿名管道的原理

匿名管道通信原理:进程之间通过管道进行通信。

主要步骤如下
假设,我们让父进程对目标文件写入数据,子进程对目标文件读取数据。

  • 那么此时父进程通过files_struct中的四号文件描述符(关闭三号文件描述符)找到struct_file文件指针,再通过inode找到目标文件加载到物理内存中文件缓冲区的数据。
  • 子进程通过files_struct中的三号文件描述符(关闭四号文件描述符)找到struct_file文件指针,再通过inode找到目标文件加载到物理内存中文件缓冲区的数据。
  • 综上,此时父子进程就看到了同一份“资源”,父子进程进而可以对该文件进行写,读。操作,从而父子进程打实现进程间通信。

在这里插入图片描述

注意

  • 在创建父子进程的时候 进程相关数据结构需要重新拷贝,被打开文件相关内核结构不会被拷贝。因为fork函数只是为了创建子进程,不会对文件相关数据结构作拷贝。

  • 此时父子进程看到的同一份文件资源,并对其进行写,读操作,并不会发生写时拷贝,因为被打开文件内核数据结构是由操作系统维护的,不受进程维护。

  • 文件描述符意义:0:标准输入,1:标准输出,2:标准错误,3:读文件描述符,4:写文件描述符。

  • 进程间通信是内存级通信,不需要将数据写入到磁盘文件中,因为反复的IO会降低效率,也没有必要。

pipe函数

pipe函数一般用于创建匿名管道,pipe函数原型如下:

int pipe( int fd[2] ) 

参数
fd: 文件描述符数组,其中fd[0]代表读端,fd[1]代表写端。

返回值
成功返回0,失败返回-1.

实践代码

//父进程写入文件,子进程读取文件

#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <assert.h>
using namespace std;
int main()
{
    //创建管道
     int pipefd[2] = { 0 }; //pipefd[0]: 读端 //pipefd[1]: 写端.
     
     int n = pipe( pipefd );
     
     assert( n !=- 1 );

     (void)n;

    //创建子进程。
    pid_t id = fork();

    assert( id != -1 );

    if( id == 0 )      //子进程,子进程读取。
    {
            
        close(pipefd[1]);
        
        char buffer[1024];

        while( true )   //先将数据读取到缓冲区中打印出来。
        {
            ssize_t s = read( pipefd[0],buffer,sizeof(buffer)-1);
            
            if( s > 0 )
            {
                buffer[s] = '\0';
                
                cout << " child get a message[" << getpid() <<"] father#" << buffer << endl; 
            }
        }
        exit(0);
    }
    
     close(pipefd[0]);  //父进程,父进程写入。
     
     string message = "我是父进程,我正在给你发信息";

     int count = 0; 

     char send_buffer[1024];  

     while( true )
     {
          snprintf( send_buffer,sizeof(send_buffer), "%s: %d",message.c_str(),count++ );

          write(pipefd[1],send_buffer,strlen(send_buffer));

          sleep(1);
     }
     
     pid_t ret = waitpid(id,nullptr,0);

     (void ) ret;
     
     assert(ret > 0 );
    
     return 0;
}

运行结果如下

在这里插入图片描述

管道的特点

1 . 管道一般用来具有血缘关系的进程用来进程间通信---->常用于父子进程。

2 .管道具有通过让进程间协同,进而提供访问控制。

  • 如果父进程写数据慢(每一次写入暂停10秒),子进程读取数据快,那么子进程便会等待父进程将数据写入后再读取(子进程也暂停10秒)。
  • 但是如果父进程写得快,子进程读得慢(每读一次暂停10秒),这样父进程在缓冲区写满之后就不会继续写了,等子进程将数据全部读完,父进程再进行写入。

3.管道提供的是面向流式的通信服务(面向字节流,写一条都一条,如果写得快,读的慢,那么就会一次性读取数据)。

4.管道是基于文件的,文件的生命周期是基于进程的,从而管道的生命周期跟随于进程。如果在通信过程中,父子进程都退出,那么文描述符便会被关闭,管道也会自动退出。
例如,当父进程写入文件10次后便关闭文件描述符退出,而子进程此时read的返回值便会为零,那么子进程也会退出。

int main()
{
    //创建管道
     int pipefd[2] = { 0 }; //pipefd[0]: 读端 //pipefd[1]: 写端.
     
     int n = pipe( pipefd );
     
     assert( n !=- 1 );

     (void)n;

    //创建子进程。
    pid_t id = fork();

    assert( id != -1 );

    if( id == 0 )      //子进程,子进程读取。
    {
            
        close(pipefd[1]);
        
        char buffer[1024];

        while( true )   //先将数据读取到缓冲区中打印出来。
        {
            ssize_t s = read( pipefd[0],buffer,sizeof(buffer)-1);
            
            if( s > 0 )
            {
                buffer[s] = '\0';
                
                cout << " child get a message[" << getpid() <<"] father#" << buffer << endl; 
            }
            
            else if(  s == 0 )
            {
                cout <<  "writer quit, i quit" << endl;
                
                break;
            }
        }
        
        close(pipefd[0]);

        exit(0);                       
    }
    
     
    
     close(pipefd[0]);  //父进程,父进程写入。
     
     string message = "我是父进程,我正在给你发信息";

     int count = 0; 

     char send_buffer[1024];  

     while( true )
     {
          snprintf( send_buffer,sizeof(send_buffer), "%s: %d",message.c_str(),count++ );

          write(pipefd[1],send_buffer,strlen(send_buffer));

          sleep(1);

          cout << count << endl;

          if( count == 10 )
          {
                cout << "writer quit(father)" << endl;
                break;
          }
     }
     close( pipefd[1]);              //父进程准备退出,关闭文件描述符。

     pid_t ret = waitpid(id,nullptr,0);

     (void ) ret;
     
     assert(ret > 0 );

     return 0;
}

运行结果如下
在这里插入图片描述
5.管道是单项通信,本质上就是半双工通信的一种特殊情况,通信双方中,一方固定为读端,一方固定为写端。

6.两种特殊情况。

  • 父进程写端关闭,子进程读0,那么表示读到了文件结尾。
  • 子进程读端关闭,父进程继续读取,OS也会终止写进程。

命名管道

命名管道的原理

  • 由于匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。一般情况下,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间便可以通信。
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作。我们可以让两个进程打开该文件,此时进而让两个进程看到了同一份“资源”,进而实现进程通信。

注意

  • 命名管道是一种特殊类型的文件,可以被打开,但是不会将内存数据刷新到磁盘中,相对于匿名管道来说,命名管道有文件名,并且存在于系统路径中。

使用相关命令创建命名管道

我们可以通过mkfifo命令创建一个命名管道。

[yzh@yzh test1]$ mkfifo fifo

并且,我们可以看到创建出来的文件类型为p,即代表的是管道文件。
在这里插入图片描述
此时,我们便可以通过shell脚本命令行每秒将一个字符串写入到命名管道文件中,再从另一端读取该命名管道中的数据打印到显示器上,这也进而体现了两个毫不相关的进程可以通过命名管道通信。
在这里插入图片描述

使用命名管道实现server端和client端通信

为了让服务端(server)与客户端(client)进程间通信,服务端(client)步骤如下:

  • 创建命名管道
  • 创建多个子进程,让这些子进程通过文件描述符读端读取命名管道数据到缓冲区中。
  • 父进程等待子进程退出,关闭文件描述符读端,删除命名管道文件。
#include "common.hpp"
static void getMessage(int fd)
{
    char buffer[SIZE];
    while (true)
    {
        memset(buffer, '\0', sizeof(buffer));
        ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            cout <<"["  << getpid() << "] "<< "client say> " << buffer<< endl;
        }
        else if (s == 0)
        {
            // end of file
            cerr <<"["  << getpid() << "] " << "read end of file, clien quit, server quit too!" << endl;
            break;
        }
        else
        {
            // read error
            perror("read");
            break;
        }
    }
}

int main()
{
    // 1. 创建管道文件
    umask(0);
    
    if (mkfifo(ipcPath.c_str(), MODE) < 0)
    {
        perror("mkfifo");
        exit(1);
    }

    Log("创建管道文件成功", Debug) << " step 1" << endl;

    // 2. 正常的文件操作
    int fd = open(ipcPath.c_str(), O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        exit(2);
    }
    Log("打开管道文件成功", Debug) << " step 2" << endl;

    int nums = 3;
    for (int i = 0; i < nums; i++)
    {
        pid_t id = fork();
        if (id == 0)             //随机让每一个子进程读取数据,当client端退出的时候
        {
            // 3. 编写正常的通信代码了
            getMessage(fd);
            exit(1);
        }
    }
    for(int i = 0; i < nums; i++)   //父进程随机等待子进程退出结果,当子进程全部退出完毕,父进程退出程序。
    {
        waitpid(-1, nullptr, 0);
    }
    // 4. 关闭文件
    close(fd);
    Log("关闭管道文件成功", Debug) << " step 3" << endl;

    unlink(ipcPath.c_str()); // 通信完毕,就删除文件
    Log("删除管道文件成功", Debug) << " step 4" << endl;

    return 0;
}

客户端(server)步骤如下:

  • 打开命名管道文件。
  • 通过getline函数将cin中的数据读取到缓冲区,再通过缓冲区写入到管道文件中。
  • 关闭文件描述符写端。
#include "common.hpp"
#include <sys/wait.h>
int main()
{
    // 1. 获取管道文件
    int fd = open(ipcPath.c_str(), O_WRONLY);
    if(fd < 0)
    {
        perror("open");
        exit(1);
    }

    // 2. ipc过程
    string buffer;
    while(true)
    {
        cout << "Please Enter Message Line :> ";
        std::getline(std::cin, buffer);                  //从cin中读取数据到buffer中。       
        write(fd, buffer.c_str(), buffer.size());             
    }

    // 3. 关闭
    close(fd);
    return 0;
}

log.hpp文件,主要包含打印命名管道文件提示符函数。

#define Debug   0
#define Notice  1
#define Warning 2
#define Error   3


const std::string msg[] = {
    "Debug",
    "Notice",
    "Warning",
    "Error"
};

std::ostream& Log(std::string message, int level)
{
    std::cout << " | " << (unsigned)time(nullptr) << " | "<< msg[level] << " | " << message;
    return cout;
}

common.hpp文件,主要包含一些系统函数头文件。

#include <sys/types.h>
#include <sys/wait.h>
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "log.hpp"

using namespace std;

#define MODE 0666
#define SIZE 128

string ipcPath = "./fifo.ipc"; //命名管道文件的路径。

运行结果如下:
在这里插入图片描述
如果客户端(server)退出后,服务端read读取的返回值变为0,此时表示服务端已经读取到数据结尾,所以子进程也将逐步退出。
在这里插入图片描述
如果服务端(server)退出后,客户端(client)写入管道的数据将不会被服务端读取,当客户端(client)下一次再向管道写入数据时,就会收到操作系统发来的13号信号(SIGPIPE),此时客户端也被强制退出。
在这里插入图片描述

system V共享内存

共享内存通信的基本原理

  • 如果进程之间通过共享内存通信,那么需要先让进程之间看到一份内存“资源”。首先,操作系统必须在物理内存之中申请一块内存空间,并将这一块共享内存通过每一个进程的页表,映射到各自的虚拟地址空间(mm_struct)堆栈之间,进而映射到每一个进程之中。
    • 如果需要释放共享内存,那么必须先将共享内存与进程之间的映射关系解除,然后再释放共享内存。
      在这里插入图片描述

共享内存的基本构成

共享内存是由操作系统维护的,由于在操作系统中可能存在大量使用共享内存进行通信的进程,为了管理这些共享内存,操作系统除了要在物理内存中开辟共享内存之外,还必须通过有关共享内存的数据结构(描述共享内存的所有属性)进行管理。所以,共享内存 = 共享内存块 + 有关共享内存的数据结构。

共享内存函数

创建共享内存

shmget函数主要用来创建共享内存。

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

参数

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

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

使用ftok函数获取key

  • ftok函数主要通过算法将已经存在的路径和一个项目ID(proj_id)转换为一个随机值(key)。如果我们通过传递参数key来使用shmget创建共享内存,那么该key会被写入到共享内存数据结构之中。
key_t ftok(const char *pathname, int proj_id);

参数

  • pathname: 一定为具有访问权限的路径
  • proj_id 项目ID

注意:

  • 使用ftok函数很有可能会创建失败(发生冲突),此时我们可以修改ftok函数参数重新创建。
  • 如果两个进程之间需要共享内存通信,那么使用在ftok函数来获取key时,必须使用相同的参数。因为只用使用同一种算法,才能获取到同一个key,进而才能识别并匹配到同一个共享内存。

shmget的常见组合方式

  • IPC_CREAT: 如果操作系统内核中不存在与key值相等的共享内存,则新建一个共享内存并返回共享内存的用户层ID。如果存在这样的共享内存,那么直接返回该共享内存的标句柄。————表明不管操作系统中是否含有与key相等的共享内存,都会返回一个共享内存,但是无法确定是否为新建的共享内存。

  • IPC_CREAT | IPC_EXCL: 如果操作系统内核中不存在与key值相等的共享内存,则新建一个共享内存,如果存在,则出错返回。————表明如果成功返回,该共享内存一定是那个新建的共享内存。

实践代码如下
我们创建一个共享内存,并打印该用户层ID和获取到的key值。

int main()
{
    //创建公共的key值
    key_t k = ftok(PATH_NAME,PROJ_ID);

    if (k == -1 )
    {
        perror("ftok");
    }
    
    Log("create key done",Debug) << "server key " << k << std::endl;

    //创建共享内存--必须要创建一个全新的共享内存。

    int shmid = shmget(k,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);

    if (shmid == -1 )
    {
        perror("shmget");
    }  

    cout << "该共享内存的句柄 ->" << shmid << endl; 
    
}

运行结果如下:
在这里插入图片描述
当然,我们也可以使用ipcs -m 命令来查看共享内存相关信息。(包含共享内存的key和shmid)
在这里插入图片描述

释放共享内存

我们知道,管道的生命周期基于文件,文件的生命周期基于进程,所以管道的生命周期基于进程,当进程退出后,管道也将被释放大。
但是,共享内存的生命周期是基于操作系统内核,并不与进程相关联。所以,当相关进程退出时,共享内存并不会释放,直到关机重启。

如果我们需要及时将共享内存释放,有两种方法:

  • 使用命令释放共享内存
  • 在进程通信后结束进程时使用相关函数进行释放

使用命令行释放共享内存

我们可以通过ipcrm -m shmid 命令将共享内存释放。

在这里插入图片描述

使用shmctl函数释放共享内存

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

参数

  • shmid: 由shmget返回的共享内存标识码
  • cmd: 将要采取的动作
  • buf:指向一个保存着共享内存的模式状态和访问权限的数据结构。为了保证关联共享内存成功,我们一般设置权限为大0666。

返回值
成功返回0,失败返回-1.

以下为shmctl函数中cmd参数常见命令

命令说明
IPC_STAT获取共享内存的当前关联值,此时参数buf作为输出型参数
IPC_SET在进程有足够权限的前提下,将共享内存的当前关联值设置为buf所指的数据结构中的值
IPC_RMID删除共享内存段
int main()
{
    //创建公共的key值
    key_t k = ftok(PATH_NAME,PROJ_ID);

    if (k == -1 )
    {
        perror("ftok");
    }
    
    Log("create key done",Debug) << " server key " << k << std::endl;

    //创建共享内存--必须要创建一个全新的共享内存。

    int shmid = shmget(k,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);

    if (shmid == -1 )
    {
        perror("shmget");
    }  

    sleep(10);
    

    //删除共享内存。
    int n = shmctl(shmid,IPC_RMID,NULL);
    assert( n != -1 );
    (void)n;

     Log("delete shm done",Debug) << " shmid " << shmid << endl;
    
}

运行结果如下:
我们使用相关脚本命令监视共享内存,当共享内存创建完毕,shmid值变为1。当进程结束,共享内存释放完毕,shmid值变为0。
在这里插入图片描述

关联共享内存

我们可以通过shmat函数将共享内存段连接到对应的进程地址空间中。

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

参数

  • shmid: 共享内存标识
  • shmaddr: 指定共享内存映射到进程地址空间的某一地址中,通常设置为NULL,表示让操作系统内核决定一个合适的地址位置。
  • shmflg:关联共享内存时设置的某些属性,它的两个可能取值是SHM_RND和SHM_RDONLY,我们一般设置为0,默认设置为读写权限。

返回值
成功:返回指向共享内存的指针;失败:返回-1

以下我们便使用shmat函数对共享内存关联。

int main()
{
    //创建公共的key值
    key_t k = ftok(PATH_NAME,PROJ_ID);

    if (k == -1 )
    {
        perror("ftok");
    }
    
    Log("create key done",Debug) << " server key " << k << std::endl;

    //创建共享内存--必须要创建一个全新的共享内存。

    int shmid = shmget(k,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);

    if (shmid == -1 )
    {
        perror("shmget");
    }  

    sleep(10);
    
    //关联共享内存
    char* shmaddr = (char*)shmat(shmid,nullptr,0);
     Log("attach shm",Debug) << "shmid " << shmid << std::endl;
     sleep(10);
      
    //删除共享内存。
    int n = shmctl(shmid,IPC_RMID,NULL);
    assert( n != -1 );
    (void)n;

     Log("delete shm done",Debug) << " shmid " << shmid << endl;
    
}

运行结果如下:
此时,我们发现关联该共享内存进程数由0变为1,随着该进程退出,又变为了0。
在这里插入图片描述

取消关联共享内存

我们可以使用shmdt函数将共享内存段与当前进程脱离。

int shmdt(const void *shmaddr);

参数
shmaddr: 由shmat所返回的指向共享内存的指针。
返回值
成功返回0;失败返回-1.

注意
将共享内存段与当前进程脱离不等于删除共享内存段。

我们接下来增加shmdt函数对该共享内存去关联。

int main()
{
    //创建公共的key值
    key_t k = ftok(PATH_NAME,PROJ_ID);

    if (k == -1 )
    {
        perror("ftok");
    }
    
    Log("create key done",Debug) << " server key " << k << std::endl;

    //创建共享内存--必须要创建一个全新的共享内存。

    int shmid = shmget(k,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);

    if (shmid == -1 )
    {
        perror("shmget");
    }  

    sleep(10);
    
    //关联共享内存。
    char* shmaddr = (char*)shmat(shmid,nullptr,0);
     Log("attach shm",Debug) << "shmid " << shmid << std::endl;
     sleep(10);

     //共享内存去关联
     //将指定的内存空间,去关联
    int n1 = shmdt(shmaddr);
    assert( n1 != -1 );
    Log("detach shm",Debug) << " shmid " << shmid << std::endl; 
    sleep(10);
    
      
    //删除共享内存。
    int n2 = shmctl(shmid,IPC_RMID,NULL);
    assert( n2 != -1 );
    (void)n2;

     Log("delete shm done",Debug) << " shmid " << shmid << endl;
    
}

``

通过结果发现,随着shmdt函数去关联,nattch(关联数)也由1变为了0,即表示去关联成功。
在这里插入图片描述

使用共享内存让shmServer和shmClien通信

结论一:只要通信双方使用共享内存,一方直接向共享内存写入数据,另一方,就可以立马看到。因为共享内存是所有进程间通信(IPC)速度最快的,不需要过多的拷贝。

shmServer.cpp

int main()
{
    //创建公共的key值
    key_t k = ftok(PATH_NAME,PROJ_ID);

    if (k == -1 )
    {
        perror("ftok");
    }
    
    Log("create key done",Debug) << " server key " << k << std::endl;

    //创建共享内存--必须要创建一个全新的共享内存。

    int shmid = shmget(k,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);

    if (shmid == -1 )
    {
        perror("shmget");
    }  

    sleep(10);
    
    //关联共享内存。
    char* shmaddr = (char*)shmat(shmid,nullptr,0);
     Log("attach shm",Debug) << "shmid " << shmid << std::endl;
     sleep(10);
     
     //向shamaddr中读取数据。
     for(;;)
    {
        // 临界区
        printf("%s\n", shmaddr);
        if(strcmp(shmaddr, "quit") == 0) break;
         sleep(1);
    }
    
    //共享内存去关联
     //将指定的内存空间,去关联
    int n1 = shmdt(shmaddr);
    assert( n1 != -1 );
    Log("detach shm",Debug) << " shmid " << shmid << std::endl; 
    sleep(10);
    
      
    //删除共享内存。
    int n2 = shmctl(shmid,IPC_RMID,NULL);
    assert( n2 != -1 );
    (void)n2;

     Log("delete shm done",Debug) << " shmid " << shmid << endl;
    
}

shmClient.cpp

int main()
{
    //1.创建公共的key值。
    key_t k = ftok(PATH_NAME,PROJ_ID);
    
    assert( k != -1 );
    
    Log("create key done",Debug) << "client key " << k << std::endl;
  
    //2. 获取共享内存。
    int shmid = shmget(k, SHM_SIZE, 0);

    assert( shmid >= 0 );

    Log("create shm success", Error) << " client key : " << k << endl;

    //3.关联共享内存。
    char *shmaddr = (char *)shmat(shmid, nullptr, 0);
    if(shmaddr == nullptr)
    {
        Log("attach shm failed", Error) << " client key : " << k << endl;
        exit(3);
    }
    Log("attach shm success", Error) << " client key : " << k << endl;

    //4.向共享内存写入数据
     char a = 'a';
    for(; a <= 'z'; a++)
     {
         shmaddr[a-'a'] = a;
         // 我们是每一次都向shmaddr[共享内存的起始地址]写入
          snprintf(shmaddr, SHM_SIZE - 1,\
         "hello server, 我是其他进程,我的pid: %d, inc: %c\n",\
          getpid(), a);
        sleep(1);
     }
    return 0;
}

common.hpp

#include <stdio.h>
//#include <cassert>
#include <sys/types.h> 
#include <iostream>
#include <sys/ipc.h> 
#include <sys/shm.h> 
#include <unistd.h>
#include "log.hpp"
using namespace std;

#define PATH_NAME "/home/yzh/test1"
#define PROJ_ID 0X66
#define SHM_SIZE 4096

由运行结果可知,当shmServer端运行,创建好共享内存后,随后运行shmClient端写入数据到共享内存时(每一次写入数据都写入到共享内存的初始地址),由shmServer读取内存数据打印在屏幕上。
在这里插入图片描述

共享内存与管道的区别

1.只要通信双方使用shm,一方直接向共享内存中写入数据,另一方就立马可以从共享内存中获取,因为相比于管道而言,共享内存是所有进程通信(IPC)中速度最快的。

管道通信过程
当我们使用管道通信,需要调用read,write接口进行数据传输。而在这个过程中,需要进行四次拷贝

  • 服务端读取输入文件中的数据到临时缓冲区中。
  • 服务端将临时缓冲区中的数据写入到管道文件中。
  • 客户端读取管道文件中的数据读取到临时缓冲区中
  • 客户端将临时缓冲区的数据写入到输出文件中。
    在这里插入图片描述
    共享内存通信过程

我们很明显的可以看出,两个进程间使用共享内存通信之间并不需要在客户端和服务端中建立临时缓冲区传输数据,所以只需要两次拷贝。
在这里插入图片描述

2.相比于管道而言,共享内存缺乏访问控制,会带来并发问题

并发问题,有可能会带来当一端还在写入数据时,还没有将数据写完,但是另一端就立马读取的问题。

为此,我们可以在shmServer端和shmClient端通信时增加一个命名管道通信(通信内容不重要),此时相当于也让两端在共享内存中通信时也具备着命名管道中进程控制的性质。

common.hpp
我们让命名管道在程序运行时就已经创建了,在进程退出时自动销毁并且对它一些函数进行包装。

#include <stdio.h>
//#include <cassert>
#include <sys/types.h> 
#include <iostream>
#include <sys/ipc.h> 
#include <sys/shm.h> 
#include <unistd.h>
#include <assert.h>
#include "log.hpp"
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>

using namespace std;

#define FIFO_NAME "myfifo"
#define PATH_NAME "/home/yzh/test1"
#define PROJ_ID 0X66
#define SHM_SIZE 4096

#define READ  O_RDONLY
#define WRITE O_WRONLY



class Init
{
public:
    Init()
    {
        umask(0);
        int n = mkfifo(FIFO_NAME, 0666);
        assert(n == 0);
        (void)n;
        Log("create fifo success",Notice) << "\n";
    }
    ~Init()
    {
        unlink(FIFO_NAME);
        Log("remove fifo success",Notice) << "\n";
    }
};

#define READ O_RDONLY
#define WRITE O_WRONLY

int OpenFIFO(std::string pathname, int flags)
{
    int fd = open(pathname.c_str(), flags);
    assert(fd >= 0);
    return fd;
}

void Wait(int fd)
{
    Log("等待中....", Notice) << "\n";
    uint32_t temp = 0;
    ssize_t s = read(fd, &temp, sizeof(uint32_t));
    assert(s == sizeof(uint32_t));
    (void)s;
}

void Signal(int fd)
{
    uint32_t temp = 1;
    ssize_t s = write(fd, &temp, sizeof(uint32_t));
    assert(s == sizeof(uint32_t));
    (void)s;
    Log("唤醒中....", Notice) << "\n";
}

void CloseFifo(int fd)
{
    close(fd);
}

shmServer.cpp
在通信时,我们先能够读取命名管道中的数据,才能读取共享额你存中的数据。

Init init;
int main()
{
    Log("child pid is : ", Debug) << getpid() << endl;
    key_t k = ftok(PATH_NAME, PROJ_ID);
    if (k < 0)
    {
        Log("create key failed", Error) << " client key : " << k << endl;
        exit(1);
    }
    Log("create key done", Debug) << " client key : " << k << endl;

    // 获取共享内存
    int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666); //
    if(shmid < 0)
    {
        Log("create shm failed", Error) << " client key : " << k << endl;
        exit(2);
    }
    Log("create shm success", Error) << " client key : " << k << endl;

    // sleep(10);

    char *shmaddr = (char *)shmat(shmid, nullptr, 0);
    if(shmaddr == nullptr)
    {
        Log("attach shm failed", Error) << " client key : " << k << endl;
        exit(3);
    }
    Log("attach shm success", Error) << " client key : " << k << endl;
  

    int fd = OpenFIFO(FIFO_NAME, READ);
    for(;;)
    {
        Wait(fd);   

        // 临界区
        printf("%s\n", shmaddr);
        if(strcmp(shmaddr, "quit") == 0) break;
        // sleep(1);
    }


    // 去关联
    int n = shmdt(shmaddr);
    assert(n != -1);
    Log("detach shm success", Error) << " client key : " << k << endl;
    // sleep(10);

    // client 要不要chmctl删除呢?不需要!!

    return 0;
}

shmServer.cpp
当shmServer运行,并且输入数据,才能激活命名管道。

int main()
{
    Log("child pid is : ", Debug) << getpid() << endl;
    key_t k = ftok(PATH_NAME, PROJ_ID);
    if (k < 0)
    {
        Log("create key failed", Error) << " client key : " << k << endl;
        exit(1);
    }
    Log("create key done", Debug) << " client key : " << k << endl;

    // 获取共享内存
   
    if(shmid < 0)
    {
        Log("create shm failed", Error) << " client key : " << k << endl;
        exit(2);
    }
    Log("create shm success", Error) << " client key : " << k << endl;

    // sleep(10);

    char *shmaddr = (char *)shmat(shmid, nullptr, 0);
    if(shmaddr == nullptr)
    {
        Log("attach shm failed", Error) << " client key : " << k << endl;
        exit(3);
    }
    Log("attach shm success", Error) << " client key : " << k << endl;
    // sleep(10);

    int fd = OpenFIFO(FIFO_NAME, WRITE);
    // 使用
    // client将共享内存看做一个char 类型的buffer
    while(true)
    {
        ssize_t s = read(0, shmaddr, SHM_SIZE-1);
        if(s > 0)
        {
            shmaddr[s-1] = 0;
            Signal(fd);
            if(strcmp(shmaddr,"quit") == 0) break;
        }
    }
    close(fd);
   
    // 去关联
    int n = shmdt(shmaddr);
    assert(n != -1);
    Log("detach shm success", Error) << " client key : " << k << endl;
    // sleep(10);

    // client 要不要chmctl删除呢?不需要!!

    return 0;
}

运行结果如下:
在这里插入图片描述

浅谈 system V信号量

在之前进程通信的几种方式中,本质上都是优先解决一个问题,让不同的进程看到同一份资源,比如共享内存,也带来了一些时序问题,造成两端数据不一致问题。

信号量的相关概念。

  • 临界资源: 多个进程(执行流)看到的公共的一份资源。
  • 临界区: 进程需要访问访问临界资源的代码段
  • 互斥:为了地对临界区的保护,在任何时刻,可以让多执行流都只能一个进程进入临界区。(为了不让进程之间互相打扰)
  • 原子性:信号量没有中间状态,要么不做,要么做完。

信号量的原理

每一个进程想访问临界资源,必须先申请信号量,而信号量本质上就是一个计数器(类似于 int count )。当申请信号量成功,本质上就是让信号量计数器–,当信号量减为0时,就不能再增加进程访问临资源了。

能不能用一个整数(n)去标识信号量呢?

cpu执行命令流程:
1.将内存中的数据加载到cpu内的寄存器中(读指令)

2:申请信号量( 分析 & 执行指令 )——> n–;

3:将CPU修改完毕的n写回内存。

然而执行流CPU执行命令的时候,每一步都有可能被另一个进程切换,此时寄存器中包含着执行流的上下文数据,被整个执行流所共享。

假设n = 2;
如果当一个进程正在执行到第2步( n = 1 ) ,然而又被另一个进程切换到第一步( 此时 n 又变成了 2 )并且执行完第三步,n 又变成了1,原本n = 2意味着只能让2个进程访问临界资源的某一部分,执行完毕后,n = 1,表明这还能容纳一个进程访问临界资源。

这就是说明整数count导致的时序问题,此时n有中间状态,进而造成数据并不一致问题。

信号量计数器

申请信号量 - > 计数器-- -> P操作(必须是具有原子性)
释放信号量 -> 计数器++ -> V操作(必须是原子性)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/913986.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux: scp 使用 Permission denied 错误解决

scp: /system.img Permission denied权限被拒绝&#xff0c;换一个目录执行&#xff0c;或者给这个目录添加权限 chmod 777 从本地复制到远程 scp local_file remote_usernameremote_ip:remote_file scp local_file remote_ip:remote_file 从远程复制到本地 scp rootwww.run.…

3D WEB轻量化引擎HOOPS产品助力NAPA打造船舶设计软件平台

NAPA&#xff08;Naval Architectural PAckage&#xff0c;船舶建筑包&#xff09;&#xff0c;来自芬兰的船舶设计软件供应商&#xff0c;致力于提供世界领先的船舶设计、安全及运营的解决方案和数据分析服务。NAPA拥有超过30年的船舶设计经验&#xff0c;年营业额超过2560万欧…

罗勇军 →《算法竞赛·快冲300题》每日一题:“小球配对” ← 并查集

【题目来源】http://oj.ecustacm.cn/problem.php?id1850http://oj.ecustacm.cn/viewnews.php?id1023【题目描述】 给定 n 个小球&#xff0c;编号为 1-n&#xff0c;给定 m 个篮子&#xff0c;编号为 1-m。 每个球只允许放入样例给定的编号为 Ai 或者 Bi 的两个篮子中的 1 个…

从不均匀性角度浅析AB实验

本篇的目的是从三个不均匀性的角度,对AB实验进行一个认知的普及,最终着重讲述AB实验的一个普遍的问题&#xff0c;即实验准确度问题。 一、AB实验场景 在首页中&#xff0c;我们是用红色基调还是绿色基调&#xff0c;是采用门店小列表外商品feed&#xff08;左图&#xff09;…

【jstat命令】查看jvm内存占用和GC情况

以下两个常用命令 第一个命令&#xff1a;用作查看内存占用和GC情况&#xff08;当前老年代内存空间、老年代使用空间…&#xff09;&#xff1b; 第二个命令&#xff1a;用作查看内存分配情况&#xff08;老年代最小内存空间、老年代最大内存空间…&#xff09;。 1、当前内存…

excel文本函数篇3

replace的替换&#xff0c;是通过指定位置做替换&#xff0c;只能替换一个&#xff0c;跟python中的不一样&#xff0c;python中是通过字串替换。那么怎么实现全部替换呢&#xff1f; ----> substitute函数 &#xff08;1&#xff09;后缀没有B&#xff1a;一个字节代表一个…

补充1 MATLAB_GUI_通过普通按钮PushButton的回调函数ButtonDownFcn创建一个长按回调按钮

目录 一、实例效果二、补充的知识点&#xff08;两种回调函数&#xff09;三、步骤  1. 先建一个空白的GUI。  2.在GUI Figure 上添加一个按钮&#xff08;PushButton&#xff09;组件&#xff0c;并设置其属性&#xff0c;例如位置、大小和文本等。  3.CtrS保存一下GUI。…

回流焊炉温曲线图讲解

从下面回流焊炉温曲线标准图分析回流焊的原理&#xff1a; 当PCB进入升温区&#xff08;干燥区&#xff09;时&#xff0c;焊锡膏中的溶剂、气体蒸发掉&#xff0c;同时焊锡膏中的助焊剂润湿焊盘、元器件端头和引脚&#xff0c;焊锡膏软化、塌落、覆盖了焊盘&#xff0c;将焊盘…

vue 项目在编译时,总是出现系统崩的状态,报错信息中有v7 或者 v8 的样式-项目太大内存溢出

vue 项目在编译时&#xff0c;总是出现系统崩的状态&#xff0c;node 命令框也会报错&#xff0c;如下图&#xff1a;有v7 或者 v8 的样式。 原因分析&#xff1a; 分析&#xff1a;遇到与上面图片相似的问题&#xff0c;我们要首先要想到是否是 有关内存的问题&#xff0c;当然…

2023年科技趋势展望报告,这几个领域程序员可以恰饭

太空电梯、MOSS、ChatGPT等&#xff0c;都预兆着2023年注定不会是平凡的一年。那么在2023年&#xff0c;科技领域都有哪些发展趋势呢&#xff1f; 麦肯锡发布了最新的《2023科技趋势展望报告》&#xff0c;从通用人工智能到未来生物工程&#xff0c;总结了备受关注的15种科技趋…

【Unity细节】Unity制作汽车时,为什么汽车会被弹飞?为什么汽车会一直抖动?

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 秩沅 原创 &#x1f636;‍&#x1f32b;️收录于专栏&#xff1a;unity细节和bug &#x1f636;‍&#x1f32b;️优质专栏 ⭐【…

8.注册中心-nacos

1.Nacos 之前有用过spring cloud的eureka注册中心&#xff0c;配置起来稍微繁琐&#xff1b;nacos是阿里开发的一款注册中心&#xff0c;操作就简单多了 1.1 Nacos 下载 Nacos 快速开始这个快速开始手册是帮忙您快速在您的电脑上&#xff0c;下载、安装并使用 Nacos。https:/…

独立站推广方式盘点,轻松赢得流量和收益!

独立站不受平台限制&#xff0c;不用给平台佣金&#xff0c;好是很好&#xff0c;但它的问题在于它完全没有自然流量。想要做独立站&#xff0c;就必须做推广。独立站推广怎么做&#xff1f;看这篇准没错&#xff01; 01.SEO搜索引擎优化 搜索引擎优化&#xff08;SEO&#x…

nlp系列(7)三元组识别(Bi-LSTM+CRF)pytorch

模型介绍 在实体识别中&#xff1a;使用了Bert模型&#xff0c;CRF模型 在关系识别中&#xff1a;使用了Bert模型的输出与实体掩码&#xff0c;进行一系列变化&#xff0c;得到关系 Bert模型介绍可以查看这篇文章&#xff1a;nlp系列&#xff08;2&#xff09;文本分类&…

摄影预约小程序开发全攻略

当今社会&#xff0c;移动互联网的普及使得手机APP和小程序成为了各行各业的必备工具。在摄影行业&#xff0c;如何让客户更加方便地预约摄影服务&#xff0c;提高工作效率&#xff0c;成为了摄影店主们亟需解决的问题。而定制一款适合自己摄影店的小程序&#xff0c;将成为一种…

【IT运维知识】暴力破解和防暴力破解定义说明

很多刚入行的小伙伴对于暴力破解相关知识不是很了解&#xff0c;今天我们就来简单聊聊。 暴力破解 暴力破解是指采用反复试错的方法并希望最终猜对&#xff0c;以尝试破解密码或用户名或找到隐藏的网页&#xff0c;或者找到用于加密消息的密钥。这是一种较老的攻击方法&#x…

Java免费自学网站墙裂推荐!!!!

最近&#xff0c;常有一些读者问我&#xff1a;“有没有什么推荐的Java学习网站啊&#xff1f;” 因为一直没有时间&#xff0c;所以我之前也是让大家上知乎、搜索引擎搜一下就好了。 但是&#xff0c;我深知不能这样&#xff0c;应该拿出更真诚的态度带来优质的内容。 于是…

TP-Link 智能灯泡缺陷能让黑客窃取用户 WiFi 密码

来自意大利和英国的研究人员在 TP-Link Tapo L530E 智能灯泡和 TP-Link Tapo 应用程序中发现了4个漏洞&#xff0c;攻击者可以利用这些漏洞窃取目标的 WiFi 密码。 TP-Link Tapo L530E 是包括亚马逊在内的多个市场上最畅销的智能灯泡。TP-link Tapo是一款智能设备管理应用程序…

Mybatis动态SQL和模糊查询

一. Mybatis动态SQL 动态 SQL 是 MyBatis 的强大特性之一。在 JDBC 或其它类似的框架中&#xff0c;开发人员通常需要手动拼接 SQL 语句。根据不同的条件拼接 SQL 语句是一件极其痛苦的工作。例如&#xff0c;拼接时要确保添加了必要的空格&#xff0c;还要注意去掉列表最后一…

MySQL 8.0 启动和关闭流程

启动流程 mysql 服务端对启动命令进行了层层封装&#xff0c;目的是为了操作简便。同时也提供了原生的启动方式以便应对特殊情况。 建议: systemctl start起来的就使用systemctl stop停。 如果用mysqld stop&#xff0c; systemctl识别不到。 启停方式 systemd ---->/et…