进程间通信(IPC)

流过昼夜 提交于 2020-01-17 01:03:50

进程间通信(Interprocess communication)
一、概述:

进程的用户空间是互相独立的,一般而言是不能互相访问的,唯一的例外是共享内存区。进程间通信是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。这使得一个程序能够在同一时间里处理许多用户的要求。因为即使只有一个用户发出要求,也可能导致一个操作系统中多个进程的运行,进程之间必须互相通话。IPC接口就提供了这种可能性。每个IPC方法均有它自己的优点和局限性,一般,对于单个程序而言使用所有的IPC方法是不常见的。

二、目的:

(1)数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。
(2)共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
(3)通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
(4)资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
(5)进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

  • 进程通过与内核及其它进程之间的互相通信来协调它们的行为。Linux支持多种进程间通信(IPC)机制,信号和管道是其中的两种。除此之外,Linux还支持System V 的IPC机制(用首次出现的Unix版本命名)。

三、种类:
进程间通信主要包括:管道, 系统IPC(包括消息队列,信号,共享内存),套接字(SOCKET)

System V 标准
管道—用于进程间的数据传输
共享内存—用于进程间的数据共享
消息队列—用于进程间的数据传输
信号量—用于实现进程间的控制

四、管道:
1、本质

内核中的一块缓冲区—通过半双工通信实现数据传输,通过让多个进程都能访问到同一块缓冲区,进而实现进程间通信。

2、管道的分类:

(1)匿名管道PIPE:这块缓冲区在内核中没有标识,通常有两种限制:

  • 一是单工,只能单向传输;
  • 二是只能在父子或者兄弟进程间使用.

( 2)流管道s_pipe: 和匿名管道一样,只是去除了匿名管道中的第一种限制,为半双工,可以双向传输。
在创建管道时,操作系统会提供两个操作句柄(文件描述符),其中一个从管道读取数据,一个向管道写入数据,子进程通过复制父进程的方式,获取到管道的操作句柄,和父进程,兄弟进程访问同一个管道,进而实现进程间通信。
( 3)命名管道:name_pipe, 在内核中有标识的缓冲区(标识符是一个可见于文件系统的管道文件), 其他进程你可以通过这个标识符,找到这块缓冲区(通过打开统一个管道文件,进而访问到同一块缓冲区),可以在许多并不相关的进程之间进行通讯.

3、接口

1.匿名管道
int pipe(int pipefd[2]);//创建一个匿名管道,向用户通过参数pipefd返回管道的操作句柄
//pipefd[0]---从管道读取数据
//pipefd[1]---向管道写入数据
//返回值--- 0,成功;  -1,失败
  #include<stdio.h>
  #include<stdlib.h>
  #include<unistd.h>
  #include<string.h>                                                                                                                                                       
  
  int main()
  {
      char buf[1024] = {0};
      int pipefd[2] = {0};
      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)//子进程
      {
          int fd = read(pipefd[0],buf,1023);//若管道中没有数据,则read就阻塞
          if(fd < 0)
          {                                                                                                                                       
              perror("read error:");                                                                                                              
              return -1;                                                                                                                          
          }                                                                                                                                       
          printf("子进程读取成功~~\n");    
          printf("buf[%s]-[%d]\n",buf,fd);    
      }                                 
                            
      else//父进程    
      {                                    
        char* ptr = "这是一个测试匿名管道的demo~~";    
         if(write(pipefd[1],ptr,strlen(ptr)) < 0)//若管道写满,则write就阻塞    
         {    
             perror("write error:");                                                                                                                                       
             return -1;                                                        
         }    
         printf("父进程写入成功~~~\n");    
      }                                                                        
                     
      while(1)                             
      {                                                                        
          printf("---------------%d\n",getpid());                                                                                                          
          sleep(1);                                                                                                                                        
      }                                                                                                                                                    
      return 0;                                                                                                                                            
  } 

特性
(1)若管道中没有数据,则read会阻塞;若管道写满了,则write会阻塞;----管道自带同步与互斥。

  • 同步:对临界资源访问的合理性,保证临界资源访问的安全性
  • 互斥:通过保证同一时间只有一个进程能够访问临界资源(一次仅允许一个进程使用的资源),保证临界资源访问的安全性,对管道进行数据操作的大小不超过PIPE_BUF(4096)的时候,则保证操作的原子性。

(2)若管道所有的写端被关闭(当前没有进程继续在写入数据了),read读完管道中的数据之后,就不会阻塞,返回0。
(3)若管道所有读端被关闭(当前没有进程读数据了),继续write就会触发异常,程序退出。

这个命令就利用了一个管道:ps -ef | grep sshps -ef是的将打印到标准输出中,grep sshd 是从标准输入读取数据过滤sshd打印到标准输出中,而|将两者链接,ps -ef的结果经过grep sshd的过滤打印到标准输出上,这就用到了管道。
在这里插入图片描述
简单模拟实现一下

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
    int pipefd[2];
    int ret = pipe(pipefd);//创建子进程之前创建管道
    if (ret < 0) 
    {
        perror("pipe error");
        return -1;
    }

    int pid1 = fork();//ps进程
  
    if(pid1 <  0)
    {
        perror("fork1 error:");
        return -1;
    }

    else if (pid1 == 0) 
    {
        dup2(pipefd[1], 1);//将ps -ef命令打印在标准输出的内容重定向到管道的写入端
        execl("/usr/bin/ps", "ps", "-ef", NULL);//替换程序ps -ef
        exit(0);
    }

    int pid2 = fork();//grep进程
    if (pid2 == 0)
    {
        close(pipefd[1]);//进入grep进程时,grep要从管道读取端读取数据,先要关闭管道的写入端,read才不会阻塞
        dup2(pipefd[0], 0);//grep要从标准输入读取数据,这是就得将标准输入重定向到管道的读取端
        execl("/usr/bin/grep", "grep", "sshd", NULL);//替换程序grep sssh
        exit(0);

    }

    //父进程
    close(pipefd[0]);//父进程既不读也不写,将写端和读端关闭。
    close(pipefd[1]);
    waitpid(pid1, NULL, 0);
    waitpid(pid2, NULL, 0);
    return 0;

}
2.命名管道:
mkfifo 文件名.fifo  //命令操作
int mkfifo(const char* pathname,mode_t mode);//系统调用函数操作,创建命名管道文件
//pathname---管道文件名称
//mode---文件权限
//返回值---  0,成功; -1,失败

特性:若管道文件以只读方式代开,则会阻塞,直到这个管道文件文件被以的方式打开;
若管道文件以只只写方式代开,则会阻塞,直到这个管道文件文件被以的方式打开;
若管道文件以只读写方式代开,则不会阻塞。

//fifo_write ---命名管道写入端
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<string.h>
int main()
{
    umask(0);
    char* file = "./test.fifo";
    int ret = mkfifo(file,0664);

    if(ret < 0 && errno != EEXIST) 
    {
        perror("mkfifo error:");
        return -1;
    }

    printf("creat fifo success\n");

    int fd = open(file,O_WRONLY);
    if(fd < 0)
    {
        perror("open error:");
        return -1;
    }

    printf("open success\n");

    while(1)
    {
        char buf[1024] = {0};
        scanf("%s",buf);
        ret = write(fd,buf,strlen(buf));
        if(ret < 0)
        {
            perror("write error:");
            return -1;
        }
        printf("写入数据buf[%s]\n",buf);
    }
    close(fd);

    return 0;
}
//fifo_write ---命名管道读取端
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<string.h>
int main()
{
    umask(0);
    char* file = "./test.fifo";
    int ret = mkfifo(file,0664);

    if(ret < 0 && errno != EEXIST) 
    {
        perror("mkfifo error:");
        return -1;
    }

    printf("creat fifo success\n");

    int fd = open(file,O_RDONLY);
    if(fd < 0)
    {
        perror("open error:");
        return -1;
    }

    printf("open success\n");

    while(1)
    {
        char buf[1024] = {0};
        ret = read(fd,buf,1023);
        if(ret < 0)
        {
            perror("read error:");
            return -1;
        }
        else if(ret == 0)//read函数没有读到数据,返回0,说明所有写端被关闭
        {                                 
             printf("all write close\n");
            return -1;                                                       
        }
        printf("读取数据buf[%s]\n",buf);
    }
    close(fd);
    return 0;
}

所有管道的特性

  • 管道生命周期随进程:管道文件被创建后内核暂时不给他分配缓冲区,只有以读写方式打开之后才会分配,进程退出后,管道缓冲区也随之释放。
  • 半双工通信:数据只能在一个方向上移动
  • 提供字节流服务:有序,可靠,连接的字节传输,传输比较灵活。(比如传输较大的数据,会慢慢一点一点的传输)

五、共享内存(share memory)

1、简介

步骤:

  • 1、创建共享内存(开辟物理内存空间–具有标识符)
  • 2、将共享内存映射到各个进程的虚拟地址空间
  • 3、各个进程直接通过虚拟地址操作共享内存
  • 4、操作完毕,解除映射关系
  • 5、释放共享内存
    在这里插入图片描述
    最快的进程间通信方式:是因为共享内存直接通过虚拟地址映射直接访问内存,而其他方式都是内核中的缓冲区,通信都会涉及到用户态和内核态之间的两次数据拷贝,但是共享内存不需要,因此共享内存是最快的通信方式。

2、接口

1.系统调用接口
int shmget(key_t key,int size_t size,int flag);
//创建共享内存
//key---共享内存标识符,多个进程通过相同的标识符可以操作同一块共享内存
//size--共享内存大小
//flag---IPC_CREAT(不存在则创建,存在则打开)|IPC_EXCL(存在则报错,不存在则创建)|权限
//返回值---成功返回操作句柄,失败返回-1
void* shmat(int shmid,void* shmaddr,int shmflag);
//建立映射
//shmid---共享内存的操作句柄
//addr---映射到虚拟地址空间的首地址,通常置空
//shflag---通常置0,可读可写; SJM_RDONLY,只读
//返回值---成功返回映射的虚拟地址空间首地址,通过这个地址堆内存进行操作;失败返回-1。
int shmdt(void* shmstart);
//解除映射
//shmstart---映射到虚拟地址空间的首地址
//返回值---成功返回0,失败返回-1
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//操作共享内存
//shmid---操作句柄
//cmd---具体对共享内存要进行的操作---IPC_RMID-删除共享内存
2.命令
ipcs---查看共享内存信息
ipcm [shmid]---删除进程间通信资源
-m---查看共享内存
-q---查看消息队列
-s---查看信号量
当删除共享内存的时候,共享内存并不会被立即删除(因为有可能会造成正在访问的进程奔溃),而是将Key值修改为0,
状态改为(Dest)Destory表示这块内存将不再继续接受连接映射,当这块共享内存的映射连接数为0时,则自动会被释放。

nattch为当前连接这块共享内存进程的个数

在这里插入图片描述

//write_shm.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<string.h>
int main()
{
    umask(0);
    char* file = "./test.fifo";
    int ret = mkfifo(file,0664);

    if(ret < 0 && errno != EEXIST) 
    {
        perror("mkfifo error:");
        return -1;
    }

    printf("creat fifo success\n");

    int fd = open(file,O_WRONLY);
    if(fd < 0)
    {
        perror("open error:");
        return -1;
    }

    printf("open success\n");

    while(1)
    {
        char buf[1024] = {0};
        scanf("%s",buf);
        ret = write(fd,buf,strlen(buf));
        if(ret < 0)
        {
            perror("write error:");
            return -1;
        }
        printf("写入数据buf[%s]\n",buf);
    }
    close(fd);

    return 0;
}
//read_shm.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<string.h>
int main()
{
    umask(0);
    char* file = "./test.fifo";
    int ret = mkfifo(file,0664);

    if(ret < 0 && errno != EEXIST) 
    {
        perror("mkfifo error:");
        return -1;
    }

    printf("creat fifo success\n");

    int fd = open(file,O_RDONLY);
    if(fd < 0)
    {
        perror("open error:");
        return -1;
    }

    printf("open success\n");

    while(1)
    {
        char buf[1024] = {0};
        ret = read(fd,buf,1023);
        if(ret < 0)
        {
            perror("read error:");
            return -1;
        }
        printf("读取数据buf[%s]\n",buf);
    }
    close(fd);

    return 0;
}

3、特性

1、最快的进程间通信方式;
2、生命周期随内核(消息队列和信号量数组也是)
注意事项:共享内存的操作是不安全的,不会具备同步与互斥的关系,需要操作用户进行控制。

六、消息队列

1、本质

内核中的一个队列,多个进程通过向同一个队列添加节点和获取节点,传输一个有类型(优先级)的数据块。

 struct msgbuf
   {
       long mtype;       /* message type, must be > 0 ,优先级*/
       char mtext[1];    /* message data ,数据块大小*/
   };

2、接口

//创建消息队列
msgget();
//添加节点
msgsnd();
//获取节点
msgrcv();
//操作-删除消息队列(IPC_RMID)
msgctl();

3、特性:

  • 自带同步与互斥
  • 生命周期随内核
  • 数据传输自带优先级
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!