C C++最新进程间通信(上),一位C C++大牛的BAT面试心得与经验总结

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

POSIX IPC(用的较多,也可以用来进行网络通信)

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

注意:System V IPC和POSIX IPC是两套标准(IPC是通信的简称)。

管道

什么是管道

  • 管道是Unix中最古老的进程间通信的形式。
  • 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

image-20221031150427050

匿名管道

image-20221031170631490

里面封装了两次open,第一次是以读方式打开,返回值写在fd[0]中,也就是打开文件的fd,第二次是以写方式打开,返回值写在fd[1]中,也就是打开文件的fd。同时通过上面的联合体标定它是一个管道文件。

#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码-1

image-20221031155504450

实例代码
简单的匿名管道实现
#include<iostream>
#include<cstdio>
#include<fcntl.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string>
#include<cstring>
#include<sys/wait.h>
using namespace std;
int main()
{
  //1.创建管道
  int pipefd[2] = {0};
  if(pipe(pipefd) != 0)
  {
    cerr << "pipe error" << endl;
    return 1;
  }
  //2.创建子进程
  pid_t id = fork();
  if(id < 0)
  {
    cerr << "fork error" << endl;
    return 2;
  }
  else if(id == 0)//子进程
  {
    //子进程来读取,关闭写端
    close(pipefd[1]);
#define MAX\_NUM 1024
    char buffer[MAX_NUM];
    while(true)
    {
      memset(buffer, 0, sizeof(buffer));
      ssize_t ret = read(pipefd[0], buffer, sizeof(buffer) - 1);
      if(ret > 0)
      {
        //读取成功,可以进行写入
        buffer[ret] = '\0';
        cout << "子进程受到消息了,消息内容:" << buffer << endl;
      }
      else if(ret == 0) 
      {
        sleep(1);//此处是为了稍微等一下父进程
        cout << "父进程写完了,我也退出了!" << endl;
        break;
      }
      else 
      {
        //Do nothing
      }
    }
    close(pipefd[0]);
    exit(0);
  }
  else//父进程
  {
    //父进程来写入,关闭读端
    close(pipefd[0]);
    const string msg = "你好子进程,我是父进程!这次发送的信息编号是: ";
    int cnt = 0;
    while(cnt < 5)
    {
      char sendBuffer[1024];
      sprintf(sendBuffer, "%s:%d", msg.c\_str(), cnt);
      write(pipefd[1], sendBuffer,strlen(sendBuffer));
      sleep(1);//为了看现象明显设计的
      cnt++;
    }
    close(pipefd[1]);
    cout << "父进程写完了" << endl;
  }
  pid_t res = waitpid(id, nullptr, 0);
  if(res > 0)
  {
    cout << "等待子进程成功!" << endl;
  }
  return 0;
}

问:父进程关闭写端了,子进程是如何知道父进程关闭写端的?

答:通过引用计数知道的,file结构体中,有类似引用的变量记录了有几个指针指向该文件。当引用计数为1了,说明此时就只有一个进程指向该文件了。此时子进程读完就不再有进程指向该文件了。

问:父进程每隔一秒写一次,为什么子进程也是一秒读一次呢?

答:当父进程在写入数据的时候,子进程在等(阻塞等待:将当前进程放在等待队列中(管道资源的等待队列中))!所以,父进程写入之后,子进程才能read(会返回)到数据,子进程打印读取数据要以父进程的节奏为主。所以父进程和子进程在读写的时候,是有一定的顺序性的(pipe内部自带访问控制(同步和互斥机制))。(父子进程在各自printf的时候(向显示器写入文件),并没有顺序,谁快谁先写,缺乏访问控制)。

管道内部,没有数据,reader就必须阻塞等待(等管道有数据);管道内部如果被写满了,writer就必须阻塞等待(等数据被读走)。

一个父进程控制单个子进程完成指定任务

代码:

#include<iostream>
#include<cstdio>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<ctime>
#include<string>
#include<vector>
#include<unordered\_map>
#include<cassert>
#include<cstdlib>

using namespace std;

typedef void(\*functor)();
vector<functor> functors;//方法集合
//for debug
unordered_map<uint32\_t, string> info;
void f1()
{
  cout << "这是一个处理日志的任务, 执行的进程id:" << getpid() << "执行时间是" << time(nullptr) << endl;;
}
void f2()
{
  cout << "这是一个备份数据的任务, 执行的进程id:" << getpid() << "执行时间是" << time(nullptr) << endl;;
}
void f3()
{
  cout << "这是一个处理网络请求的任务, 执行的进程id:" << getpid() << "执行时间是" << time(nullptr) << endl;;
}
void loadFunctor()
{
  info.insert({functors.size(), "处理日志的任务"});
  functors.push\_back(f1);
  info.insert({functors.size(), "处理备份数据的任务"});
  functors.push\_back(f2);
  info.insert({functors.size(), "处理网络请求的任务"});
  functors.push\_back(f3);
}

int main()
{
  //0.加载任务列表
  loadFunctor();
  //1.创建管道
  int pipefd[2] = {0};
  if(pipe(pipefd) != 0)
  {
    cerr << "pipe error" << endl;
    return 1;
  }
  //2.创建子进程
  pid_t id = fork();
  if(id < 0)
  {
    cerr << "fork error" << endl;
    return 2;
  }
  else if(id == 0)
  {
    //3.关闭不需要的文件
    close(pipefd[1]);
    //child:read
    //4.业务处理
    while(true)
    {
      uint32\_t operatorType = 0;
      //如果有数据就读取,如果没有数据就阻塞等待,等待任务的到来
      ssize_t s = read(pipefd[0], &operatorType, sizeof(uint32\_t));
      if(s == 0)
      {
        cout << "读取数据结束!退出!" << endl;
        break;
      }
      assert(s == sizeof(uint32\_t));
      //assert是断言,是编译有效debug模式
      //release模式,断言就没有了
      //如果断言没有了,那么s变量就是只被定义,没有被使用,在release模式中,就会有warning存在
      (void)s;
      if(operatorType < functors.size())
      {
        functors[operatorType]();
      }
      else 
      {
        cerr << "bug?operatorType = " << operatorType << endl;
      }
    }
    close(pipefd[0]);
    exit(0);
  }
  else if(id > 0)
  {
    srand((long long)time(nullptr));
    //3.关闭不需要的文件
    close(pipefd[0]);
    //parant:write
    //4.业务生成
    int num = functors.size();
    int cnt = 10;
    while(cnt--)
    {
      //5.形成任务码
      uint32\_t commandCode = rand() % num;
      cout << "父进程指派任务完成,任务是:" << info[commandCode] << ", 任务的编号是:"<< cnt << endl;
      //向指定的进程下达执行任务的操作
      
      write(pipefd[1], &commandCode, sizeof(uint32\_t));
      sleep(1);
    }
    close(pipefd[1]);
    pid_t res = waitpid(id, nullptr, 0);
    if(res)
    {
      cout << "wait success" << endl;
    }
  }

  return 0;
}

父进程控制一批子进程完成任务(进程池)

代码:

#include<iostream>
#include<cstdio>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<ctime>
#include<string>
#include<vector>
#include<unordered\_map>
#include<cassert>
#include<cstdlib>

using namespace std;

typedef void(\*functor)();
vector<functor> functors;//方法集合
//for debug
unordered_map<uint32\_t, string> info;
void f1()
{
  cout << "这是一个处理日志的任务, 执行的进程id:" << getpid() << "执行时间是" << time(nullptr) << endl << endl;;
}
void f2()
{
  cout << "这是一个备份数据的任务, 执行的进程id:" << getpid() << "执行时间是" << time(nullptr) << endl <<  endl;;
}
void f3()
{
  cout << "这是一个处理网络请求的任务, 执行的进程id:" << getpid() << "执行时间是" << time(nullptr) << endl << endl;;
}
void loadFunctor()
{
  info.insert({functors.size(), "处理日志的任务"});
  functors.push\_back(f1);
  info.insert({functors.size(), "处理备份数据的任务"});
  functors.push\_back(f2);
  info.insert({functors.size(), "处理网络请求的任务"});
  functors.push\_back(f3);
}
//int32\_t:进程pid int32\_t:该进程对应的管道写端fd
typedef pair<int32\_t, int32\_t> elem;
int processNum = 5;//创建子进程的个数
void work(int blockFd)
{ 
  cout << "进程:" << getpid() << "开始工作" << endl;
  //子进程核心工作的代码
  while(true)
  {
    //a.阻塞等待 b.获取任务信息
    uint32\_t operatorCode = 0;
    ssize_t s = read(blockFd, &operatorCode, sizeof(uint32\_t));
    if(s == 0)
    {
      break;
    }
    assert(s == sizeof(uint32\_t));
    (void)s;
    //c.处理任务
    if(operatorCode < functors.size())
    {
      functors[operatorCode]();
    }
  }
  cout << "进程:" << getpid() << "结束工作" << endl;
}
//[子进程的pid, 子进程的管道fd]
void BalanceSendTask(vector<elem>& processFds)
{
  srand((long long)time(nullptr));
  int cnt = 10;//cnt是要分配任务的数目
  while(cnt != 0)
  {
    sleep(1);
    //选择一个进程
    int pick = rand() % processFds.size();
    //选择一个任务
    int task = rand() % functors.size();
    //把任务给一个指定的进程
    write(processFds[pick].second, &task, sizeof(int));
    //打印对应的提示信息
    cout << "父进程指派任务"<< info[task] << "给进程:"  << processFds[pick].first << "编号:" << pick << endl;
    cnt--;
  }
}
int main()
{
  loadFunctor();
  vector<elem> assignMap;
  //创建processNum个进程
  for(int i = 0; i < processNum; i++)
  {
    //定义保存管道fd的对象
    int pipefd[2] = {0};
    //创建管道
    pipe(pipefd);
    //创建子进程
    pid_t id = fork();
    if(id == 0)
    {
      //子进程读取
      close(pipefd[1]);
      //子进程执行
      work(pipefd[0]);
      close(pipefd[0]);
      exit(0);
    }
    //父进程做的事情 
    close(pipefd[0]);
    elem e(id, pipefd[1]);
    assignMap.push\_back(e);
  }
  cout << "creat all process success!" << endl;
  //父进程派发任务
  BalanceSendTask(assignMap);
  
  //回收资源
  for(int i = 0; i < processNum; i++)
  {
    close(assignMap[i].second);
  }
  for(int i = 0; i < processNum; i++)
  {
    if(waitpid(assignMap[i].first, nullptr, 0) > 0)
    {
      cout << "wait for" << assignMap[i].first << "success!"  << "number:" << i << endl;
    }
  }
  return 0;
}

用fork来共享管道

image-20221031161452915

站在文件描述符角度-深度理解管道

image-20221031164213553

问:为什么父进程要分别打开读和写?

答:为了让子进程继承,让子进程不必再打开了。

问:为什么父子要关闭对应的读和写?

答:因为管道必须是单向通信的,一端是读端,另已端必须是写端。

问:谁决定父子关闭读端还是写端?

答:由需求决定。

站在内核角度-管道本质

image-20221101192704676

理解管道操作 – |

注意:|操作的本质就是匿名管道

image-20221101193113741

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

g.csdnimg.cn/img_convert/0853067f33810009063c3b78c34decef.png)

理解管道操作 – |

注意:|操作的本质就是匿名管道

image-20221101193113741

[外链图片转存中…(img-1p7OEtWh-1715732972819)]
[外链图片转存中…(img-8iu0g3zU-1715732972819)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值