进程间通信(管道、共享内存、消息队列、信号量)

进程间通信

因为进程间具有独立性,每个进程操作的都是自己的虚拟地址空间。由于通信场景不同,提供了不同的通信方式。通信方式有:管道、共享内存、消息对列、信号量等

管道(用于进程之间的数据传输)

  • 什么是管道呢?

    管道的本质是内核中的一块缓冲区。
    在这里插入图片描述

  • 管道的特性

    半双工通信:半双工通信(Half-duplex Communication)可以实现双向的通信,但不能在两个方向上同时进行,必须轮流交替地进行。在这种工作方式下,发送端可以转变为接收端;相应地,接收端也可以转变为发送端。但是在同一个时刻,信息只能在一个方向上传输。因此,也可以将半双工通信理解为一种切换方向的单工通信。
    ②:管道的生命周期随进程,进程关闭,对应的管道端口关闭,两个进程都关闭,则管道关闭。
    ③:管道自带同步与互斥:管道为空时读取,read 阻塞;管道满时写入,write 阻塞。
    ④:管道提供字节流传输服务

  • 管道的分类

    ①:匿名管道
    ②:命名管道

匿名管道
什么是匿名管道?
  • 匿名管道缓冲区没有标识符,只能通过子进程复制父进程的方式获取到管道的操作句柄。因为父进程frok子进程,父子进程各自拥有一个文件描述符表,但是两者的内容是一样的,既然内容一样,那么指向的就是同一个管道。匿名管道只能在具有亲缘关系的进程之间使用创建匿名管道一定要放在创建子进程之前
如何创建匿名管道
  • int pipe(int fildes[2]);

成功:返回0 。失败:返回-1
pipefd[0]:从管道读取数据
pipefd[1]:向管道写入数据

代码举例:

#include <stdio.h>    
#include <unistd.h>    
     
int main() {    
    int pipefd[2]; 
    int ret = pipe(pipefd);   
    if(ret < 0) {    
        perror("pipe error");    
        return -1;    
    }    
                                                                                                                                       
    pid_t pid = fork();    
    if(pid < 0)
     {    
        perror("fork error");    
        return -1;    
    }    
    else if(pid == 0)
    {    
        sleep(3); //子进程休眠3s    
        close(pipefd[0]);    
        write(pipefd[1], "Hello pipe", 10);      
        close(pipefd[1]);    
    }    
    else
    {    
        close(pipefd[1]);    
        char buf[20] = {0};    
        read(pipefd[0], buf, 20);    
        printf("Parent process read:%s\n", buf);    
    }       
    return 0;    
}
匿名管道的读写规则(命名管道也是相同)(详细了解匿名管道读写规则
  • 管道为空时读取:
    在这里插入图片描述

  • 管道为满时写入:
    在这里插入图片描述

  • 当管道所有读端关闭时写入:

    如果所有管道读端对应的文件描述符被关闭,则 write 操作会产生 SIGPIPE 信号,进而导致 write 进程直接退出。

  • 当管道所有写端关闭时读取:

    如果所有管道写端对应的文件描述符被关闭,那么管道中剩余的数据都被读取完后, read 会返回 0。

管道的原子性问题(详细了解管道的原子性问题
  • 当要写入的数据量不大于 PIPE_BUF(4096B) 时,Linux将保证写入的原子性;
  • 当要写入的数据量大于 PIPE_BUF(4096B) 时,Linux将不再保证写入的原子性。
命名管道
什么是命名管道?
  • 内核中的这块缓冲区有一个标识符,是一个真实存在于文件系统的管道文件。可用于同一主机上的任意进程间通信(这也是和匿名管道相比最主要的区别)。
如何创建命名管道
  • 通过指令创建

mkfifo filename(管道文件名)

  • 在程序中创建

int mkfifo(const char *pathname, mode_t mode);
pathname:文件路径名
mode:文件权限
成功:返回 0 。失败:返回 -1

代码举例:

int main()
{
  //int mkfifo(const char *pathname, mode_t mode);
  char *file = "./mytest.fifo";
  umask(0);
  int ret = mkfifo(file,0444);
  if(ret < 0)
  {
    if(errno != EEXIST)//设置为当返回的错误不是因为文件已经存在时才报错
    {
      perror("mkfifo error");
      return -1;
    } 
  }
    return 0;
  }
命名管道文件的打开规则(此处是受open接口的第二个参数影响,O_RDONLY、O_WRONLY时阻塞,O_RDWR时成功打开)在这里插入图片描述
  • 若命名管道文件以只读的方式打开,则会阻塞,直到管道文件被其它进程以写的方式打开。
  • 若命名管道文件以只写的方式打开,则会阻塞,直到管道文件被其它进程以读的方式打开。
命名管道读写规则
  • 用代码来检验:

    fifo_write.c (向管道写入数据) :

//fifo_write.c
int main()
{
  //int mkfifo(const char *pathname, mode_t mode);
  char *file = "./mytest.fifo";
  umask(0);
  int ret = mkfifo(file,0666);
  if(ret < 0)
  {
    if(errno != EEXIST)//当错误不是因为文件已经存在时才报错
    {
      perror("mkfifo error");
      return -1;
    } 
   }
  int fd = open(file,O_WRONLY);//设置当前进程管道文件已只写方式打开
  if(fd < 0)
  {
      perror("open error");
      return 0;
  }
  printf("open success\n");
    
  while(1)
  { 
     char buf[1024] = {0};
     scanf("%s",buf);
     int ret1 = write(fd,buf,strlen(buf));
     if(ret1 < 0)
     {
         perror("write error");
         return -1;
     }
  }
  return 0;
}

fifo_read.c (从管道读取数据):

//fifo_read.c
int main()
{
  //int mkfifo(const char *pathname, mode_t mode);
  char *file = "./mytest.fifo";
  umask(0);
  int ret = mkfifo(file,0666);
  if(ret < 0)
  {
    if(errno != EEXIST)//当错误不是因为文件已经存在时才报错
    {
      perror("mkfifo error");
      return -1;
    } 
   }

   int fd = open(file,O_RDONLY);//设置当前进程管道文件已只写方式打开
   if(fd < 0)
   {
      perror("open error");
      return 0;
   }
    printf("open success\n");

    while(1)
    {
       char buf[1024]={0};
       int ret = read(fd,buf,1023);
       if(ret == 0)
       {
           printf("所有写端被关闭!\n");
           return 0;
       }
       else if(ret < 0)
       {
         perror("read error");
         return -1;
       }
       printf("buf:[%s]\n",buf);
    }
    return 0;
}
  • 文件打开的规则验证(上面已经提到具体规则,此处进行验证)

    当只编译运行 fifo_read.c 或 fifo_write.c 文件时,可以发现打开文件失败,进程阻塞:
    在这里插入图片描述
    当在两个终端同时运行 fifo_read.c 和 fifo_write.c 文件时,可以发现打开文件成功:
    在这里插入图片描述
    总结:若命名管道文件以只读的方式打开,则会阻塞,直到管道文件被其它进程以写的方式打开。若命名管道文件以只写的方式打开,则会阻塞,直到管道文件被其它进程以读的方式打开。

  • 命名管道的读写规则与匿名管道的读写规则相同

    ①管道为空时读取:

    当同时运行 fifo_read.c 和 fifo_write.c 文件时 ,在这里插入图片描述
    总结:
    在这里插入图片描述
    ②管道满时写入:

    当我在 fifo_read.c 中在从管道读取数据之前 sleep(1000),在 fifo_wirte 中不断写入,制造管道写满时再写入的情况:

    在这里插入图片描述
    输出结果:
    在这里插入图片描述
    总结:在这里插入图片描述
    ③当管道所有读端关闭时写入:
    在这里插入图片描述
    总结:

    如果所有管道读端对应的文件描述符被关闭,则 write 操作会产生 SIGPIPE 信号,进而导致 write 进程直接退出。

    ④当管道所有写端关闭时读取:
    在这里插入图片描述
    总结:

    如果所有管道写端对应的文件描述符被关闭,那么管道中剩余的数据都被读取完后, read 会返回 0。

共享内存(用于进程之间的数据共享)

什么是共享内存?
  • 用于进程间的数据共享,相比较于其它通信方式来说,它是最快的进程间通信方式。因为在通信过程中,少了两次用户态与内核态之间的数据拷贝。进程不再通过执行进入内核的系统调用来传递彼此的数据,而是直接进行读写 。
共享内存特性
  • 生命周期随内核,当进程退出后依然存在于内核。
  • 不存在同步与互斥!
实现原理
  • 1、在物理内存中开辟一块空间(这块空间在内核中是具有标识的)
  • 2、将这块空间通过页表映射到自己的虚拟地址空间中。
  • 3、通过虚拟地址进行内存操作。
  • 4、解除映射关系。
  • 5、删除共享内存。
    在这里插入图片描述
通过实现原理体会对共享内存的基本操作
  • 开辟物理空间,创建共享内存

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

    key:共享内存在内核中的标识,其他进程通过相同标识符打开同一个内存。是一个大于0的32位整数
    size:共享内存大小
    shmflg: 设置参数有:IPC_CREAT、IPC_EXCL
    // IPC_CREAT: 如果内核中不存在键值与key相等的共享内存,则新建一个共享内存;如果存在这样的共享内存,返回此共享内存的标识符。
    // IPC_EXCL:如果内核中不存在键值 与key相等的共享内存,则新建一个共享内存;如果存在这样的共享内存则报错。
    //shmflg参数还需要加上权限,一般用法为:IPC_CREAT|IPC_EXCL|mode(mode为权限,如:0664),如果不加权限,则默认为 0
    返回值:成功- - -》返回共享内存的操作句柄 。失败- - - 》返回 -1

    #define IPC_KEY 0x22222222
    #define SHM_SIZE 4096
    int main()
    {
      int shmid = shmget(IPC_KEY,SHM_SIZE,IPC_CREAT|IPC_EXCL|0664);
      if(shmid < 0)
      {
        perror("shmget error");
        return -1;
      }
      return 0;
    }
    
    

    可以通过指令 ipcs 可以查看进程间通信方式,其中 ipcs -m 可以只查看共享内存:
    在这里插入图片描述

  • 建立映射关系(将共享内存映射到进程地址空间)

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

    shmid:共享内存的操作句柄
    shmaddr:指定共享内存出现在进程内存地址的什么位置,一般直接指定为NULL,让内核自己决定一个合适的地址位置
    shmflg:SHM_RDONLY:为只读模式,0 为读写模式

      //建立映射关系
      void *shm_start = shmat(shmid,NULL,0);
      if(shm_start == (void*)-1)
      {
        perror("shmat error");
        return -1;
      }	
    
  • 解除映射关系

    int shmdt(const void *shmaddr);

    shmaddr:连接的共享内存的起始地址

     //解除映射关系
     shmdt(shm_start);
    
  • 删除共享内存

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

    shmid:共享内存的操作句柄
    cmd:可设置的参数有IPC_STAT、IPC_SET、IPC_RMID三个,其中主要了解IPC_RMID(删除这片共享内存)
    buf:不获取相关信息则通常设置为NULL

    //删除共享内存
    shmctl(shmid,IPC_RMID,NULL);	
    

    其中,IPC_RMID调用后并不会直接删除共享内存,而是等到 nattch (链接数,可以通过 ipcs -m 查看)减到 0 时才会真正删除。建立映射的进程每退出一个,链接数就减 1。

通过共享内存实现读端进程与写端进程通信
  • shm_write.c:

    //shm_write.c
    #define IPC_KEY 0x22222222
    #define SHM_SIZE 4096
    int main()
    {
      //开辟物理空间
      int shmid = shmget(IPC_KEY,SHM_SIZE,IPC_CREAT|0664);
      if(shmid < 0)
      {
        perror("shmget error");
        return -1;
      }
      
      //建立映射关系
      void *shm_start = shmat(shmid,NULL,0);
      if(shm_start == (void*)-1)
      {
        perror("shmat error");
        return -1;
      }
      
      //通过虚拟地址进行内存操作
      int i = 0;
      while(1)
      {
        sprintf(shm_start,"%s-%d\n","hello world",i++);
        //sprintf:格式化数据放到一个buf里面
      }
    
      //解除映射关系
      shmdt(shm_start);
    
      //删除共享内存
      shmctl(shmid,IPC_RMID,NULL);
    
      return 0;
    }	
    
  • shm_read.c:

    //shm_read.c
    #define IPC_KEY 0x22222222
    #define SHM_SIZE 4096
    int main()
    {
      //开辟物理空
      int shmid = shmget(IPC_KEY,SHM_SIZE,IPC_CREAT|0664);
      if(shmid < 0)
      {
        perror("shmget error");
        return -1;
      }
       
      shmctl(shmid,IPC_RMID,NULL);
      return  0;
      //建立映射关系
      void *shm_start = shmat(shmid,NULL,0);
      if(shm_start == (void*)-1)
      {
        perror("shmat error");
        return -1;
      }
      
      //通过虚拟地址进行内存操作
      int i = 0;
      while(1)
      {
        printf("%s\n",shm_start);
        sleep(1);
      }
    
      //解除映射关系
      shmdt(shm_start);
    
      //删除共享内存
      shmctl(shmid,IPC_RMID,NULL);
    
      return 0;
    }
    

    运行如下图:
    在这里插入图片描述
    值得注意的是,向共享内存映射的进程地址空间写数据时是覆盖写入的!!!

消息队列(用于进程之间的数据块传输)(了解)

什么是消息队列?
  • 它的本质是内核中的一个优先级队列,多个进程通过向同一队列中放置队列结点或获取结点实现通信。
消息队列特性
  • 消息队列自带同步与互斥
  • 传输有类型的数据块
  • 传输数据时不会粘连
  • 生命周期随内核
实现原理
  • 在内核中创建消息队列
  • 向队列中获取节点
  • 从队列中获取节点
  • 删除消息队列

信号量(用于实现进程之间的同步与互斥)(了解)

什么是信号量?
  • 本质上就是共享资源的数目,就是内核中一个原子操作的计数器和一个等待队列,用来控制对共享资源的访问。信号量用于实现进程间的同步与互斥。
如何实现进程间的同步与互斥
  • 实现互斥(通过一个只有0/1的计数器实现)

    通过一个状态标记临界资源当前的访问状态。对临界资源进行访问之前先判断一下这个标记,若状态为可访问,则将这个状态修改为不可访问,然后去访问数据,访问完毕之后再将状态修改为可访问状态。

  • 实现同步(通过一个计数的判断以及等待与唤醒功能的实现)

    通过一个计数器对资源数量进行计数,想要获取临界资源的时候,则先判断计数,是否有资源可供访问。若有资源(计数 > 0),则计数 -1 ,获取一个资源进行操作。若没有资源(计数 <= 0),则进行等待,等到其他进程生产数据后计数进行 +1,然后唤醒等待的进程。

  • 信号的PV原语:

    P操作:对计数进行判断,然后进行 -1 ,若没有资源则等待。
    V操作:对计数进行 +1 ,唤醒等待队列中的挂起进程。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值