linux进程间通信

Posted 可乐不解渴

tags:

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

等你穿过暴风雨,你就不再是原来的你。

linux进程间通信


进程间通信,为什么需要通信,这是因为想让进程间实现数据传输、资源共享、通知事件以及进程控制。
而每一个进程想要访问物理内存,都是通过访问进程虚拟地址空间 **(mm_struct)**以及页表,每次访问内存时,通过页表来组织进行虚拟地址以及物理地址的映射关系。这虽然会给每一个进程变的具有独立性,在运行时不会互相干扰,但是这就造成了进程与进程之间互相通信困难。
而在进程间通信的常用方式中有如下几种:管道、共享内存等。
进程间通信的本质就是让不同的进程来看到同一份资源(内存、文件内核缓冲等等)。

管道

那么什么是管道呢?
答:管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。在linux中,一个 **|**符号就代表的是管道。

管道的本质是内核当中的一块系统提供的缓冲区,该缓冲区供不同的进程进行读写信息来进程进程间通信。
而管道又分匿名管道命名管道两种。

匿名管道

匿名管道的创建

我们利用pipe函数来创建管道。
函数原型:int pipe(int pipefd[2]);
头文件:#include <unistd.h>
函数参数:
pipefd:填入文件描述符数组,其中fd[0]表示读端, fd[1]表示写端。
**函数返回值:**成功返回0,失败返回-1。
注意: fd[0]表示的是读端,fd[1]表示写端,不能搞混淆。

站再文件描述符的角度----深度理解管道。

下面我们来利用匿名管道来实现父子进程间通信,父进程写数据,子进程读数据。

#include<iostream>                                                                                                                                       
#include<stdlib.h>
#include<string.h>               
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>  
using namespace std;
                 
int main()             
                                                      
  int fd[2]=0;        
  if(pipe(fd)<0)         //创建管道
                    
    cerr<<"pipe error"<<endl;
    return 1;
         
       
  pid_t id=fork();//创建子进程
  if(id<0)                               
                                    
    cerr<<"fork error"<<endl;
    return 2;
                                    
  else if(id==0)
      
    close(fd[0]);                     //关闭对应的读端      
    char buff[100];
    ssize_t  size=read(fd[0],buff,sizeof(buff)-1);//子进程读数据
    buff[size]='\\0';
    cout<<buff<<endl;            
    close(fd[0]);
    exit(0);
  
  else
  
    close(fd[1]); //关闭对应的写端
    const char * str = "I am Father";
    write(fd[1],str,strlen(str)); //父进程写数据
    close(fd[1]); 
  
  pid_t pid =waitpid(-1,NULL,0); //等待子进程,父进程回收子进程的资源
  if(pid>0)
  
   cout<<"wait child success"<<endl;
  
  else
  
    cout<<"wait error"<<endl;
  
  return 0;
  


再这里的表现看的不明显,这效果和正常的程序输出一个语句没区别,但这里本质还是父子两个进程间进行的通信。

命名管道

匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件。

命名管道的创建

1、命名管道可以从命令行上创建,命令行方法是使用下面这个命令:

mkfifo filename

2、命名管道也可以从程序里创建,相关函数有:

int mkfifo(const char *pathname, mode_t mode);

头文件为:
#include <sys/types.h>
#include <sys/stat.h>
函数参数:
pathname:将管道文件创建到那个路径下。
mode:表示的是创建管道文件的权限。(8进制)
函数返回值:
成功返回0,失败返回-1。

下面我们以一段代码为例,用命名管道来实现客户端与服务端的通信。
其中comm.h这个头文件是client.c和server.c两个源文件共同包含的头文件。
comm.h

#pragma once
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#define FILE_NAME "fifo"

服务端用来创建命名管道以及读client端发来的数据。
server.c

#include"comm.h"
int main()

  if(mkfifo(FILE_NAME,0644)<0)
  
    perror("mkfifo error\\n");
    return 1;
  
  int fd=open(FILE_NAME,O_RDONLY);  
  if(fd<0)
  
    perror("open error\\n");
    return 2;
  
  char msg[128];
  while(1)
  
    ssize_t size=read(fd,msg,sizeof(msg)-1);
    if(size>0)
    
      msg[size]='\\0';
      printf("client### %s\\n",msg); 
    
    else if(size==0)
    
      printf("client quiet\\n");
      break;
    
    else
    
      printf("read over\\n");
      break;
    
  

  close(fd);
  return 0;


client端打开管道文件,并写入数据到管道文件中。
client

#include"comm.h"
int main()

  int fd=open(FILE_NAME,O_WRONLY);
  if(fd<0)
  
    perror("open error\\n");
    return 1;
  
  char msg[128];
  while(1)
  
    printf("Please Write  ");
    fflush(stdout);
    ssize_t size=read(0,msg,sizeof(msg));
    if(size>0)
    
      msg[size-1]='\\0';
      //往管道里写
      write(fd,msg,strlen(msg));
    
  
  close(fd);
  return 0;


在这里我们就能很明显的看到了两个进程间的通信的效果了,其中client端给服务器在发生信息,server端来读取client端发送过来的信息,并将其打到显示屏中。

匿名管道与命名管道的区别:
1、匿名管道由pipe函数创建并打开。
2、命名管道由mkfifo函数创建,打开用open。
3、FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。

管道的读写规则

1、写端不写,读端一直读,读端会被阻塞。
2、读端不读,一直写,写端会被阻塞。
3、写端写完,关闭写端,读端就会读到返回值0,代表着读取文件结束。
4、读端关闭,一直写,写端会被操作系统立即杀掉,因为此时写入无意义了,由于没有进程去读取里面的数据。

管道的特点

  • 匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务。
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。

SYSTEM V 共享内存

共享内存原理

1、在物理内存共享区中开辟一段空间。
2、不同的进程通过操作自己进程虚拟地址空间当中的虚拟地址,通过各自的用户级页表将该空间映射到自己的进程虚拟地址空间当中,进而来读取或写入数据到共享内存当中。

共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。

共享内存示意图

共享内存函数

ftok

函数原型: key_t ftok(const char *pathname, int proj_id);
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
函数功能:
首先手动生成唯一一个标识,利用ftok函数来得到一个唯一的一个key值,来给shmget函数来创建共享内存。
函数参数:
pathname:这个参数的路径必须要存在,存在的路径可以随便设置。
proj_id:这个参数也可以随便设置。
函数返回值:
返回一个唯一的key值,返回值key_t类型本质就是int类型,失败返回-1。

shmget

函数原型: int shmget(key_t key, size_t size, int shmflg);
头文件:
#include <sys/shm.h>
#include <sys/ipc.h>
函数功能: 创建共享内存,并给该共享内存设置权限及空间大小。
函数参数:
key:这个参数用来填充ftok函数返回生成的key值。
size:这个参数用来设置共享内存的大小,并且这个共享空间大小分配是以页来分配的。一页的空间的大小为(4096字节),所以我们设置最好是以4096的整数倍来设置。
shmflg:这个参数类似于open的O_CREAT等。
选项:
IPC_CREAT:如果共享内存存在直接返回该共享内存;如果不存在,则先创建在返回。
IPC_EXCL:该选项单独使用无意义。
当IPC_CREAT | IPC_EXCL 选项组合起来使用的含义为:先去查找当这个key对应的共享内存存在时,则出错;
如果不存在就会创建一个新的共享内这样就能够保证我们创建的共享内存一定是最新生成的。
如要给这个共享内存设置权限需要 | 上一个八进制的数字,如下图所示:

函数返回值:
返回一个唯一的int类型的变量,该变量用来标识共享内存,失败返回-1。

shmat

函数原型: void *shmat(int shmid, const void *shmaddr, int shmflg);
函数头文件:
#include <sys/types.h>
#include <sys/shm.h>
函数功能:
该函数用来关联到共享内存, 将共享内存段连接到进程地址空间 。
函数参数:
shmid:该参数传入的是刚刚shmget函数的返回值,用来标识唯一的共享内存。
shmaddr:该参数表示的是指定连接的虚拟空间,默认传入NULL,OS就会自动分配。
函数返回值:
成功返回一个指针,指向共享内存第一个节;失败返回-1。

shmdt

函数原型: int shmdt(const void *shmaddr);
函数头文件:
#include <sys/types.h>
#include <sys/shm.h>
函数功能:
该函数用来将共享内存段与当前进程脱离。
函数参数:
shmaddr:填入shmat的返回值。
函数返回值:
成功返回0, 失败返回-1。
注意:将共享内存段与当前进程脱离不等于删除共享内存段。

shmctl

函数原型: int shmctl(int shmid, int cmd, struct shmid_ds *buf);
函数头文件:
#include <sys/ipc.h>
#include <sys/shm.h>
函数功能:
该函数用于控制共享内存。
函数参数:
shmid:由shmget返回的共享内存标识码
cmd:该参数有三个选项如下:
选项
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值。
IPC_RMID:删除共享内存段。
IPC_SET:在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值。
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构返回值:
函数返回值:
成功返回0;失败返回-1。

同上文一样,用共享内存来实现客户端与服务端的通信。
其中comm.h这个头文件是client.c和server.c两个源文件共同包含的头文件。

#pragma once
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include<sys/shm.h>
#include<unistd.h>
#define PATH_NAME "/home/ZJ/code/10/1027/shm" 
#define PROJ_ID 0x6666
#define SIZE 1000

client.c

#include"comm.h"

int main()

  key_t key=ftok(PATH_NAME,PROJ_ID);
  if(key<0)
  
    perror("ftok error\\n");
    return 1;
  

  int shmid=shmget(key,SIZE,IPC_CREAT);

  if(shmid<0)
  
    perror("shmget error\\n");
    return 2;
  
  
  char*str=(char*)shmat(shmid,NULL,0);
  
  char c='a';
  for(;c<='z';++c)
  
    str[c-'a']=c;
    if(c=='c')
    
      ++c;
      str[c-'a']='\\0';
    
    sleep(3);
  
  shmdt(str);
  return 0;


server.c

#include"comm.h"
int main()

  //1、首先手动生成唯一一个标识
  //这个参数的路径必须要存在,存在的路径可以随便设置
  //PROJ_ID也可以随便设置
  key_t key=ftok(PATH_NAME,PROJ_ID);
  if(key<0)
  
    perror("ftok error\\n");
    return 1;
  
  printf("%x",key);
  //2、生成共享内存,权限为666,
  //选项组合含义为:当这个key对应的共享内存存在时,则出错,否则创建一个新的共享内存
  //这样就能够保证我们创建的共享内存一定是最新生成的
  int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);
  if(shmid<0)
  
      perror("shmget error\\n");
      return 2;
  

  //3、利用shmat函数来关联到刚刚创建的共享内存,返回的是共享内存的起始地址
  //第二个参数代表的是:你自己并不知道你要映射到那个区,所有设置为NULL,让系统默认映射
  //第三个参数是:关联这个共享内存给这个共享内存设置某些属性,填入0表示使用默认
  char *s =(char*) shmat(shmid,NULL,0);
  
  while(1)
  
    printf("%s\\n",s);
    sleep(1);
  
  //4、利用shmdt函数来去关联到刚刚创建的共享内存,成功返回0,失败返回-1
  shmdt(s);
  sleep(10);

  //5、利用shmctl函数来删除创建的共享内存,如果不去主动删除,该共享内存会一直存在,直到服务器内核被重启
  shmctl(shmid,IPC_RMID,NULL);
  sleep(10);
  return 0;


在这上面的代码中的ftok不要和我们创建子进程函数fork函数弄混淆。且要实现两个进程通过共享内存来实现通信,两个进程中ftok的函数中传进去的两个参数的值必须相同,这样这两个进程就会拿到同一个key值来找到同一块共享内存。
client端给server端每隔3秒发送一个字符,但我们会发现,client端还为写入新的数据时,server端并没有阻塞去等待写入,而是直接读出之前的数据。这也是为什么共享内存是最快的原因之一,因为它底层没有同步与互斥的机制

结果如下:

我们可以利用ipcs命令来查看共享内存、消息队列、信号量的详细信息。

若指向查询它们其中一个。
1、只查询消息队列:ipcs -q

指定删除消息队列 ipcrm -q +msqid,这个msqid可以利用ipcs命令来查看。
2、只查询共享内存:ipcs -m

删除共享内存:ipcrm -m shmid

3、只查询信号量:ipcs -s

删除信号量:ipcrm -s semid

注意:
共享内存底层不提供任何同步与互斥机制。
共享内存是随内核的,其生命周期是就是随内核的。
并且读写共享内存的时候,并没有使用OS接口。

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

简述Linux进程间通信的几种方式

Linux进程间通信简介

深刻理解Linux进程间通信(IPC)

深刻理解Linux进程间通信(IPC)

Linux进程通信之匿名管道

Linux 进程间通信