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脚本命令行每秒将一个字符串写入到命名管道文件中,再从另一端读取该命名管道中的数据打印到显示器上,这也进而体现了两个毫不相关的进程可以通过命名管道通信。
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清欢 Allen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值