进程间通信(IPC)

Posted 正义的伙伴啊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进程间通信(IPC)相关的知识,希望对你有一定的参考价值。

进程间通信(Inter-Process Communication)简称IPC,下面介绍两种IPC方式:

  • 管道
  • system V

文章目录

管道

什么是管道

以前我们学过管道操作,具体可以参考:shell
管道通过一系列操作使一个命令的输出作为另一个命令的输入数据,由于命令在系统看来都是一个个的进程,所以管道在系统层面是进程间的通信(就是两个进程间相互交换数据)。那么如何实现 两个进程之间实现通信?
管道的本质 让管道的读写段分别看到同一块资源

  • 如何描述这一块资源?
    用文件来表示这块资源,但是这个文件属性与普通文件不同,是管道文件文件属性显示的是p(也就是pipe的简写)
  • 如何让多个文件共享这一个文件
    不同的文件用不同的方式打开这个文件,对这个文件进行读或写就实现了“共享”

    注意
  • 一个文件可以被多个进程打开,也可以被一个进程打开多次(这样一个文件就可能对应多个文件描述符!)
  • 管道只支持单向数据传输,如果想要实现双向数据传输可以建立两个管道

管道的分类:

  • 匿名管道:不可见
  • 命名管道:可见,文件属性是p管道类型文件

匿名管道

匿名管道的概念

匿名管道也是进程间通信的一种,但是通信的进程之间存在父子关系,通俗的解释是有亲缘关系进程之间的通信
匿名管道运用的原理的是 :子进程在被创建的时候会默认打开父进程中所有的文件(文件描述符的继承)。这也是为什么所有进程都默认打开了三个标准输入、输出、错误,只要最原始的进程打开了这三个,他的所有子进程都会打开这三个文件。

匿名管道的原理

  • 首先父进程用读和写的方式打开管道文件,这时候会产生两个文件描述符,我们用一个数组fd[2]存储,fd[0]代表读,fd[1]代表写
  • 接下来创建子进程由于子进程是按照父进程继承下来,所以fd[0]fd[1]也会在子进程中默认被打开
  • 最后根据自己数据传输方向关闭父进程中不用的读端或写段(也可以不关闭,但是管道文件只能单向传输)

pipe(int fd[2])

#include <unistd.h>
int pipe(int pipefd[2]);

参数含义
这里pipefd[2]是一个输出型参数,存储的是文件描述符,输出的pipefd[2]pipefd[0]代表的是读端,pipefd[1]代表的是写端

这个函数执行完成之后会创建一个管道文件(不可见),下面就可以创建子进程了,我们让子进程不断写入管道,父进程从管道文件中读取:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()

  int fd[2];
  pipe(fd); // fd[0]->read  fd[1]->write
  int ret=fork();
  if(ret==0)  //子进程
  
    close(fd[0]);
    const char buffer[20]="i am a idiot";
    int count=0;
    while(1)
    
      sleep(1);
      write(fd[1],buffer,strlen(buffer));
      printf("child write %d\\n",++count);

    
    return 0;
  
  else // 父进程
  
    close(fd[1]);
    char buffer[64];
    int sz=1;
    while(sz)
    
      sleep(1);
      sz=read(fd[0],buffer,sizeof(buffer)-1);
      buffer[sz]='\\0';
      printf("father read message: %s\\n",buffer);
    
    return 0;
  

读慢写快

如果我们让子进程写入的速度远大于父进程从管道读取的速度,会发生什么结果?

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()

  int fd[2];
  pipe(fd); // fd[0]->read  fd[1]->write
  int ret=fork();

  if(ret==0)  //子进程
  
    close(fd[0]);
    const char buffer[20]="i am a idiot";
    int count=0;
    while(1)
    
      sleep(1);  //每隔1s向管道写入一次
      write(fd[1],buffer,strlen(buffer));
      printf("child write %d\\n",++count);
    
    return 0;
  
  else // 父进程
  
    close(fd[1]);
    char buffer[64];
    int sz=1;
    while(sz)
    
      sleep(5);  //没隔5s从管道读取一次
      sz=read(fd[0],buffer,sizeof(buffer)-1);
      buffer[sz]='\\0';
      printf("father read message: %s\\n",buffer);
    
    return 0;
  


结果是
管道直接被写满之后,才开始读取。但是注意在读取的时候写入端停止写入被阻塞,这就是写快读慢的后果写入端阻塞等待读取管道内容

读快写慢

如果我们让子进程写入的速度小于父进程读取管道的速度结果会怎么样?

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()

  int fd[2];
  pipe(fd); // fd[0]->read  fd[1]->write
  int ret=fork();

  if(ret==0)  //子进程
  
    close(fd[0]);
    const char buffer[20]="i am a idiot";
    int count=0;
    while(1)
    
      sleep(5);    //每隔5s向管道写入一次
      write(fd[1],buffer,strlen(buffer));
      printf("child write %d\\n",++count);
    
    return 0;
  
  else // 父进程
  
    close(fd[1]);
    char buffer[64];
    int sz=1;
    while(sz)
    
      sleep(1);    //没隔1s从管道读取一次
      sz=read(fd[0],buffer,sizeof(buffer)-1);
      buffer[sz]='\\0';
      printf("father read message: %s\\n",buffer);
    
    return 0;
  


结果是
虽然读取管道的速度远大于写入的速度(读端每隔1s读取一次),但是读端最终还是每隔5s读取一次,这中间的四秒被阻塞等待管道另一段写入,所以如果写端不关闭文件描述符且不写入,读端可能要长时间阻塞

写段关闭,读段不关闭

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()

  int fd[2];
  pipe(fd); // fd[0]->read  fd[1]->write
  int ret=fork();

  if(ret==0)  //子进程
  
    close(fd[0]);
    const char buffer[20]="i am a idiot";
    int count=0;
    while(1)
    
      write(fd[1],buffer,strlen(buffer));
      printf("child write %d\\n",++count);
      if(count==5)  //写端写入5次后就退出
      
        close(fd[1]);
        _exit(0);
      

    
    return 0;
  
  else // 父进程
  
    close(fd[1]);
    char buffer[64];
    int sz=1;
    while(sz)
    
      sz=read(fd[0],buffer,sizeof(buffer)-1);
      if(sz==0)
      
        printf("father read nothing!  \\n");
        continue;
      
      buffer[sz]='\\0';
      printf("father read message: %s\\n",buffer);
    
    return 0;
  




结果是
写端关闭文件描述符,读端会读取到文件结尾(也就是read的返回值为0的时候就是读到管道结尾)

读端关闭,写段不关闭

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>

int main()

  int fd[2];
  pipe(fd); // fd[0]->read  fd[1]->write
  int ret=fork();
  if(ret==0)  //子进程
  
    close(fd[0]);
    const char buffer[20]="i am a idiot";
    int count=0;
    while(1)
    
      sleep(1);
      write(fd[1],buffer,strlen(buffer));
      printf("child write %d\\n",++count);
    
    return 0;
  
  else // 父进程
  
    close(fd[1]);
    char buffer[64];
    int sz=1;
    int count=0;
    while(sz)
    
      sleep(1);
      sz=read(fd[0],buffer,sizeof(buffer)-1);
      if(sz==0)
      
        printf("father read nothing!  \\n");
        continue;
      
      buffer[sz]='\\0';
      printf("father read message: %s\\n",buffer);
      if(count++==5)
      
        close(fd[0]);
        break;
      
    
    int status=0;
    waitpid(ret,&status,0); //回收子进程的终止信号
    printf("子进程的终止信号是:%d\\n",status & 0x7f);
    return 0;
  



结果是
如果读端关闭,写端可能直接被操作系统终止掉(如果你用ps命令一直监视a.out的两个进程你会发现一个进程会直接终止),由于这是异常终止所以一定是操作系统发送了信号将进程杀死,所以我们用waitpid来拿到子进程的返回值并打印出来(如上图)

对照命令列表:发现SIGPIPE信号将进程杀死

命名管道

命名管道的

server.c

#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()

  int fd=open("./FIFO",O_RDONLY);
  int sz=1;
  char buffer[64];
  while(sz)
  
    sz=read(fd,buffer,sizeof(buffer)-1);
    if(sz>0)
    
      buffer[sz]='\\0';
      printf(">: %s",buffer);
    
    else if(sz==0)
    
      printf("connect done!\\n");
    
    else 
    
      printf("connect error!\\n");
    
  

  return 0;


client.c

#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()

  mkfifo("./FIFO",0777);
  int fd=open("./FIFO",O_WRONLY);
  char buffer[64];
  int sz=0;
  while(1)
  
    printf(">: ");
    fflush(stdout);
    sz=read(1,buffer,64);
    write(fd,buffer,sz);
  
  return 0;



makefile

.PHONY:all  #由于makefile不能同时编译两个文件,这里用一个伪对象
all:server client

server:server.c
	gcc -o $@ $^
client:client.c
	gcc -o $@ $^

.PHONY:clean
clean:
	rm -rf server client FIFO

实现

你在左边输入右边的进程就会把你输入的打出来:

system V 共享内存

什么是共享内存?

共享内存也是一种进程间通信的方式,但是共享内存和文件系统没有半毛钱关系,而是直接在内存上进行数据通信不依赖文件系统

os如何实现共享内存?


这是我们虚拟地址空间的示意图,图中在堆栈相向之间存在一片共享内存区域,这块区域可以加载共享库,同时还可以加载共享内存,所以我们就可以在内存上开辟一块公共区域,让需要通信的进程的虚拟进程地址的共享内存区域通过页表挂载到这片公共区域上,从而实现进程间通信

为什么要用共享内存?

如果你自己去写一遍上面命名管道的代码你就会发现命名管道需要一个缓冲区buffer[]来将数据从管道文件拷贝出来/进去,而共享内存因为不涉及文件系统直接在内存上进行通信,所以访问共享内存区域和访问进程独有的内存区域一样快,并不需要通过系统调用或者其它需要切入内核的过程来完成。同时它也避免了对数据的各种不必要的复制。

如何使用命令查看共享内存

ipcs——查看

  • -m:查看共享内存
  • -q:查看消息队列

我们这里输入ipcs -m就可以查看共享内存的情况:

ipcrm——删除

  • -M:加上想要删除的共享内存的key值
  • -m :加上想要删除的共享内存的shmid值

如何使用共享内存?

上面的图中我们会有以下几个问题没有解决:

  • 内存中 有多个共享内存,操作系统如何描述和管理这些共享内存?
  • 如何标识共享内存让两个不同的进程都挂接到同一个共享内存?
    我们从下面四个方面来学习

创建共享内存

查看共享内存的内核代码,操作系统是用shmid_ds结构体来描述一个共享内存的,同时这个数据结构被宏定义作为一个接口暴露给用户,来查看、直接修改共享进程的信息,这个结构体被包含在头文件include/linux/shm.h中,所以你的代码只要出现#include<shm.h>就可以定义出这么一个结构体!

	struct shmid_ds
		struct ipc_perm shm_perm;				/* 操作权限*/
		int shm_segsz;             				/*段的大小(以字节为单位)*/
		time_t shm_atime;          				/*最后一个进程附加到该段的时间*/
		time_t shm_dtime;          				/*最后一个进程离开该段的时间*/
		time_t shm_ctime;          				/*最后一个进程修改该段的时间*/
		unsigned short shm_cpid;   				/*创建该段进程的pid*/
		unsigned short shm_lpid;  				/*在该段上操作的最后1个进程的pid*/
		short shm_nattch;          				/*当前附加到该段的进程的个数*/
		unsigned short shm_npages;  			/*段的大小(以页为单位)*/
		unsigned long *shm_pages;   			/*指向frames->SHMMAX的指针数组*/
		struct vm_area_struct *attaches;	 	/*对共享段的描述*/
	;

其中struct ipc_perm shm_perm; /* 操作权限*/这个尤为重要,这个记录的是共享内存的属性(里面的key值是os用来唯一标识一个共享内存的标识,具有唯一性):

	struct ipc_perm 
	   key_t          __key;    /* 操作系统用来标识共享内存的标识*/
	   uid_t          uid;      /* Effective UID of owner */
	   gid_t          gid;      /* Effective GID of owner */
	   uid_t          cuid;     /* Effective UID of creator */
	   gid_t          cgid;     /* Effective GID of creator */
	   unsigned short mode;     /* Permissions + SHM_DEST and SHM_LOCKED flags */
	   unsigned short __seq;    /* Sequence number */
	;

所以你想要建立一个共享内存就需要一个key值来让操作系统标记这个共享内存,这里就需要介绍一个函数用来生成具有唯一性的key值:

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

函数的参数要求是一个是字符串,一个是一个整型(非零),返回值就是key值。注意这两个参数如果相同的话产

以上是关于进程间通信(IPC)的主要内容,如果未能解决你的问题,请参考以下文章

IPC进程通信

进程间通信IPC之--共享内存

进程间通信(IPC)

Linux 进程间通信(IPC)

计算机基础OS 进程间通信(IPC)机制介绍

「转」进程间通信IPC (InterProcess Communication)