Bootstrap

Linux 进程间通信(IPC)

进程间通信介绍

进程间通信的概念

进程间通信(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操作(必须是原子性)

;