linux系统:进程间通信

1 进程间通信的目的

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

2进程间通信方式的分类

2.1 linux原生可以提供

  1. 匿名管道
  2. 命名管道

2.2 SystemV

主要是多进程,单机通信

  1. 共享内存
  2. 消息队列
  3. 信号量

2.3 POSIX

主要是多线程,网络通信

3 进程间通信的本质

先让不同的进程看到同一份资源

4 管道

4.1 管道的特征

①有入口,有出口
②单向传输内容
③传输的都是"资源"(数据)

4.2 匿名管道

4.2.1 使用的接口

int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

4.2.2 如何实现

①分别以读,写方式打开同一个文件
②fork()创建子进程
③双方进程各自关闭自己不需要的文件描述符
在这里插入图片描述

4.2.3 demo代码 父子进程进行通信

#include <iostream>
#include <cstdlib>
#include <string>
#include <cstdio>
#include <cstring>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;

int main()
{
   int pipefd[2] = {0};//创建文件描述符数组
   int n = pipe(pipefd);//创建匿名管道
   assert(n != -1);
   pid_t id = fork();
   assert(id != -1);

   if (id == 0) // 子进程进行读
   {

      char buffer[1024 * 8];
      close(pipefd[1]); // 关闭写端

      while (true)
      {
         ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);

         if (s == 0) // 读到文件末尾
         {
            cout << "father quit I quit" << endl;
            break;
         }
         else
         {
            cout << "child read a message[" << getpid() << "] father say:" << buffer << endl;
         }
      }
      exit(0);
   }
   else // 父进程进行写
   {
      close(pipefd[0]); // 关闭读端
      string message = "我是父进程,我正在给你发消息";
      int count = 0;
      char send[1024 * 8];
      // sleep(10);
      while (true)
      {
         int n = snprintf(send, sizeof(send) - 1, "%s[%d]  %d", message.c_str(), getpid(), count++);
         write(pipefd[1], send, sizeof(send) - 1);
         sleep(1);
         if (count == 10)
         {
            cout << "writer quit(father)" << endl;
            break;
         }
      }
      close(pipefd[1]);
      int ret = waitpid(id, nullptr, 0);
      assert(ret > 0);
      cout << ret << endl;
      return 0;
   }
}

在这里插入图片描述

4.2.4 匿名管道的特点

①具有血缘关系的进程来进行进程间通信,常用于父子通信
②实现进程间协同,提供了访问控制
写快,读慢,写满便不能再写了
写慢,读快,管道没有数据的时候,读必须等待
③管道提供的是面向流式的通信服务:面向字节流
每次写一条,读可以读多条
④管道是基于文件的,文件的生命周期是随进程的,管道的生命周期也是随进程的。
⑤管道是单向通信的,就是半双工通信的一种特殊情况

验证:
在这里插入图片描述
在这里插入图片描述
可以看到,由于在父进程进行写之前进行了休眠,匿名管道没有数据,所以子进程在读的时候会先阻塞,直到父进程停止休眠,往管道里面写数据。
在这里插入图片描述
在这里插入图片描述

父进程一直往管道里面写数据,由于子进程刚开始在休眠,没有读数据,直到休眠结束后,是将管道中已写的数据一次全部读出。即可以一次读多条信息

4.3 命名管道

4.3.1 原理

管道文件:一种特殊的文件,可以被打开,但是不会将内存数据进行刷新到磁盘
文件一定在系统路径中,路径具有唯一性。所以双方进程,可以通过管道文件的路径,看到同一份资源。

4.3.2 如何实现

①创建命名管道
int mkfifo(const char *filename,mode_t mode)
filename:你要创建的文件名字
mode :文件权限
返回值: 0:success ; -1: error

②之后就是进行相关的文件操作。双方进程各自以读方式和写方式打开文件,一方进行写数据,另一方进行读数据。

4.3.3 代码实现

mutiServer.cxx

#include "comm.hpp"
#include <wait.h>
void RDMessage(int fd)
{
    char message[SIZE];
    memset(message, '\0', sizeof(message));
    while (true)
    {
        ssize_t s = read(fd, message, sizeof(message));
        if (s > 0)
        {
            std::cout << "[" << getpid() << "]client say:" << message << endl;
        }
        else if (s == 0) // 读到最后一个
        {
            cout << "Read the last one client quit server quit too" << endl;
            break;
        }
        else
        {
            perror("read");
            exit(3);
        }
    }
}
int main()
{
    // 创建命名管道
    if (mkfifo(ipcPath.c_str(), MODE) == -1)
    {
        perror("mkfifo");
        exit(1);
    }
    Log("创建命名管道成功", Debug) << "step1" << endl;
    // 进行正常的文件操作
    // 打开文件
    int fd = open(ipcPath.c_str(), O_RDONLY); // 以只读方式打开
    if (fd == -1)
    {
        perror("open");
        exit(2);
    }
    Log("打开文件成功", Debug) << "step2" << endl;
    // 进行通信操作,读信息
    for (int i = 0; i < 3; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            RDMessage(fd);
            exit(3);
        }
    }
    for (int i = 0; i < 3; i++)
    {
        int ret = waitpid(-1, nullptr, 0);
        if (ret == -1)
        {
            perror("wait");
        }
    }

    // 关闭文件
    if (close(fd) == -1)
    {
        perror("close");
        exit(4);
    }
    Log("关闭文件成功", Debug) << "step3" << endl;
    // 删除管道
    unlink(ipcPath.c_str());
    Log("删除管道文件成功", Debug) << "step4" << endl;
    return 0;
}

client.cxx

#include"comm.hpp"
int main()
{
    //打开文件
    int fd=open(ipcPath.c_str(),O_WRONLY);
    if(fd<0)
    {
        perror("open");
    
    }
    //进行通信操作,往命名管道里面写
     string message;
     while(true)
     {
        cout<<"Please Enter Your Message"<<endl;
        getline(cin,message);
        write(fd,message.c_str(),message.size());
     }
     //关闭文件
     close(fd);
    return 0;
}

comm.hpp

#ifndef _COMM_H_
#define _COMM_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";


#endif

Log.hpp

#ifndef _LOG_H_
#define _LOG_H_

#include <iostream>
#include <ctime>

#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 std::cout;
}


#endif

在这里插入图片描述

可以看到,命名管道也是具有访问控制的。当前打开操作是为读而打开管道时,会先阻塞直到有相应进程为写而打开管道。进行写操作打开管道的时候也是如此。在读管道数据的时候,只有client进程先往管道里面写数据,server进程才会读数据。管道里面没有数据的时候会阻塞。

5 system V共享内存

5.1 实现原理

在这里插入图片描述

5.2 共享内存函数

共享内存提供者是操作系统
共享内存=共享内存块+对应的共享内存的内核数据结构

5.2.1 ftok函数

功能:通过一定的算法,使两个进程产生相同的key值
这是进行通信的关键:两个进程,只要key值相同,就代表看到的是同一个共享内存
原型
key_t ftok(const char *pathname, int proj_id);
参数
pathname:指定的文件名,这个文件必须是存在的而且可以访问的
proj_id:在0~255之间随意设置
返回值:当函数执行成功,则会返回key_t键值,否则返回-1
函数便是利用传进来的两个参数pathname和proj_id,使用一些算法,生成key值

5.2.2 shmget函数

功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
参数
key:ftok函数的返回值
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码(shmid);失败返回-1

5.2.3 shmat函数

功能:将共享内存段挂接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址(一般设置为NULL,自动选择一个地址)
shmflg:挂接以后允许的操作。比如只读、只写、可读可写等,一般设置为0,表示可读可写
返回值:成功返回一个指针,指向共享内存第一个字节;失败返回-1

5.2.4 shmdt函数

功能:将共享内存段与当前进程去关联
原型
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段

5.2.5 shmctl函数

功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

在这里插入图片描述

5.3 代码实现

shmServer

#include "comm.hpp"
#include <iostream>
#include <cstring>

int main()
{

  // 创建公共的key值
  key_t k = ftok(PATH_NAME, PROJ_ID);
  assert(k != -1);
  Log("create key success ", Debug) << "server key:" << k << endl;
  sleep(1);
  // 创建一个全新的共享内存
  //使用IPC_CREAT和IPC_EXCL:如果底层不存在,创建之,并返回;如果存在,出错返回。可以保证每一次创建的都是新的共享内存
  int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
  if (shmid == -1)
  {
    perror("shmget");
    exit(1);
  }
  Log("create shm success ", Debug) << "shmid:" << shmid << endl;
  sleep(1);
  // 将指定的共享内存,挂接到自己的地址空间
  char *shmaddr = (char *)shmat(shmid, nullptr, 0);
  Log(" attach shm success ", Debug) << "shmid:" << shmid << endl;
  sleep(1);
  // 通信1
  // 将shm想象成为一个大的数组
  while (true)
  {
    cout << shmaddr << endl;//直接将共享内存中的内容输出
    if (strcmp(shmaddr, "quit") == 0)//quit为结束标志
    {
      break;
    }
    sleep(1);
  }
   // 将指定的共享内存,从自己的地址空间中去关联
  int n = shmdt(shmaddr);
  assert(n != -1);
  (void)n;
  Log(" detach shm success ", Debug) << "shmid:" << shmid << endl;

  // 删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
  int s = shmctl(shmid, IPC_RMID, nullptr);
  assert(s != -1);
  (void)n;
  Log(" delete shm success ", Debug) << "shmid:" << shmid << endl;
  return 0;
}

shmClient

#include "comm.hpp"
#include <iostream>
int main()
{
  // 生成公共的key值
  key_t k = ftok(PATH_NAME, PROJ_ID);
  if (k < 0)
  {
    perror("ftok");
    exit(1);
  }
  Log("create key success ", Debug) << "client key:" << k << endl;

  // 获得共享内存
  int shmid = shmget(k, SHM_SIZE, 0);
  if (shmid == -1)
  {
    perror("shmid");
    exit(2);
  }
  Log("get shm success ", Debug) << "shmid:" << shmid << endl;

  // 将共享内存挂接到地址空间上
  char *shmaddr = (char *)shmat(shmid, nullptr, 0);
  Log("attach shm success ", Debug) << "shmid:" << shmid << endl;

  // 通信过程1
   char ch='a';
  for(ch='a';ch<='d';ch++)//向共享内存中写入数据
  {
     snprintf(shmaddr,SIZE_MAX,"[%d] hello server I am Client :%c\n",getpid(),ch);
     sleep(3);
   }
   snprintf(shmaddr,sizeof(shmaddr),"quit");
// 将指定的共享内存,从自己的地址空间去关联
 shmdt(shmaddr);
 Log("detach shm success ", Debug) << "shmid:" << shmid << endl;

  return 0;
}

comm.hpp

#include <iostream>
#include<cstring>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cassert>
#include "Log.hpp"

using namespace std; 

#define PATH_NAME "/home/ztb"
#define PROJ_ID 0x66
#define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE: 4096)的整数倍 

#define FIFE_NAME "./fifo.ipc"

在这里插入图片描述

通过运行结果可以发现
①只要通信双方使用shm,一方直接向共享内存中写入数据,另一方就可以立马看到。
这是因为共享内存创建好后属于用户空间,不用经过系统调用,直接可以访问。
所以共享内存是所有IPC中速度最快的,不需要过多拷贝(不需要将数据交给操作系统)
②共享内存缺乏访问控制
eg:在共享内存中没有数据的时候,也会进行读取
在写的一方写数据时,有可能数据还没有写完,此时读方已经将数据读走。
因为会带来并发问题
如何解决? 可以通过命名管道解决

要实现访问控制,即共享内存中写了数据之后才能读数据。
所以在读数据之前,如果共享内存是空的,便要阻塞在这里。
命名管道在读取数据的时候,如果没有数据,便会阻塞在这里
因此,在往共享内存写入数据后,往命名管道内写数据,证明可以开始读了
在读共享内存数据之前,先读命名管道内的数据,如果没有数据,证明此时共享内存也是空的,便会阻塞在这里,直到命名管道内有数据,可以开始读,此时共享内存也已经写了数据。实现了访问控制

comm.hpp

#pragma once

#include <iostream>
#include<cstring>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cassert>
#include "Log.hpp"

using namespace std; //不推荐

#define PATH_NAME "/home/ztb"
#define PROJ_ID 0x66
#define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE: 4096)的整数倍 

#define FIFE_NAME "./fifo.ipc"


class Init{
    public:
    Init()
    {
        int n=mkfifo(FIFE_NAME,0666);
        assert(n==0);
        Log("create fifo success\n",Notice);
       

    }
    ~Init()
    {
        unlink(FIFE_NAME);
        Log("delete fifo success\n",Notice);
    }
};
//定义Init类,在构造函数里面进行创建命名管道文件
//             在析构函数里面删除匿名管道文件
#define READ O_RDONLY
#define WRITE O_WRONLY
int OPEN_FIFO(string pathname,int flags)//打开文件
{
    int fd=open(pathname.c_str(),flags);
    assert(fd>=0);
    return fd;

}
//要实现同步控制,在写数据之后才能进行读数据
//在读共享内存中的数据之前,先调用wait()函数,只有写了数据后,才读数据
void wait(int fd)
{
    cout<<"等待中....."<<endl;
  uint32_t temp=0;
  ssize_t s=read(fd,&temp,sizeof(uint32_t));
  //命名管道中的数据是什么并不关心,主要是命名管道中没有数据便会阻塞在这里
  assert(s==sizeof(uint32_t));
}
 //在往共享内存中写数据之后,调用signal函数,往命名管道中写数据,这样另一方才能读数据,解除阻塞
void signal(int fd)
{
    uint32_t temp=1;
   
   ssize_t s= write(fd,&temp,sizeof(uint32_t));
    assert(s==sizeof(uint32_t));
    cout<<"唤醒Server进程"<<endl;

}
 void CLOSE_FIFO(int fd)//关闭文件
 {
    close(fd);
 }

shmServer

 // 通信2 利用命名管道实现共享内存的访问控制
   int fd=OPEN_FIFO(FIFE_NAME,READ);
   cout<<"server fd:"<<fd<<endl;
  while(true)
   {
      wait(fd);
      cout<<shmaddr<<endl;
      if(strcmp(shmaddr,"quit")==0)
     
            break;
      }
  }
  CLOSE_FIFO(fd);

shmClient


  int fd = OPEN_FIFO(FIFE_NAME, WRITE);
  cout << "client fd:" << fd << endl;
  while (true)
  {
    ssize_t s = read(0, shmaddr, SHM_SIZE); // 0号文件描述符代表的是标准输入,即将数据直接从标准输入读进共享内存中,实现写数据操作
    if (s > 0)
    {
      shmaddr[s - 1] = 0;
      signal(fd);
      if (strcmp(shmaddr, "quit") == 0)
      {
        break;
      }
    }
  }
  CLOSE_FIFO(fd);

在这里插入图片描述

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值