【Linux】--进程间通信

进程间通信(IPC)

在此之前,我们得明白一个道理,为什么操作系统要提供进程间通信方式给用户?

举个栗子,为什么我们能够跟其他人可以很顺畅的交流?答:因为空气是一种传播声音的介质。好了,可以类比一下,进程之间具有独立性,每个进程都是操作自己的虚拟地址空间,无法与其他进程进行直接通信。如果两个独立的个体间没有公共的媒介,那么将无法沟通,操作系统就是提供了这种公共的媒介。

通信场景:数据传输、数据共享、数据控制
进程间的通信方式共有四种:管道、共享内存、消息队列、信号量
1、管道
管道本质是内核中的一块缓冲区,多个进程通过访问同一块缓冲区实现通信。
管道的分类:匿名管道和命名管道
(1)匿名管道
一个进程通过系统调用在内核中创建了一个管道,并且调用返回管道的操作句柄,这个管道没有其他的标识符,只能通过操作句柄访问。因此匿名管道只能用于有亲缘关系的进程间通信,因为只能通过子进程复制父进程的方式获取同一个管道的操作句柄,进而访问同一个缓冲区。并且一定要创建于创建子进程之前。
管道的操作句柄:两个文件描述符。
问题来了,为啥有两个?其实这个跟我们日常生活的管道还有点像,一个入,一个出。但是咱们Linux下的管道一个用于读取数据,一个用于写入数据。

函数:int pipe(int pipefd[2])----->int pipefd[2] pipe(pipefd)
意思是说有两个元素的int型数组,然后将数组的首地址传进去,通过pipefd返回两个操作句柄。其中:
pipefd[0]—用于向管道中读取数据
pipefd[1]—用于向管道中写入数据

返回值:成功返回0,失败返回-1
实例:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main()
{
        int pid=0;
        int pipefd[2];
        int ret=pipe(pipefd);  //创建管道应该在创建子进程之前
        if(ret<0)
          {
            perror("pipe error\n");
            return -1;
           }
        pid=fork();  //创建进程
        if(pid==0)   //子进程读数据
          {
          char buf[1024]={0};
          int ret=read(pipefd[0],buf,1023);
          if(ret<0)
         {
         perror("read error\n");
         return -1;
          }
        printf("child read data:[%s]\n",buf);
          }
        else  //父进程写数据
      {
         char *ptr="cold day";
        int ret=write(pipefd[1],ptr,strlen(ptr));
        if(ret<0)
        {
        perror("write error\n");
        return -1;
        }
     }
 }

运行结果:
可以看到,子进程读数据和父进程写数据是一致的。
在这里插入图片描述
**1)**那如果先让父进程sleep(5)?会发生什么呢?其实这意味着管道中就没有数据,因为才开始的五秒,父进程根本就没有生产数据,子进程read时候会阻塞,直到有数据
**2)**当管道数据写满的时候,write继续写入数据会阻塞,直到有数据读出去
如下图,我们先让子进程睡眠5秒

if(pid==0)   //子进程读数据
          {
          sleep(5);
          char buf[1024]={0};
          ........

让父进程先写数据,并求出数据长度总和

else  //父进程写数据
     {
     int pipe_size=0;   //定义起始数据长度为0
     while(1)
     {
        char *ptr="cold day";
        int ret=write(pipefd[1],ptr,strlen(ptr));
        if(ret<0)
        {
        perror("write error\n");
        return -1;
        }
     }
     pipe_size+=ret;  //写入的数据长度总和
     printf("write data:%d\n",pipe_size);  //打印总共写了多少
     }

在程序运行时,子进程会先睡眠5秒,此时父进程正在写数据,时间片完后,子进程被唤醒,则开始读数据,我们来看看运行结果(这里没有控制好输出格式,见谅):
在这里插入图片描述
结果也印证了我们的猜想,父进程写到65536的时候,子进程开始读数据。
**3)**若管道的所有写端被关闭,则read读完管道中的数据之后不会阻塞,而是返回0(返回0,表明没有人写,那继续读的话就没有意义)
**4)**若管道所有读端被关闭,则write 写入数据时候,会触发异常,进程退出
在这里插入图片描述
如图我们关闭了父进程的读端,那么进程将会异常退出。
**5)**对管道进行读写时候,若读写的数据大小不超过PIPE_BUF=4096大小,则可以保证操作的原子性。
以上就是我们说的管道的五个特性。

互斥:同一时间只有一个执行流能够操作临界资源数据,实现数据的安全操作,保证数据的操作安全性
同步:通过一种时序的判断,来实现对临街资源访问的时序合理性
(2)命名管道
命名管道:内核中的这块缓冲区有一个标识符,这个标识符是一个可见于文件系统的管道文件
特性:可用于同一主机上的任意进程间通信
注意:若命名管道以只读方式打开,则会阻塞直到管道文件被其他进程以写的方式打开
若命名管道以只写方式打开,则会阻塞直到管道文件被其他进程以读的方式打开
函数:int mkfifo(const char*pathname,mode_t mode)
pathname:管道路径名称
mode:管道文件的操作权限
返回值:成功返回0,失败返回-1
以下的demo演示的是命名管道的基本操作:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errono.h>
#include <fcntl.h>
#include <sys/stat.h>

int main()
{
  char *file="./test.fifo";
  int ret=mkfifo(file,0664);   //创建一个管道
    if(ret<0)
    {
      if(errno!=EEXIST)   //说明是其他方面的报错
      {
      perror("mkfifo error");
      return -1;
      }
    }
    int fd=open(file,O_RDWR);  //以可读可写方式打开这个文件
    if(fd<0)
    {
    perror("open error");
    return -1;
    }
    printf("open success\n");
    return 0;
}

当我们编译这个代码发现编译成功
在这里插入图片描述

2、共享内存

共享内存:用与进程间的数据共享
特性:最快的进程间通信方式
实现原理:
1)在物理内存中开辟一块空间,这块空间在内核中是具有标识的
2)将这块空间通过页表映射到自己的虚拟地址空间
3)通过虚拟地址进行内存操作
4)解除映射关系
5)删除共享内存

  • 共享内存相较于其他进程间通信方式,在通信过程中,少了两次用户态与内核态的数据拷贝,因此是速度最快的。

开辟出一块内存空间:int shmget(key_t key,size_t size,int shmflag)

  • 参数说明:
    key:共享内存在内核中的标识,其他进程通过相同的标识打开同一个内存
    size:共享内存的大小
    shmflg:IPC_CREAT|IPC_EXCL|mode 标志位
    返回值:成功返回共享内存的操作句柄,失败返回-1;

建立映射关系:void shmat(int shmid,const void shmaddr,int shmflg)

参数说明:
shmid:操作句柄
shmaddr:影射首地址
shmflg:操作
返回值:成功返回映射的首地址,失败返回-1

shmdt(shm_start); 解除映射关系,放入影射首地址就可以

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

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#include <sys/ipc.h>

#define IPC_KEY 0x12345678
#define SHM_SIZE 4096

int main()
{
        int shmid=shmget(IPC_KEY,SHM_SIZE,IPC_CREAT|664);  //创建一个共享内存
        if(shmid<0){
          perror("shmget error");
          return -1;
        }
        void *shm_start=shmat(shmid,NULL,0);   
        if(shm_start==(void*)-1)
        {
          perror("start error");
          return -1;
        }
        int i=0;
        while(1)
        {
          sprintf(shm_start,"%s-%d\n","good day",i++);//格式化字符串放到buff里面
          sleep(1);
        }
        shmdt(shm_start);  //解除映射关系,放入影射首地址就可以
        shmctl(shmid,IPC_RMID,NULL);  //删除共享内存
        return 0;
}

3、消息队列

  • 作用:用于进程间的数据块传输

  • 本质:内核中的优先级队列,多个进程通过向同一个队列中放置队列结点或者获取节点实现通信

  • 实现原理
    (1)在内核中创建队列
    (2)向队列中添加节点
    (3)从队列中获取节点
    (4)删除消息队列

  • 特性:
    (1)消息队列自带同步与互斥:如果队列无数据,则接收会阻塞,若队列满了,则添加数据也会阻塞
    (2)传输有类型的数据块
    (3)数据不会粘连
    消息队列目前已经被淘汰

4、信号量

  • 信号量:用于进程间的同步与互斥
    互斥:通过同一时间对临界资源只有一个进程能够访问,实现数据的安全操作–数据访问的安全性
    同步:通过一些条件判断实现对临界资源的有序访问-数据访问的合理性
  • 本质:是一个计数器+等待队列,但是计数器只有0/1两种状态
  • 实现
  • 互斥:通过一个状态标记临界资源当前的额访问状态;对临界资源进行访问之前先判断这个标记,若状态可访问,则将这个状态修改为不可访问,然后去访问数据,访问完毕后再将状态修改为可访问状态。通过一个只有0/1的计数器实现
  • 同步:通过一个计数器对资源数量进行计数,想要获取临界资源的时候,先判断计数,是否有资源可供访问,若有,则计数-1,获取一个资源进行操作,若没有资源(即就是计数<=0),则进行等待,等到其他进程生产数据后计数进行+1,然后唤醒等待的进程。通过一个计数的判断以及等待与唤醒功能实现
  • 信号量的PV原语
  • p操作:对计数进行判断,然后进行-1,如果没有资源则等待
  • v操作:对奇数进行+1,唤醒等待队列中挂起的进程

总结

进程间的通信笔者就先整理到这里,不完善的后续会继续订正添加,还请关注后续Linux–信号

发布了33 篇原创文章 · 获赞 13 · 访问量 1065

猜你喜欢

转载自blog.csdn.net/Vicky_Cr/article/details/103101187