【Linux】进程间通信

今天我们来讲解一下进程间的通信。

1. 进程间通信介绍

1.1 进程间通信目的

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

1.2 进程间通信的发展

进程通信的发展分为三个阶段;

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

其中前两种是针对本地设备之间的进程通信,第三种可以通过网络实现跨设备的进程通信。今天我们只学习后两种,第三种等我们接触了网络之后再讲解。

不管是哪一种通信方式,实际上我们要做的都是 保证不同的进程,看到同一份资源。关于这一观点,在本文中,我会多次强调。

1.3 进程间通信分类

  1. 管道
  • 匿名管道
  • 命名管道
  1. System V IPC
  • System V 消息队列
  • System V 共享队列
  • System V 信号量
  1. POSIX IPC
  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

2. 管道

2.1 什么是管道

管道是 UNIX 中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
在这里插入图片描述
简单来说,管道就是一个文件,并且以数据的传输为目的而存在。


2.2 匿名管道

匿名管道 的作用是 供具有血缘关系的进程之间进行通信,常见于父子进程之间。

我们先来回忆一下我们之前讲过的IO 文件系统:
【Linux】基础IO详解

其中我们讲了 进程 与 文件之间的关系以及 进程找到文件的原理。管道的原理与这一块内容是强相关的。

2.2.1 创建管道

  1. 父进程通过 struct files_struct内部的fd_array数组 找到 一个 struct file (文件结构体)(这个文件是pipe_file,并不是随意的一个文件),分别以读方式打开一次,写方式再打开一次。
    在这里插入图片描述

  2. 父进程 fork出子进程,那么很显然,子进程也会对该文件分别以读方式打开一次,写方式再打开一次。

在这里插入图片描述

  1. 父进程关闭 fd[0],子进程关闭fd[1],也就是保留父进程的写段,子进程的读端,这样父进程就可以通过管道向子进程传输信息。

在这里插入图片描述
那么这里就有两个问题

  1. 为什么父进程开始的时候要打开两次文件?
    如果不打开读写。那么子进程拿到的文件打开方式必定与父进程的相同,无法通信。同时也更加灵活,父子进程可以选择作为输入还是输出端。

  2. 为什么一定要关闭一个,不关闭的话不是可以互相传输了吗?
    防止误操作


2.2.2 匿名管道创建接口 pipe

int pipe (int pipefd[2]);

其中数组pipefd是一个输出型参数,它存储这拿到的管道文件的文件描述符fd, 两个fd 一个read,一个write.

对于该函数,默认fd[0] 打开的是读端,fd[1]打开的是写端
在这里插入图片描述
如代码所示,我们做完了创建匿名管道的第一步,接下来还有两步:

创建子进程,然后将子进程的读端关闭,父进程的写段关闭,建立一个由子向父的信息传输管道
在这里插入图片描述

2.2.3 匿名管道的数据传输

完成了匿名管道的建立,现在我们可以来测试一下:对于子进程写入管道的数据,父进程程能否接受到。

  • 首先是子进程:
    我们调用write向管道中连续输入五行:“hello parent,I am child”。在写入结束之后,要记得关闭写端。
    在这里插入图片描述
  • 然后是父进程
    我们调用 read 接受来自管道中的数据。如果read返回值大于0,说明读取成功,如果等于0,说明文件关闭了,也就是读完了。
    最后我们对子进程进行进程等待,保证子进程结束之后,在关闭接受端(读端)。

在这里插入图片描述


完整代码:

#include<stdio.h>                                                                                              
  2 #include<unistd.h>
  3 #include<sys/wait.h>
  4 #include<string.h>
  5 #include<stdlib.h>
  6 
  7 int main()
  8 {
  9   int pipe_fd[2]={0};
 10 
 11   if(pipe(pipe_fd)<0){
 12     perror("pipe");
 13     return 1;
 14   }   
 15 
 16   printf("%d, %d\n",pipe_fd[0],pipe_fd[1]);
 17 
 18   pid_t id = fork();
 19   if(id<0){
 20     perror("fork");
 21     return 2;
 22   }
 23   else if(id==0) {
 24      //child write
 25      close(pipe_fd[0]);
 26 
 27      const char* msg =" hello parent,I am child";
 28 
 29      int count=5;
 30      while(count){
 31        write(pipe_fd[1],msg,strlen(msg));
 32        sleep(1);
 33        count--;
 34      }
 35      close(pipe_fd[1]);
 36      exit(0);
 37 
 38   }
 39   else{
 40      //parent read
 41      close(pipe_fd[1]);
 42 
 43      char buffer[64];
 44      while(1){
 45        buffer[0]=0;
 46        ssize_t size = read(pipe_fd[0],buffer,sizeof(buffer)-1);
 47        if(size>0){
 48          buffer[size] = 0;
 49          printf("parent get message from child#  %s\n",buffer);
 50        }
 51        else if(size==0){
 52          printf("pipe file close ,child quit!\n");
 53          break;
 54        }
 55        else{
 56          //TODO
 57          break;
 58        }
 59      }
 60     
 61      int status = 0;
 62      if(waitpid(id,&status,0)>0){
 63 
 64        printf("child quit,wait sucess!\n");
 65      }
 66      close(pipe_fd[0]);
 67   }
 68 
 69   return 0;

2.2.4 匿名管道的特性

  1. 管道 自带同步机制
  • 如果管道中没有消息,此时父进程会等待,等待管道内部有数据就绪
  • 如果管道中已经写满了,写端不会继续写入,而是等待, 等待管道的内部有空闲空间。
read端write端结果
不读write阻塞
不写read阻塞
不读&关闭write被终止
不读&关闭read读取到0代表文件结束

如果读端不读,而且关闭读端,写端此时如果继续写,本质就是在浪费系统资源,写进程会立马被OS 发送SIGPIPE信号终止掉。

在这里插入图片描述

同时我们还有一个问题,匿名管道的大小是多少?
65536 字节 ,即64KB(linux)

同时我们也可以使用系统命令【ulimit -a】.可以看到pipe size =512字节(4096bytes),约为4kb.这并不矛盾,这是原子性写入管道的单元大小。
在这里插入图片描述


  1. 管道是单向通信的
  2. 管道是面向字节流的
  3. 匿名管道只能保证具有血缘关系的进程进行进程通信,常用于父子
  4. 管道可以保证一定程度的数据读取的原子性(4KB以内)
  5. 管道的生命周期随进程

2.3 命名管道

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

命名管道是一种特殊类型的文件。简单来说,普通文件是需要将接受的数据刷新到磁盘的并持久化存储的,但是fio文件(管道文件)不需要,其作用是作为管道用来传递信息。
在这里插入图片描述
在这里插入图片描述

2.3.1 创建管道

我们可以直接通过命令行来创建命名管道的:

$ mkfifo filename

在这里插入图片描述
在这里插入图片描述

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

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

2.3.2 命名管道实例

多说无益,我们写一个小例子来增进对命名管道的使用与理解。
我们想实现一个客户端程序(client.c) 与一个 服务端程序(server.c),所有客户端输入的数据,都能被服务端实时的接收到。

为了方便之后的编译,我们编写一个Makefile,这里注意要添加all关键字,否则makefile 只会默认编译第一程序。
在这里插入图片描述

  • 服务端 server.c
  1. 创建命名管道
  2. 调用open,打开管道文件
  3. 调用read ,读取管道中的信息
    在这里插入图片描述
  • 用户端 client.c
  1. 打开管道文件
  2. 向管道中写入信息
    在这里插入图片描述

我们来测试一下:
可以看见,当我们通过用户端输入 语句的时候,服务端能够作出实时的接收。
在这里插入图片描述

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

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

3. system V 共享内存

3.1 什么是共享内存

我们在物理内存上开辟一块 内存空间,并且通过 页表映射到进程地址空间 的 共享区,
在这里插入图片描述

共享内存的基本原理是:

  1. OS申请一块物理内存地址
  2. OS将该内存映射进入对应的进程的共享区(堆区与栈区之间)
  3. OS可以将映射之后的虚拟地址返回给用户

也就是说,为了保证大量进程之间的通信,OS内存在提供了大量的共享内存,那么,这些共享内存OS该如何管理?
先描述,再组织


在这里插入图片描述
我们今天学习的主要是两个进程之间的通信(如上图),所以我们可以这样概括system V共享内存:

  1. 进程向os申请共享内存
  2. 进程1与进程2分别挂接对应的共享内存到自己的地址空间(共享区)
  3. 双方此时看到的就是同一份资源,即 可以进行正常的通信

3.2 共享内存的相关操作

1. shmget

功能: 用来创建共享内存
函数原型

#include<sys/ipc.h>
#include<sys/shm.h>

int shmget(key_t key,size_t size,int shmflg);

参数解释

  • key: 这个共享内存段名字,作用是 进行共享内存之间的唯一性区分
  • size: 共享内存大小(建议是4kb 的倍数)
  • shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的。

对于第三个参数,我们主要选择IPC_CREAT 和 IPC_EXCEL 一起使用。 如果目标的共享内存不存在,就创建。如果已经存在,则出错返回。除此之外,单独使用IPC_EXCL是没有意义的。

返回值:

成功返回一个非负整数,即该共享内存段的标识码;失败返回-1。


2 ftok

功能: 获取 kEY值
函数原型;

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

参数解释:
这里的参数可以按照自己的情况,任意填写。只需要之后保证A,B进程填写的是一样的,这样就可以保证AB进程使用的是同一个共享内存。


我们在使用ftok之前,一般在头文件中将pathname 与 proj_id 以宏的方式设定号,方便两个进程拿取。
在这里插入图片描述


在 服务端程序(server.c)创建一个共享内存
在这里插入图片描述
在这里插入图片描述

3 查看共享内存

我们可以通过命令行来获取当前OS 内的共享内存:

$ ipcs -m

可以发现,在我们运行server之前,只有一个共享内存,在我运行之后,多出了一个。
在这里插入图片描述
细心的同学可能发现,为什么在server运行结束之后,我们依旧能看到我们创建的共享内存?

这是因为与管道不同,所有的ipc资源都是随内核的,不随进程。只有当我们主动在进程退出的时候,用系统调用释放它,或者OS重启,它才会释放。

所以我们上面的程序就像 打开了文件 却不关闭一样,是有缺陷和隐患的,还要继续改进。

当然,也存在命令行命令可以释放共享内存:

ipcrm -m shmid的值

在这里插入图片描述


4 shmctl

功能: 用于控制共享内存
函数原型:

int shmctl(int shmid, int cmd ,struct shmid_ds* buf);

参数

  • shmid : 由shmget返回的共享内存表示码
  • cmd: 将要采取的动作 (有三个可取值)
  • buf :指向一个保存着共享内存的模式状态和访问权限的数据结构
    返回值
    成功返回0,失败返回-1

在这里插入图片描述


这里我们主要学习使用IPC_RMID 来删除共享内存段:
在这里插入图片描述

5 shmat

功能:将共享内存段连接到进程地址空间
函数原型

void *shmat(int shmid, const void *shmaddr, int shmflg);

参数

  • shmid: 共享内存标识
  • shmaddr:指定连接的地址
  • shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY

返回值
成功返回一个指针,指向共享内存第一个节;失败返回-1

说明:
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr -(shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存

简单来说,在我们不知道共享内存的地址的时候,我们把shmaddr设置为NULL就行。
在这里插入图片描述


6 shmdt

功能:将共享内存段与当前进程脱离
函数原型

int shmdt(const void *shmaddr);

参数
shmaddr: 由shmat所返回的指针

返回值:成功返回0;失败返回-1

注意:将共享内存段与当前进程脱离不等于删除共享内存段

在这里插入图片描述


3.3 共享内存实例

  • 服务端
    在这里插入图片描述

  • 用户端
    在这里插入图片描述
    运行结果;

在这里插入图片描述

3.4 共享内存的特性

  1. 共享内存的生命周期随OS
  2. 共享内存不提供任何同步与互斥的操作,双方彼此独立
  3. 共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数

系统在分配 shm 的时候,是按照4KB为基本单位的,也就是说,如果我们申请了4097byte的时候,实际上系统会分配4096*2 个byte,但是我们的使用权限只在0–>4097byte之间,所以,为避免浪费,一般建议申请4kb的整数倍空间

3.5 shmid 和key 再理解

key: 是一个用户层生成的唯一键值,核心作用是为了区分“唯一性”,不能用来进行IPC资源的操作。
shmid: 是一个系统给我们返回的IPC资源标识符,用来进行操作ipc资源。

类比一下,key值就是文件的inode号,shmid是文件的fd

共享内存数据结构(了解)

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 */
};

其中 ipc_perm 里存储了key值

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ornamrr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值