进程间通信

Posted 雨轩(爵丶迹)

tags:

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

进程间通信

进程间通信目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程。
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

进程间通信发展

  • 管道
  • System V进程间通信
  • POSIX进程间通信

进程间通信分类

管道

  • 匿名管道pipe
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

我们这里主要学习匿名管道,命名管道,共享内存,其他的内容后面了解。

管道

前言

前面说了进程间通信目的、发展、分类,现在我们先来说第一个发展,管道。

什么是管道?

现实中的管道:在日常生活中,我们可以理解就是一根运输资源的管道,至于运输的资源,可以是水、石油、天然气等。
Linux中的管道:我们把一个进程链接到另一个进程的一个数据流称为“管道”。管道是UNIX中最孤独老的进程间通信的方式。

进程间通信的本质:让不同的进程,看到同一份系统资源(这份系统资源是系统通过某种方式提供的系统内存)。

某种方式决定了通信策略的差异。那么系统到底如何进行通信?继续往下看。

匿名管道

函数原型

#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

前面我们学了基础IO,知道文件描述符的本质,我们先站在文件描述符角度,深刻理解管道。

  1. 我们这里可以看到3 和 4都指向了管道,文件描述符3对管道进行读,文件描述发4对管道进行写,从侧面我们可以知道管道也是文件
  2. 我们看到是父子进程在进行通信,所以匿名管道只能供具有血缘关系的的进程,进行进程间通信(常见于父子)。父进程fork出子进程后,让两个不同的进程,看到的是同一份系统资源(系统内存区域)。
  3. 管道只能进行单向数据通信。(父进程写,子进程读 或者 父进程读,子进程写)如果想双向数据通信,那么只能创建多个管道。

这里,我们可以看到创建一个pipe文件描述符数组后,拿到文件描述符3和4;

#include <stdio.h>    
#include <sys/types.h>    
#include <unistd.h>    
int main()                                                                                                                                                                  
    
  文件描述符数组
  int pipe_fds[2];    
  //创建失败    
  if(pipe(pipe_fds) == -1)    
      
    perror("pipe fail");    
    return -1;    
   
  拿到文件描述符   
  printf("%d, %d\\n",pipe_fds[0],pipe_fds[1]);    
    
  return 0;    
  

拿到的对应文件描述符,3对应read,4对应write。


所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”

接下来,我们就可以开始进行进程间的通信,fork出一个子进程,父进程读,子进程写。

#include <stdio.h>    
#include <sys/types.h>    
#include <string.h>    
#include <stdlib.h>    
#include <fcntl.h>    
int main()    
    
  int pipe_fds[2] =  0  ;    
  //创建失败    
  if(pipe(pipe_fds) == -1)    
      
    perror("pipe fail\\n");    
    return 1;    
      
  //打印对应文件描述符
  printf("%d, %d\\n",pipe_fds[0],pipe_fds[1]);    
    
  pid_t id = fork();//创建子进程    
  //fork失败    
  if(id < 0)    
      
    perror("fork fail\\n");    
    return 2;    
      
  else if(id == 0)    
      
    close(pipe_fds[0]);//关闭读,保证单向通信    
    const char *msg = "I am a child!!!";    
    //我们写入五次数据    
    int count = 5;    
    while(count--)    
        
      write(pipe_fds[1], msg, strlen(msg));//strlen(msg)不需要+1    
      sleep(1);    
        
    close(pipe_fds[1]);//发送完毕,需要关闭    
    exit(0);  //子进程任务完毕,退出  
      
    
  else    
      
    close(pipe_fds[1]);    
    char buffer[64];    
    while(1)    
        
      buffer[0] = 0; //先初始化为0
      ssize_t s = read(pipe_fds[0], buffer, sizeof(buffer) - 1);//注意这里是字节大小
      //C语言中,最后一位是'0'                                                                                                                                                                               
      if(s > 0)    
          
        buffer[s] = 0;//最后一位抹0    
        printf("parent get message from child : %s\\n", buffer);    
          
      else if(s == 0)    
          
        printf("child is quit!!!\\n");;    
        break;    
	  
      else
      
        break;
      
    
    //需要等待,不然成为僵尸进程都不知道
    int status = 0;
    //等待子进程退出
    if(waitpid(id, &status, 0) > 0)
    
      printf("child is quit success\\n");
    
    //不需要读取 关闭
    close(pipe_fds[0]);
  
  return 0;
                                            

从这里的结果,我们可以看到父进程一直在等待子进程,并读取到子进程写入的数据,这里就实现了管道,一个进程写入,一个进程读取,看到同一份资源。

我们在看看下面的图,加深理解!!!

这里提出几个问题

  1. 为什么曾经要打开文件描述符对应的读写?

答:如果不打开读写rw,子进程拿到的文件打开方式必定和父进程一样,无法进行通信(比如都只能读或者都只能写)。打开读写更加灵活(可以父进程读/写,子进程读/写)。

  1. 为什么一定要关闭对应的读写?

答:父进程读,子进程写,如果不关闭对应读/写,会发生误操作,比如父进程写,子进程也在写。

匿名管道的特性

  1. 如果管道里面没有消息,父进程(读端)在干什么?

答:等待,等待管道内部有数据就绪(子进程写入)。

  1. 如果管道里面写端已经写满了,继续写入,还能写吗?

答:当然不能,需要等待,等待管道内部有空闲空间(父进程读取)。

父进程读取,子进程写入,这里体现了进程同步。

特性:

  • 管道自带同步机制
  • 管道是单向通信的
  • 管道是面向字节流的(以字节形式读取或写入)
  • 管道只能保证是具有血缘关系的进程进行通信,常用于父子
  • 管道可以保证一定程度的数据读取的原子性(同时读/写)
  • 管道也是文件,随着进程退出,管道也会被关闭,生命周期随进程

我们在来看一下,如果一直写,不读或者关闭,会怎么样那?

读取关闭,当然是一直写入数据,一直写入毫无意义,本质就是在浪费系统资源。写进程会立马被OS终止掉。

一直写入,我们在来看看能写多久,也就是管道有多大?

我们这里可以看到写到4368就不写了,大约4KB的内容
Linux中规定也是4字节,这里可以通过man命令进行查看


综上,我们说了管道读写两个方面的内容。
总结一下:

read端write端结果
不读write阻塞
不写read阻塞
不读&关闭write被OS发送SIGPIPE杀掉
不写&关闭read读取到‘0’,文件结束

我们可以做个小实验,sleep也可以创建匿名管道

[dy@VM-12-10-centos noname]$ sleep 1000 | sleep 2000 | sleep 3000 &
[1] 19253
这里可以看到三个进程的父进程pid是一样的,所以他们是兄弟进程,符合匿名管道的特性
[dy@VM-12-10-centos noname]$ ps axj | head -1 && ps axj | grep sleep
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13679 19251 19251  3359 pts/0    19330 S     1003   0:00 sleep 1000
13679 19252 19251  3359 pts/0    19330 S     1003   0:00 sleep 2000
13679 19253 19251  3359 pts/0    19330 S     1003   0:00 sleep 3000
 1498 19310  1381  1381 ?           -1 S        0   0:00 sleep 60
13679 19331 19330  3359 pts/0    19330 R+    1003   0:00 grep --color=auto sleep

底层for循环创建出三个sleep

命名管道

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

原理:

  1. 创建一个文件,让进程1打开这个文件,进程2页打开这个文件,使用的是同一路径下的文件
  2. 当然创建的文件不能是磁盘上的普通文件,而是管道文件,管道文件不存在磁盘上,进程1读,进程2写,这时管道文件存在内存中,需要对应的数据去对应的缓冲区中拿即可

命名管道的创建

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

我们创建了如何去查看这个管道文件的内容?我们先写一段命令将数据写入,并查看该文件

持续写入的命令:
while :; do echo "hello bit"; sleep 1; done > myfifo
查看命令:
cat myfifo


这里的命令实现了两个进程简单的通信

  1. 命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);

创建命名管道:

int main(int argc, char *argv[])

	mkfifo("p2", 0644);//对应名称、权限
	return 0;

匿名管道与命名管道的区别

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

命名管道的打开规则

如果当前打开操作是为读而打开FIFO时

  • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
  • O_NONBLOCK enable:立刻返回成功

如果当前打开操作是为写而打开FIFO时

  • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
  • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

用命名管道实现server&client通信

server.c
#include <stdio.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
#define FIFO "./fifo"    
int main()    
    
  int ret = mkfifo(FIFO, 0644);//目录,权限    
  if(ret < 0)    
      
    perror("mkfifo fail");    
    return 1;    
      
  //管道创建好,后面就是文件操作    
  int fd = open(FIFO, O_RDONLY);    
  if(fd < 0)    
      
    perror("open fail");    
    return 2;    
      
  char buffer[128];    
  while(1)    
      
    printf("Server# ");    
    fflush(stdout);//缓冲区中,需要强制刷新    
    buffer[0] = 0;//初始化    
    ssize_t s = read(fd, buffer, sizeof(buffer) - 1);    
    if(s > 0)    
        
      buffer[s] = 0;//最后一位置'0'    
      printf("Client#: %s\\n",buffer);    
        
    else if(s == 0)    
        
      printf("client quit!!!\\n");//终止情况                                                       
      break;    
        
    else    
        
      break;    
        
      
  close(fd);//打开记得关闭     
  return 0;    
   
client.c
#include <stdio.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
#define FIFO "./fifo"    
#include <string.h>    
int main()    
    
  //server中已经创建好管道文件,这里就不需要在去创建    
  int fd = open(FIFO, O_WRONLY);    
  if(fd < 0)    
      
    perror("open fail");    
    return 2;    
      
  char buffer[128];    
  while(1)    
      
    printf("Please Enter# ");    
    fflush(stdout);//缓冲区中,需要强制刷新    
    buffer[0] = 0;//初始化    
    //最后一位为'0',C语言本质,文件不管终止符                                                    
    ssize_t s = read(0, buffer, sizeof(buffer) - 1);//此时从输入流读取数据    
    if(s > 0)    
        
      buffer[s] = 0;//最后一位置'0'    
      write(fd, buffer, strlen(buffer));    
        
    else if(s == 0)    
        
      printf("client quit!!!\\n");//终止情况    
      break;    
        
    else    
        
      break;    
        
      
  close(fd);//打开记得关闭     
  return 0;    
  

我们可以看到client进程写入,server进程读取,这就是通过程序完成命名管道的创建。其实创建好管道文件好,后面的内容就是基础IO里面的打开读写等操作。

例外我们可以清楚的看到管道文件的大小是0,充分证明了管道文件不存在磁盘上,而是系统的内存区域。

system V共享内存

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

原理:共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存。

共享内存数据结构

在系统当中可能会有大量的进程在进行通信,因此系统当中就可能存在大量的共享内存,那么操作系统必然要对其进行管理,所以共享内存除了在内存当中真正开辟空间之外,系统一定还要为共享内存维护相关的内核数据结构。

共享内存的数据结构如下:

struct shmid_ds 
	struct ipc_perm shm_perm; /* operation perms */
	int shm_segsz; /* size of segment (bytes) */
	__kernel_time_t shm_atime; /* last attach time */
	__kernel_time_t shm_dtime; /* last detach time */
	__kernel_time_t shm_ctime; /* last change time */
	__kernel_ipc_pid_t shm_cpid; /* pid of creator */
	__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
	unsigned short shm_nattch; /* no. of current attaches */
	unsigned short shm_unused; /* compatibility */
	void *shm_unused2; /* ditto - used by DIPC */
	void *shm_unused3; /* unused */
;

当我们申请了一块共享内存后,为了让要实现通信的进程能够看到同一个共享内存,因此每一个共享内存被申请时都有一个key值,这个key值用于标识系统中共享内存的唯一性。
可以看到上面共享内存数据结构的第一个成员是shm_perm,shm_perm是一个ipc_perm类型的结构体变量,每个共享内存的key值存储在shm_perm这个结构体变量当中,其中ipc_perm结构体的定义如下:

struct ipc_perm
	__kernel_key_t  key;
	__kernel_uid_t  uid;
	__kernel_gid_t  gid;
	__kernel_uid_t  cuid;
	__kernel_gid_t  cgid;
	__kernel_mode_t mode;
	unsigned short  seq;
;

共享内存的建立与释放

共享内存的建立大致包括以下两个过程:

  • 在物理内存当中申请共享内存空间。
  • 将申请到的共享内存挂接到地址空间,即建立映射关系。

共享内存的释放大致包括以下两个过程:

  • 将共享内存与地址空间去关联,即取消映射关系。
  • 释放共享内存空间,即将物理内存归还给系统。

共享内存创建

shmget函数

功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
参数
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

我们把具有标定某种资源能力的东西叫做句柄,而这里shmget函数的返回值实际上就是共享内存的句柄,这个句柄可以在用户层标识共享内存,当共享内存被创建后,我们在后续使用共享内存的相关接口时,都是需要通过这个句柄对指定共享内存进行各种操作。

传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取

ftok函数的函数原型如下:

key_t ftok(const char *pathname, int proj_id);

ftok函数的作用就是,将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值,在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中。需要注意的是,pathname所指定的文件必须存在且可存取。

注意:

  • 使用ftok函数生成key值可能会产生冲突,此时可以对传入ftok函数的参数进行修改。
  • 需要进行通信的各个进程,在使用ftok函数获取key值时,都需要采用同样的路径名和和整数标识符,进而生成同一种key值,然后才能找到同一个共享资源

传入shmget函数的第三个参数shmflg,常用的组合方式有以下两种:

组合方式作用
IPC_CREAT如果内核中不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则直接返回该共享内存的句柄
IPC_CREAT IPC_EXCL如果内核中不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则出错返回
  • 使用组合IPC_CREAT,一定会获得一个共享内存的句柄,但无法确认该共享内存是否是新建的共享内存。
  • 使用组合IPC_CREAT | IPC_EXCL,只有shmget函数调用成功时才会获得共享内存的句柄,并且该共享内存一定是新建的共享内存。

现在我们可以使用shmget和ftok函数创建一块共享内存了,代码,运行截图如下:

#include <stdio.h>    
#include <sys/types.h>    
#include <sys/ipc.h>    
#include <sys/shm.h>    
#define PATHNAME "./tmp" //路径    
#define PROJ_ID 0x6666    
int main()    
    
  key_t k = ftok(PATHNAME, PROJ_ID);    
  if(k < 0)    
      
    perror("ftok");    
    return 1;    
      
    
  int shmid = shmget(k, 4096, IPC_CREAT);    
  if(shmid < 0)    
      
    perror("shmget");    
    return 2;    
      
  printf("key:%x\\n", k);                                                                                                                                                                                                                                             
  printf("shmid:%d\\n",shmid);    
  return 0;    
001:进程间通信类型

进程间通信和线程间通信

进程间通信

进程间通信方式

进程间,线程间通信方式

8种进程间通信方式