【Hello Linux】进程间通信

作者:@小萌新
专栏:@Linux
作者简介:大二学生 希望能和大家一起进步!
本篇博客简介:介绍Linux进程间通信

进程间通信

概念

进程间通信(InterProcess Communication,IPC)是指在不同进程之间传播或交换信息。

进程间通信的目的

我们都知道 进程是由程序员创建的 所以说进程之间通信本质就是程序员之间的通信

程序员在合作完成一个项目的时候需要同步数据

有时候在完成一个小demo之后需要共享这个demo的资源加快后序的开发进度

在一个小组的模块做完之后需要通知另外的一个小组进行后序的测试工作

如果测试的小组测出了bug要停止当前的开发修改此bug

所以说进程通信的目的有下面四个

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

进程通信的本质

进程间通信的本质就是让不同的进程看到同一份资源

扫描二维码关注公众号,回复: 14717395 查看本文章

我们都知道进程之间是具有独立性的 就拿父子进程对于同一个全局变量来说 如果子进程修改它的话会发生写时拷贝 从而达到一个保持进程独立性的效果

所以说我们如果想要两个进程看到同一份数据 这个数据肯定不能是属于某一个进程的

这个数据一定要属于操作系统 让操作系统来居中调度

在这里插入图片描述

由于这块资源可以由操作系统的不同模块来分配(内存 文件内核缓冲等)所以说这里就出现了很多种的进程通信方式

进程通信的分类

管道

  • 匿名管道
  • 命名管道

System V IPC

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

POSIX IPC

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

管道

什么是管道

在Linux中,管道是一种进程间通信的方式,它把一个程序的输出直接连接到另一个程序的输入

我们使用ls指令能够查看目录下的文件

在这里插入图片描述

我们使用grep指令可以搜索关键字

那么如果我们想要查看目录下所有带有14这个关键字的文件呢?是不是可以输入下面的指令

  ls | grep 14

在这里插入图片描述

与此同时 结合我们之前的进程部分的学习 我们知道使用指令本质上也是在创建一个进程

所以说这里我们是不是就是在使用两个进程互相合作 既然两个进程在合作那么是不是它们之间一定发生了通信?

而这里实际上就是使用管道来进行进程间的通信

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

匿名管道

匿名管道是什么

匿名管道是一种用于父子进程之间通信的方式 它不占用外部存储空间 只存在于内存中

匿名管道的原理

我们在前面的讲解中说过 进程间通信的本质就是让不同的进程看到同一份资源 当然匿名管道也不例外

它的原理是让父子进程看到同一份文件资源 之后父子进程就可以对该文件进行写入或者是读取操作 从而实现进程间通信

在这里插入图片描述

这里有两个注意点

  • 父子进程看到的同一份文件是由操作系统来进行管理的 所以说父子进程写入数据的时候并不会发生写时拷贝
  • 虽然我们这里使用的是文件作为第三方资源 可是这里操作系统并不会将父子进程写入的数据刷新到物理内存中 因为这样子会牺牲效率 所以说我们这里所说的这种文件是内存中的文件

创建匿名管道

系统中给我们提供了一个函数来创建匿名管道 函数原型如下

  int pipe(int pipefd[2]);

它的返回值是一个整型 如果我们调用成功返回0 失败返回-1

它的参数是一个输出型参数 返回的是管道读端和写端的文件描述符(匿名管道只能单向读写 即只能通过一端写入 一端输出)

数组元素 含义
pipefd[0] 管道读端的文件描述符
pipefd[1] 管道写端的文件描述符

我们可以这样子来形象的记忆

0代表嘴巴 用嘴巴来吃饭 所以应该是读端

1代表铅笔 用铅笔来写字 所以应该是写端

匿名管道的使用步骤

我们在前面的原理部分说过 匿名管道的管理其实就是让父子进程看到同一份资源

这里的先决条件是父子进程

所以说我们在使用匿名管道的时候一定会使用到fork函数和pipe函数

其具体步骤如下

  1. 父进程调用pipe函数创建管道

在这里插入图片描述

  1. 父进程创建子进程

在这里插入图片描述

  1. 父进程关闭写端 子进程关闭读端

在这里插入图片描述

这里有两点需要注意:

  1. 管道只能进行单向通信 这是因为如果可以双向通信有可能会造成自己写入的数据自己读取这种情况
  2. 从管道写端写入的数据会被内核缓冲 之后才会被读端读取

如果站在文件的角度我们可以这么理解

  1. 父进程调用pipe函数创建管道

在这里插入图片描述

  1. 父进程创建子进程

在这里插入图片描述

  1. 父进程关闭写端 子进程关闭读端

在这里插入图片描述

下面是代码示例

  1 // 这是一个测试管道的c语言程序
  2 #include <stdio.h>
  3 #include <stdlib.h>
  4 #include <unistd.h>
  5 #include <string.h>
  6 #include <sys/types.h>
  7 #include <sys/wait.h>
  8 
  9 
 10 
 11 int main()
 12 {
    
    
 13   // 1. 父进程创建管道
 14   int fd[2] = {
    
    0};
 15 
 16   if (pipe(fd) == -1)
 17   {
    
    
 18     // -1表示创建失败
 19     perror("pipe error!\n");
 20     exit(-1);
 21   }
 22                                                                                                                            
 23   // 2. 父进程创建子进程
 24   pid_t pid = fork();
 25   if (pid == 0)
 26   {
    
    
 27     // child
 28     // 3. child write
 29     close(fd[0]); // 关闭读端
 30 
 31     // 4. send message
 32     const char* msg = "hello! im child!\n";
 33     int count = 5;                                                                                                         
 34     while(count--)
 35     {
    
    
 36       write(fd[1] , msg , strlen(msg));
 37       // 这里我们不需要把/0 拷贝到文件中因为这只是c语言字符串的规则
 38       sleep(2);
 39     }
 40     close(fd[1]);
 41     exit(0);
 42   }
 43   
 44 
 45   close(fd[1]);
 46   // father
 47   // 3. father read 
 48   // 关闭写端
 49   // 4. read message
 50   char buff[64] = {
    
    0}; 
 51   while (1)
 52   {
    
    
 53     ssize_t s = read(fd[0] , buff , sizeof(buff));
 54     if (s == 0)
 55     {
    
    
 56       // 写文件关闭了
 57       printf("write file close\n");
 58       break;
 59     }
 60     else if (s > 0)
 61     {
    
    
 62       printf("child say: %s",buff);
 63     }
 64     else
 65     {
    
    
 66       printf("error!\n");
 67       break;
 68     }
 69   }
 70   close(fd[0]);
 71   waitpid(-1 , NULL , 0);
 72   printf("wait child process success!\n");
 73   return 0;
 74 }

在这个程序中 我们使用父进程打开了一个管道并且创建了一个子进程

父进程关闭写端即只读 子进程关闭读端即只写

接着子进程向文件中写入五段消息 父进程读取这五段消息

子进程退出后 父进程回收子进程资源 结束

他的演示效果如下

请添加图片描述

匿名管道的特点

  1. 管道内部自带同步与互斥机制

在了解这个特点之前我们需要了解下面的几个概念

  • 临界资源: 临界资源是指一次仅允许一个进程使用的共享资源
  • 同步: 同步(Synchronization)是指在多线程或多进程环境中 确保多个线程或进程之间能够按照预定的顺序执行
  • 互斥: 互斥(Mutual Exclusion)是指在多线程或多进程环境中 确保对共享资源的访问是排他的 也就是说 在任何时刻 只有一个线程或进程能够访问共享资源

从临界资源的概念上来讲 我们很容易的推断出临界资源是需要被保护的

不然我们没法保证在同一时刻只有同一进程访问这一共享资源

为了形成对于临界资源的保护 操作系统会对管道进行同步和互斥

同步其实是一种更加复杂的互斥 而互斥是一种特殊的同步

  1. 管道的生命周期

管道本质上是通过文件进行通信的 也就是说管道依赖于文件系统

那么当所有打开该文件的进程都退出后 该文件也就会被释放掉 所以说管道的生命周期随进程

  1. 管道提供的是流式服务

我们首先要理解下面的两个概念

流式服务: 数据没有明确的分割 不分一定的报文段

数据报服务: 数据有明确的分割 拿数据按报文段拿

也就是说 对于子进程输入的数据父进程读取多少是任意的 这就是流式服务

  1. 管道是半双工通信的

在数据的通信中 数据的传输方式大致可以分为下面的三种

单工通信 指数据只能在一个方向上传输。例如,广播电台只能向外发送信号,而不能接收来自外部的信号。

半双工通信 允许数据在两个方向上传输,但是在某一时刻,只允许数据在一个方向上传输。它实际上是一种切换方向的单工通信。例如,对讲机就是一种半双工通信设备。

全双工通信 允许数据同时在两个方向上传输。因此,全双工通信是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力。例如,在打电话时,我们可以同时听到对方说话并且说话。

我们的管道就是一种典型的半双工通信

如果我们想要父子进程之间可以相互交流通信我们可以再创建一根管道

在这里插入图片描述

管道的四种特殊情况

我们在使用管道的时候会遇到下面四种特殊情况

  1. 写端不写 读端一直读

遇到这种情况的时候 读端会挂起 直到管道里面有数据时 读端才会被唤醒

代码表示如下 (其实我们前面写的演示代码就是这种情况 我们这里直接复用)

  1 // 这是一个测试管道的c语言程序
  2 #include <stdio.h>
  3 #include <stdlib.h>
  4 #include <unistd.h>
  5 #include <string.h>
  6 #include <sys/types.h>
  7 #include <sys/wait.h>
  8 
  9 
 10 
 11 int main()
 12 {
    
    
 13   // 1. 父进程创建管道
 14   int fd[2] = {
    
    0};
 15 
 16   if (pipe(fd) == -1)
 17   {
    
    
 18     // -1表示创建失败
 19     perror("pipe error!\n");
 20     exit(-1);
 21   }
 22                                                                                                                            
 23   // 2. 父进程创建子进程
 24   pid_t pid = fork();
 25   if (pid == 0)
 26   {
    
    
 27     // child
 28     // 3. child write
 29     close(fd[0]); // 关闭读端
 30 
 31     // 4. send message
 32     const char* msg = "hello! im child!\n";
 33     int count = 5;                                                                                                         
 34     while(count--)
 35     {
    
    
 36       write(fd[1] , msg , strlen(msg));
 37       // 这里我们不需要把/0 拷贝到文件中因为这只是c语言字符串的规则
 38       sleep(2);
 39     }
 40     close(fd[1]);
 41     exit(0);
 42   }
 43   
 44 
 45   close(fd[1]);
 46   // father
 47   // 3. father read 
 48   // 关闭写端
 49   // 4. read message
 50   char buff[64] = {
    
    0}; 
 51   while (1)
 52   {
    
    
 53     ssize_t s = read(fd[0] , buff , sizeof(buff));
 54     if (s == 0)
 55     {
    
    
 56       // 写文件关闭了
 57       printf("write file close\n");
 58       break;
 59     }
 60     else if (s > 0)
 61     {
    
    
 62       printf("child say: %s",buff);
 63     }
 64     else
 65     {
    
    
 66       printf("error!\n");
 67       break;
 68     }
 69   }
 70   close(fd[0]);
 71   waitpid(-1 , NULL , 0);
 72   printf("wait child process success!\n");
 73   return 0;
 74 }

在这里插入图片描述

我们可以看到 子进程其实是隔两秒才开始写数据的 而父进程一直在读数据

要是按照我们的理解 其实第二次父进程读数据的时候就会跳出while循环了

可是并没有 这就是管道的第一种特殊情况造成的影响 如果写端不写 读端会挂起

  1. 读端不读 写端一直写

遇到这种情况的时候 当管道里面的数据写满后 写端会被挂起 当读取了一定的数据后 写端才会被唤醒

代码表示如下

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <string.h>
  4 #include <unistd.h>
  5 #include <sys/types.h>
  6 #include <sys/wait.h>
  7 
  8 int main()
  9 {
    
    
 10   // 1. ´ò¿ª¹ÜµÀ
 11   int fd[2] = {
    
    0};
 12   if (pipe(fd) == -1)
 13   {
    
    
 14     perror("pipe error!\n");
 15     exit(-1);
 16   }
 17 
 18   // 2. ´´½¨×Ó½ø³Ì
 19   pid_t pid = fork();
 20   if (pid == 0)
 21   {
    
    
 22     // child 1                                                                                                             
 23     close(fd[0]);
 24     int count = 0;
 25     while(1)
 26     {
    
    
 27       write(fd[1] , "a" , 1);
 28       count++;
 29       printf("child say success! :%d\n",count);
 30     }
 31   }
 32   // father
 33   close(fd[1]);
 34   // 4. read message
 35   waitpid(-1 , NULL , 0);
 36   return 0;
 37 }

我们可以看到 子进程一直在写数据 在写满了管道之后就挂起了

此时需要父进程读取一定的数据子进程才能够继续写

在这里插入图片描述

  1. 写端关闭

遇到这种情况的时候 读端会在读取完全部的数据之后继续执行下面的流程

代码表示如下

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <string.h>
  4 #include <stdlib.h>
  5 #include <sys/types.h>
  6 #include <sys/wait.h>
  7 
  8 
  9 int main()
 10 {
    
    
 11   int fd[2] = {
    
    0};
 12   if (pipe(fd) == -1)
 13   {
    
    
 14     perror("pipe error!\n");
 15     exit(-1);
 16   }
 17 
 18   pid_t id = fork();
 19 
 20   if (id == 0)
 21   {
    
    
 22     // child                                                                                                               
 23     close(fd[0]);
 24     int count = 5;
 25     const char* msg = "hello world\n";
 26     while(count--)
 27     {
    
    
 28       write(fd[1] , msg , strlen(msg));
 29       sleep(1);
 30     }
 31     close(fd[1]);
 32     exit(0);
 33   }                                                                                                                        
 34 
 35   char buff[64];
 36   close(fd[1]);
 37   // father
 38   while(1)
 39   {
    
    
 40     ssize_t s =  read(fd[0] ,buff ,sizeof(buff));
 41     if (s == 0)
 42     {
    
    
 43       printf("write close\n");
 44       break;
 45     }
 46     printf("child say :%s",buff);
 47   }
 48 
 49   while(1)
 50   {
    
    
 51     printf("father still exist\n");
 52     sleep(1);
 53   }
 54   return 0;
 55 }                                                                       

我们可以看到 子进程停止写数据退出后 父进程在读完数据之后执行其他内容去了

在这里插入图片描述

读端关闭

遇到这种情况的时候 写端进程会直接退出

代码表示如下

  1 #include <stdio.h>                                                                                                         
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <string.h>
  5 #include <sys/types.h>
  6 #include <sys/wait.h>
  7 
  8 
  9 
 10 int main()
 11 {
    
    
 12   int fd[2] = {
    
    0};
 13   if (pipe(fd) < 0) // -1 error
 14   {
    
    
 15     perror("pipe error!\n");
 16     return -1;
 17   }
 18 
 19   pid_t pid = fork();
 20   if (pid == 0) // child
 21   {
    
    
 22     close(fd[0]);
 23     const char* msg = "hello world!\n";
 24     while (1)
 25     {
    
    
 26       write(fd[1] ,msg , strlen(msg));
 27       printf("send success!\n");
 28       sleep(1);
 29     }
 30 
 31     // we do not set _exit func there
 32   }
 33 
 34   // father 
 35   close(fd[1]);
 36   sleep(6);
 37   close(fd[0]);
 38   waitpid(pid , NULL ,0);
 39   printf("wait child process success!\n");
 40   return 0;
 41 }

我们在这段代码中 在创建管道六秒后 将读端全部关闭 父进程等待子进程

但是我们并没有对子进程做任何事

我们来看看效果

在这里插入图片描述

我们可以发现 在六秒后读端关闭的瞬间 子进程退出了

这是为什么呢? 我们这里可以用到之前进程控制部分的相关知识 获取进程的退出信号 (因为这里的进程肯定不是正常退出的 所以查看退出码没有意义)

我们在原先的代码最后加上这段代码

  int status = 0;    
  waitpid(pid , &status ,0);    
  printf("wait child process success!\n");    
  printf("the singal is : %d\n", status & 0x7f);         

在这里插入图片描述

我们可以发现 这里的进程退出信号是13

接着我们使用kill -l指令查看所有的退出信号

在这里插入图片描述
那么现在我们就能知道了 当匿名管道发生情况四的时候 操作系统会向进程发送13号命令来终止进程

匿名管道的大小

我们可以复用上面的代码来测试出管道的大小

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <string.h>
  4 #include <unistd.h>
  5 #include <sys/types.h>
  6 #include <sys/wait.h>
  7 
  8 int main()
  9 {
    
    
 10   // 1. 创建管道
 11   int fd[2] = {
    
    0};
 12   if (pipe(fd) == -1)
 13   {
    
    
 14     perror("pipe error!\n");
 15     exit(-1);
 16   }
 17 
 18   // 2. 父子进程
 19   pid_t pid = fork();
 20   if (pid == 0)
 21   {
    
    
 22     // child 1                                                                                                             
 23     close(fd[0]);
 24     int count = 0;
 25     while(1)
 26     {
    
    
 27       write(fd[1] , "a" , 1);
 28       count++;
 29       printf("child say success! :%d\n",count);
 30     }
 31   }
 32   // father
 33   close(fd[1]);
 34   // 4. read message
 35   waitpid(-1 , NULL , 0);
 36   return 0;
 37 }

在这里插入图片描述

我们可以看到 子进程一共向管道中写入了65536字节的数据 也就是512kb

命名管道

命名管道是什么

命名管道是一种特殊的管道,存在于文件系统中。它也被称为先进先出队列 (FIFO) 。它可以用于任何两个进程间的通信 而不限于同源的两个进程

我们可以狭义上的理解匿名管道就是没有名字的管道而命名管道就是有名字的管道

但是实际上它们还是有很多的不同点 比如说匿名管道必须要同源的两个进程才能通信而命名管道则不需要

命名管道的原理

命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。

这里我们有两点需要注意:

  1. 普通文件是很难做到通信的 即便做到通信也无法解决一些安全问题
  2. 命名管道和匿名管道一样 都是内存文件 只不过命名管道在磁盘有一个简单的映像 但这个映像的大小永远为0 因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中

系统中创建命名管道

我们在系统中一般使用mkfifo命令来创建一个命名管道

使用效果如下

在这里插入图片描述
此时我们就可以使用该管道来实现进程间的通信了

我们使用一个进程不停的往该管道文件内写入数据再使用一个进程不停的读取数据 效果如下:

请添加图片描述

在左边 我们使用了这样的一行shell脚本

while true; do echo "hello fifo" ; sleep 1 ; done > fifo

它的意思是每隔一秒不停的循环输出hello fifo 并将输出的hello fifo重定向到fifo文件中

在右边我们不停的在从fifo管道中读取数据

我们可以发现这样一个神奇的现象:当我们退出右边的进程的时候 左边的bash进程也退出了

这是为什么呢?

我们前面讲管道的四种特殊情况下讲过这一点 当管道的读端关闭的时候 写端进程会被操作系统使用13信号杀掉

而我们的shell脚本就是由bash进程执行的 所以说当我们关闭读端的左边的bash进程就终止了

程序中创建命名管道

我们在程序中创建命名管道要使用mkfifo函数 它的函数原型如下

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

返回值

  • 如果管道创建成功则返回 0
  • 如果管道创建失败则返回 -1

参数

  1. const char *pathname

第一个参数是一个字符串

  • 如果我们以路径的方式给出 则命名管道会默认创建在pathname路径下
  • 如果我们以文件名的方式给出 则命名管道会默认创建在当前路径

这里关于当前路径的概念 如果还有不理解的同学可以参考我的这篇博客

基础IO

  1. mode_t mode

这里设置是管道文件的权限 我们一般使用八进制的数字设置

当然权限的设置还和umask有关

有关权限的概念我之前的一篇博客以及详细介绍了 这里就不再赘述

Linux中权限

下面是该函数的使用

  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/stat.h>
  4 
  5 
  6 int main()
  7 {
    
    
  8   if (mkfifo("myfifo" , 0666) < 0)
  9   {
    
    
 10     perror("mkfifo fail!\n");
 11     return -1;
 12   }                                                                                                 
 13   return 0;
 14 }

上面这段代码的意思是 在当前路径下创建一个叫做mkfifo的命名管道

执行代码后查看当前目录下的文件 我们发现管道文件真的被创建了

在这里插入图片描述

命名管道实现通信

我们可以创建两个程序分别代表客户端 (client) 和服务端(server)

服务器创建一个命名管道 之后客户端和服务端全部打开该命名管道

服务端读数据 客户端写数据

服务端代码

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <sys/types.h>
  4 #include <sys/stat.h>
  5 #include <string.h>
  6 #include <fcntl.h>
  7 #include <stdlib.h>
  8 #define FILENAME "myfifo"
  9 
 10 // 服务端要求创建一个命名管道 
 11 // 并且接受客户端发来的消息
 12 int main()
 13 {
    
    
 14   // open the fifo
 15   if (mkfifo(FILENAME , 0666) < 0)
 16   {
    
    
 17     perror("mkfifo error!\n");
 18     return -1;
 19   }
 20 
 21   // open the file 
 22   int fd = open(FILENAME , O_RDONLY);
 23   if (fd < 0)
 24   {
    
    
 25     perror("open fail!\n");                                                                                                
 26     return 1;
 27   }
 28 
 29   char msg[128];
 30   // read msg from fifo                                                                                                    
 31   while (1)
 32   {
    
    
 33     msg[0] = 0;
 34     ssize_t s = read(fd , msg , sizeof(msg)-1); 
 35     if (s > 0)
 36     {
    
    
 37       msg[s] = 0;
 38       printf("client say: %s\n" , msg);
 39     }
 40     else if (s == 0)
 41     {
    
    
 42       printf("client end!\n");
 43       break;
 44     }
 45     else 
 46     {
    
    
 47       perror("read error!\n");
 48       exit(-1);
 49     }
 50   }
 51   close(fd);
 52   return 0;
 53 }

而对于客户端来说 服务端已经将命名管道创建好了

所以说客户端只需要往管道里面写入数据就好了

服务端代码

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <sys/types.h>
  4 #include <sys/stat.h>
  5 #include <string.h>
  6 #include <fcntl.h>
  7 #include <stdlib.h>
  8 #define FILENAME "myfifo"
  9 
 10 
 11 
 12 int main()                                                                                                                                      
 13 {
    
    
 14   int fd = open(FILENAME , O_RDWR);
 15   if (fd < 0)
 16   {
    
    
 17     perror("open error\n");
 18     exit(-1);
 19   }
 20 
 21   char msg[128];
 22   while(1)
 23   {
    
    
 24     msg[0] = 0;
 25     printf("Please write :");
 26     fflush(stdout);
 27     // the screen is line fflush but there is no \n
 28     ssize_t s = read (0 , msg , sizeof(msg) - 1);
 29     if (s > 0)
 30     {
    
    
 31       msg[s-1] = 0; // because the end of msg is :xxxx\n\0
 32       write(fd , msg , strlen(msg));
 33     }
 34   }
 35   close(fd);
 36   return 0;
 37 }

接下来我们只需要将服务端和客户端都运行起来 就能够实现两个进程之间的通信了

在这里插入图片描述

服务端和客户端之间的退出关系

我们这里首先要明白客户端是写端

服务端是读端

当写端退出后 读端会继续执行后面的程序

所以说客户端退出后 服务端会继续执行后面的代码

而当读端退出后 写端会被操作系统杀死

所以说服务端退出后 客户端会被操作系统杀死

通信是在内存中进行的

我们可以尝试让客户端一直写数据 但是服务端一直不读数据

之后查看fifo文件的大小

在这里插入图片描述

我们可以发现fifo文件的大小还是0 这说明我们的命名管道通信还是在内存当中进行通信的

命名管道实现进程遥控

我们通过命名管道可以实现一个进程对于另一个进程遥控

当然这里要利用到子进程和进程替换的一些知识(因为如果使用父进程进行进程替换的话替换一次服务端就停止服务了)

我们这里只需要对于服务端的代码进行一些修改

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <sys/types.h>
  4 #include <sys/stat.h>
  5 #include <sys/wait.h>
  6 #include <string.h>
  7 #include <fcntl.h>
  8 #include <stdlib.h>
  9 #define FILENAME "myfifo"
 10 
 11 // 服务端要求创建一个命名管道 
 12 // 并且接受客户端发来的消息
 13 int main()
 14 {
    
    
 15   // open the fifo
 16   if (mkfifo(FILENAME , 0666) < 0)
 17   {
    
    
 18     perror("mkfifo error!\n");
 19     return -1;
 20   }
 21 
 22   // open the file 
 23   int fd = open(FILENAME , O_RDONLY);
 24   if (fd < 0)
 25   {
    
    
 26     perror("open fail!\n");
 27     return 1;                                                       
 28   }
 29 
 30   char msg[128];
 31   // read msg from fifo 
 32   while (1)
 33   {
    
    
 34     msg[0] = 0; 
 35     ssize_t s = read(fd , msg , sizeof(msg)-1); 
 36     if (s > 0)                                                      
 37     {
    
    
 38       msg[s] = 0;
 39       printf("client say: %s\n" , msg);
 40       if (fork() == 0)
 41       {
    
    
 42         execlp(msg , msg , NULL);
 43         exit(1);
 44       }
 45       waitpid(-1 , NULL , 0);
 46     }
 47     else if (s == 0)
 48     {
    
    
 49       printf("client end!\n");
 50       break;
 51     }
 52     else 
 53     {
    
    
 54       perror("read error!\n");
 55       exit(-1);
 56     }
 57   }
 58   close(fd);
 59   return 0;
 60 }

下面是实机效果

在这里插入图片描述

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

  • 匿名管道只能用于有亲缘关系的进程之间的通信 而命名管道能用于任意进程之间通信
  • 匿名管道使用pipe函数创建并打开 而命名管道使用mkfifo函数创建 由open函数打开
  • 匿名管道不在磁盘中创建文件 命名管道会在磁盘中创建文件

虽然它们有这些不同点 但是它们工作的时候都是在内存中传输数据的

命令行中的管道

我们在命令行中也可以使用管道来通信

比如说我们下面的指令
在这里插入图片描述
这就是我们在使用命令行中的管道进行通信

那么命令行中的管道是匿名管道还是命名管道呢?

答案是匿名管道 因为实际上我们在使用这个管道的时候磁盘上并没有创建文件

system V 进程间通信

system V 是什么

我们之前讲的管道 不管是匿名管道还是命名管道 实际上都是利用了文件来作为第三方资源通信 操作系统只提供这块资源而并不做过多的管理

而system V IPC则是由操作系统所涉及出来的 帮助进程进行进程间通信的方式 他包括以下三种方式

  • system v 共享内存
  • system v 信号量
  • system v 消息队列

其中共享内存和消息队列是为了传输数据而设计出来的 而信号量是为了保证进程之间的同步和互斥而设计的

共享内存

共享内存是什么

共享内存是一种允许两个或多个进程访问同一个逻辑内存的通信机制

共享内存的原理

我们首先来看下面这张图
在这里插入图片描述

我们的操作系统首先在物理空间中开辟一块内存

之后通过页表映射到各个进程的虚拟地址空间中

于是乎我们的进程便可以看到同一份共享内存了

这就是我们的共享内存了

共享内存数据结构

我们都知道系统中存在着大量的进程 这些进程如果要通信那就势必要使用到大量的共享内存 那么既然存在着大量的共享内存操作系统就必须要对其进行管理

而操作系统的管理原则是什么呢? 先描述 再组织

我们也都知道linux操作系统是由c语言写的 c语言中描述一个复杂对象的方式是结构体

共享内存的结构体如下

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

我们申请一块共享内存之后 为了保证这块共享内存的唯一性 我们必须要对它做个标识

我们在linux系统中我们使用key值来唯一标识一块共享内存

在我们的 shmid_ds 结构体的第一行有一个叫做 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;
};

我们看它的第一行有这么一段描述 __kernel_key_t key;

而这个key就是唯一标识共享内存的key值

共享内存创建和释放的步骤

共享内存的创建:

  1. 操作系统申请一块共享内存空间
  2. 操作系统通过页表将这块共享内存挂接到虚拟地址空间上

共享内存的释放:

  1. 取消共享内存和虚拟地址空间的映射
  2. 释放共享内存

创建共享内存

我们创建共享内存需要用到shmget函数 它的函数原型如下

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

返回值

  • 如果创建成功 shmget函数会返回一个整型的内存标识符
  • 如果创建失败 shmget函数会返回-1

参数

  1. key_t key 待创建的共享内存在系统中的唯一标识
  2. size_t size 待创建的共享内存的大小
  3. int shmflg 创建共享内存的方式

参数详解

  1. key_t key

我们在前面说过 它是系统对于共享内存的唯一标识

那么我们怎么去创建这么一个唯一标识呢?

在linux中我们使用ftok函数去创建它 它的函数原型是

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

ftok函数的作用就是将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值 这个值被称为IPC键值

在使用shmget函数获取共享内存时 这个key值会被填充进维护共享内存的数据结构当中

需要注意的是 pathname所指定的文件必须存在且可存取

其实这个函数的底层就是一种算法 通过这个算法将路径的字符串和id转化为一个key_t类型的key值

所以说只要我们传入的路径和id是一样的最后生成的key值就是一样的

  1. size_t size

按照需要创建共享内存的大小 注意不要太大也不要太小

  1. int shmflg

这个格式我们见过很多次了 在文件操作中我们也是用一个整型来表示文件的打开方式

实际上我们是用其中比特位来判断打开哪些模式

常见的打开方式如下

组合 作用
IPC_CREAT 如果内核中不存在键值与key相等的共享内存 则新建一个共享内存并返回该共享内存的句柄 如果存在这样的共享内存 则直接返回该共享内存的句柄
IPC_CREAT l IPC_EXCL 如果内核中不存在键值与key相等的共享内存 则新建一个共享内存并返回该共享内存的句柄 如果存在这样的共享内存 则出错返回

句柄: 标定某种资源能力的东西叫做句柄

  • 如果我们使用IPC_CREAT创建共享内存 我们一定能得到一个共享内存 但是不一定是新的
  • 如果我们使用IPC_CREAT | IPC_EXCL创建共享内存 我们如果得到了一个共享内存 它一定是最新的

在了解了上面前置知识之后我们就可以开始创建共享内存了

代码如下

  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/ipc.h>
  4 #include <sys/shm.h>
  5 #include <unistd.h>
  6 #include <stdlib.h>
  7 #define PATH "/home/shy"
  8 #define ID 0x6666
  9 #define SIZE 4096
 10 
 11 
 12 int main()
 13 {
    
    
 14   key_t key =ftok(PATH , ID);
 15   if (key < 0)                                                                                                    
 16   {
    
    
 17     perror("ftok!\n");
 18     exit(1);
 19   }
 20 
 21   int shm = shmget(key , SIZE , IPC_CREAT | IPC_EXCL);
 22   // get the new memory
 23   if (shm < 0)
 24   {
    
    
 25     perror("shmget!\n");
 26     exit(2);
 27   }
 28 
 29   printf("the key is : %d\n" , key);
 30   printf("the shm is : %d\n" , shm);
 31   return 0;
 32 }

在这里插入图片描述

查看共享内存

我们可以使用ipcs指令查看有关进程间通信设施的信息

在这里插入图片描述

当我们单独使用ipcs指令的时候会出现出消息队列、共享内存以及信号量相关的信息 如果只想知道其中的一个信息的话 我们可以在后面携带选项

  • -q:列出消息队列相关信息
  • -m:列出共享内存相关信息
  • -s:列出信号量相关信息

在这里插入图片描述

其中它的每列信息含义如下

命名 含义
key 系统区别各个共享内存的唯一标识
shmid 共享内存的用户层id(句柄)
owner 共享内存的拥有者
perms 共享内存的权限
bytes 共享内存的大小
nattch 关联共享内存的进程数
status 共享内存的状态

key和shmid的区别:

我们的key是系统中是被共享内存的唯一标识 而shmid是我们用户层识别共享内存的标识

这就好像是inode和文件名的区别

系统中使用inode来标识一个文件 而我们使用文件名来标识一个文件

共享内存的释放

如果再次运行下sever程序 我们就会发现这样子的错误

在这里插入图片描述

这是因为共享内存的生命周期是随内核的 就算进程结束了共享内存也不会直接消失

想要释放共享内存只有两种方式

  • 关机重启
  • 主动使用函数释放

使用命令释放共享内存

我们可以使用ipcrm -m shmid指令释放指定id的共享内存资源

在这里插入图片描述

我们可以看到 删除之后共享内存就不存在了

使用程序释放共享内存

我们一般使用shmctl函数来控制共享内存 它的函数原型如下

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

返回值:

  • 如果调用成功 返回0
  • 如果调用失败 返回-1

参数:

  • 第一个参数shmid 表示用户层面标识共享内存的句柄
  • 第二个参数cmd 表示具体的控制命令
  • 第三个参数buf 用于获取或设置所控制共享内存的数据结构

其中关于第二个参数 常用的命令有以下几个

选项 作用
IPC_STAT 获取共享内存的当前关联值 此时参数buf作为输出型参数
IPC_SET 在进程有足够权限的前提下 将共享内存的当前关联值设置为buf所指的数据结构中的值
IPC_RMID 删除共享内存段

将上面的代码加上释放内存的代码

  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/ipc.h>
  4 #include <sys/shm.h>
  5 #include <unistd.h>
  6 #include <stdlib.h>
  7 #define PATH "/home/shy"
  8 #define ID 0x6666
  9 #define SIZE 4096
 10 
 11 
 12 int main()
 13 {
    
    
 14   key_t key =ftok(PATH , ID);
 15   if (key < 0)
 16   {
    
    
 17     perror("ftok!\n");
 18     exit(1);
 19   }
 20 
 21   int shm = shmget(key , SIZE , IPC_CREAT | IPC_EXCL);
 22   // get the new memory
 23   if (shm < 0)
 24   {
    
    
 25     perror("shmget!\n");
 26     exit(2);
 27   }    
  8 
 29   printf("the key is : %d\n" , key);
 30   printf("the shm is : %d\n" , shm);
 31 
 32 
 33   sleep(5); 
 34   shmctl(shm , IPC_RMID , NULL);
 35   printf("delete success!\n");
 36   return 0;
 37 }                                 

之后我们继续运行代码

在这里插入图片描述

我们可以看到 共享内存再创建后就被释放了

共享内存的关联

我们一般使用shmat函数来进行共享内存和虚拟地址空间的关联 它的函数原型如下

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

返回值:

  • shmat调用成功 返回共享内存映射到进程地址空间中的起始地址
  • shmat调用失败 返回(void*)-1

参数:

  • 第一个参数shmid 表示待关联共享内存的用户级标识符
  • 第二个参数shmaddr 指定共享内存映射到进程地址空间的某一地址 通常设置为NULL 表示让内核自己决定一个合适的地址位置
  • 第三个参数shmflg 表示关联共享内存时设置的某些属性

其中它的第三个参数的选项如下表所示

选项 作用
SHM_RDONLY 关联共享内存后只进行读取操作
SHM_RND 若shmaddr不为NULL 则关联地址自动向下调整为SHMLBA的整数倍 公式:shmaddr-(shmaddr%SHMLBA)
0 默认为读写权限

之后我们尝试下关联共享内存

在这里插入图片描述
这是为什么呢?

在这里插入图片描述

这里因为我们的权限是0 所以说普通用户并没有权限建立关联

要解决这个问题也很简单 我们只需要再创建的时候加上权限就好了

在这里插入图片描述

 int shm = shmget(key , SIZE , IPC_CREAT | IPC_EXCL | 0666);

在这里插入图片描述

共享内存的去关联

我们一般使用shmat函数来进行共享内存和虚拟地址空间的去关联 它的函数原型如下

  int shmdt(const void *shmaddr);

返回值:

  • shmdt调用成功 返回0
  • shmdt调用失败 返回-1

参数:

  • 待去关联共享内存的起始地址 即调用shmat函数时得到的起始地址

代码演示如下

  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/ipc.h>
  4 #include <sys/shm.h>
  5 #include <unistd.h>
  6 #include <stdlib.h>
  7 #define PATH "/home/shy"
  8 #define ID 0x6666
  9 #define SIZE 4096
 10 
 11 
 12 int main()
 13 {
    
    
 14   key_t key =ftok(PATH , ID);
 15   if (key < 0)
 16   {
    
    
 17     perror("ftok!\n");
 18     exit(1);
 19   }
 20 
 21   int shm = shmget(key , SIZE , IPC_CREAT | IPC_EXCL | 0666);
 22   // get the new memory
 23   if (shm < 0)
 24   {
    
    
 25     perror("shmget!\n");
 26     exit(2);
 27   }
 28 
 29    printf("the key is : %d\n" , key);
 30    printf("the shm is : %d\n" , shm);
 31 
 32 
 33    // Establish a connection                                                                                                                                                  
 34    void* mem = shmat(shm , NULL , 0);
 35    if (mem  == (void*)-1)
 36    {
    
    
 37      perror("shmat");
 38      exit(1);
 39    }
 40    else
 41    {
    
    
 42      printf("shmat success!\n");
 43    }
 44   
 45 
 46   sleep(2);
 47   shmdt(mem);
 48   printf("dttach success!\n");
 49   return 0;
 50 }

在这里插入图片描述

但是我们需要注意的是 去关联并不代表着释放共享内存

在这里插入图片描述
我们使用ipcs指令查看 发现创建的共享内存依然没有被释放

共享内存实现客户端服务端通信

要求如下

我们的客户端不停的写入数据 服务端不停的将客户端输出的数据打印出来

共享内存要是客户端创建的 服务端通过相同的路径和id获取到该共享内存

代码实现如下

服务端

  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/ipc.h>
  4 #include <sys/shm.h>
  5 #include <unistd.h>
  6 #include <stdlib.h>
  7 #define PATH "/home/shy"
  8 #define ID 0x6666
  9 #define SIZE 4096
 10 
 11 
 12 int main()
 13 {
    
    
 14   key_t key =ftok(PATH , ID);
 15   if (key < 0)
 16   {
    
    
 17     perror("ftok!\n");
 18     exit(1);
 19   }
 20 
 21   int shm = shmget(key , SIZE , IPC_CREAT | IPC_EXCL | 0666);
 22   // get the new memory
 23   if (shm < 0)
 24   {
    
    
 25     perror("shmget!\n");
 26     exit(2);
 27   }
 28 
 29    printf("the key is : %d\n" , key);
 30    printf("the shm is : %d\n" , shm);
 31 
 32 
 33    // Establish a connection
 34    char* mem =(char*)shmat(shm , NULL , 0);
 35    if (mem  == (void*)-1)
 36    {
    
    
 37      perror("shmat");
 38      exit(1);
 39    }
 40    else
 41    {
    
    
 42      printf("shmat success!\n");
 43    }
 44 
 45    while(1)
 46    {
    
    
 47       printf("client# %s\n", mem);
 48       sleep(1);
 49    }
 50 
 51 
 52   sleep(2);
 53   shmdt(mem);
 54   printf("dttach success!\n");
 55 
 56 
 57   shmctl(shm , IPC_RMID , NULL);
 58   return 0;
 59 }

客户端

  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/ipc.h>
  4 #include <sys/shm.h>
  5 #include <unistd.h>
  6 #include <stdlib.h>
  7 #include <string.h>
  8 #define PATH "/home/shy"
  9 #define ID 0x6666
 10 #define SIZE 4096
 11 
 12 
 13 
 14 int main()
 15 {
    
    
 16   key_t key = ftok(PATH, ID); //获取与server进程相同的key值
 17   if (key < 0){
    
    
 18     perror("ftok");
 19     return 1;
 20   }
 21   int shm = shmget(key, SIZE, IPC_CREAT); //获取server进程创建的共享内存的用户层id
 22   if (shm < 0){
    
    
 23     perror("shmget");
 24     return 2;
 25   }
 26 
 27   printf("key: %x\n", key); //打印key值
 28   printf("shm: %d\n", shm); //打印共享内存用户层id
 29 
 30   char* mem = (char*)shmat(shm, NULL, 0); //关联共享内存
 31   char buff[128];
 32   while (1)
 33   {
    
    
 34     ssize_t s = read(0 , buff , sizeof(buff)-1);
 35     buff[s-1] = 0;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
 36     memcpy(mem,buff,sizeof(buff));
 37     buff[0] = 0;
 38     sleep(1);
 39     mem[0] = 0;
 40   }
 41   shmdt(mem); //共享内存去关联
 42   return 0;
 43 }

最终效果如下

在这里插入图片描述

共享内存和管道的区别

我们先来看管道通信的图

在这里插入图片描述

再来看看共享内存的图

在这里插入图片描述

从这里我们可以看出

使用共享内存进行通信 将一个文件从一个进程传输到另一个进程只需要进行两次拷贝操作
而我们使用管道通信缺需要四次

所以说共享内存实际上是目前为止最快的一种通信方式

但是它的缺点野有 就是没有提供任何的同步和互斥机制

System V消息队列

消息队列的基本原理

消息队列实际上就是在系统当中创建了一个队列,队列当中的每个成员都是一个数据块,这些数据块都由类型和信息两部分构成,两个互相通信的进程通过某种方式看到同一个消息队列,这两个进程向对方发数据时,都在消息队列的队尾添加数据块,这两个进程获取数据块时,都在消息队列的队头取数据块

在这里插入图片描述

其中消息队列当中的某一个数据块是由谁发送给谁的 取决于数据块的类型

  1. 消息队列提供了一个从一个进程向另一个进程发送数据块的方法
  2. 每个数据块都被认为是有一个类型的 接收者进程接收的数据块可以有不同的类型值
  3. 和共享内存一样 消息队列的资源也必须自行删除 否则不会自动清除 因为system V IPC资源的生命周期是随内核的

消息队列的数据结构

我们都知道系统中存在着大量的进程 这些进程如果要通信那就势必要使用到大量的消息队列 那么既然存在着大量的消息队列操作系统就必须要对其进行管理

而操作系统的管理原则是什么呢? 先描述 再组织

我们也都知道linux操作系统是由c语言写的 c语言中描述一个复杂对象的方式是结构体

消息队列的结构体如下

struct msqid_ds {
    
    
	struct ipc_perm msg_perm;
	struct msg *msg_first;      /* first message on queue,unused  */
	struct msg *msg_last;       /* last message in queue,unused */
	__kernel_time_t msg_stime;  /* last msgsnd time */
	__kernel_time_t msg_rtime;  /* last msgrcv time */
	__kernel_time_t msg_ctime;  /* last change time */
	unsigned long  msg_lcbytes; /* Reuse junk fields for 32 bit */
	unsigned long  msg_lqbytes; /* ditto */
	unsigned short msg_cbytes;  /* current number of bytes on queue */
	unsigned short msg_qnum;    /* number of messages in queue */
	unsigned short msg_qbytes;  /* max number of bytes on queue */
	__kernel_ipc_pid_t msg_lspid;   /* pid of last msgsnd */
	__kernel_ipc_pid_t msg_lrpid;   /* last receive pid */
};

可以看到消息队列数据结构的第一个成员是msg_perm 它和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;
};

消息队列的创建

我们创建消息队列需要用到msgget函数 它的函数原型如下

int msgget(key_t key, int msgflg);
  1. 创建消息队列也需要使用ftok函数生成一个key值 这个key值作为msgget函数的第一个参数
  2. msgget函数的第二个参数与创建共享内存时使用的shmget函数的第三个参数相同
  3. 消息队列创建成功时 msgget函数返回的一个有效的消息队列标识符(用户层标识符)

消息队列的释放

释放消息队列我们需要用msgctl函数 msgctl函数的函数原型如下:

  int msgctl(int msqid, int cmd, struct msqid_ds *buf);

这里的三个参数和共享内存的三个参数相同

消息队列发送数据

向消息队列发送数据我们需要用msgsnd函数 msgsnd函数的函数原型如下:

ssize_t msgsnd(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

msgsnd函数的参数说明:

  • 第一个参数msqid 表示消息队列的用户级标识符
  • 第二个参数msgp 表示待发送的数据块
  • 第三个参数msgsz 表示所发送数据块的大小
  • 第四个参数msgflg 表示发送数据块的方式 一般默认为0即可

msgsnd函数的返回值说明:

  • msgsnd调用成功 返回0
  • msgsnd调用失败 返回-1

其中msgsnd函数的第二个参数必须为以下结构:

struct msgbuf{
    
    
	long mtype;       /* message type, must be > 0 */
	char mtext[1];    /* message data */
};

该结构当中的第二个成员mtext即为待发送的信息 当我们定义该结构时 mtext的大小可以自己指定

从消息队列获取数据

从消息队列获取数据我们需要用msgrcv函数,msgrcv函数的函数原型如下:

  ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

msgrcv函数的参数说明:

  • 第一个参数msqid 表示消息队列的用户级标识符
  • 第二个参数msgp 表示获取到的数据块,是一个输出型参数
  • 第三个参数msgsz 表示要获取数据块的大小
  • 第四个参数msgtyp 表示要接收数据块的类型

msgrcv函数的返回值说明:

  • msgsnd调用成功 返回实际获取到mtext数组中的字节数
  • msgsnd调用失败 返回-1

System V信号量

信号量相关概念

  • **进程互斥:**由于进程要求共享资源 而且有些资源需要互斥使用 因此各进程间竞争使用这些资源 进程的这种关系叫做进程互斥
  • **临界资源:**系统中某些资源一次只允许一个进程使用 称这样的资源为临界资源或互斥资源
  • 临界区: 在进程中涉及到临界资源的程序段叫临界区
  • 生命周期: IPC资源必须删除 否则不会自动删除 因为system V IPC的生命周期随内核

信号量相关数据结构

在系统当中也为信号量维护了相关的内核数据结构 信号量的数据结构如下:

struct semid_ds {
    
    
	struct ipc_perm sem_perm;       /* permissions .. see ipc.h */
	__kernel_time_t sem_otime;      /* last semop time */
	__kernel_time_t sem_ctime;      /* last change time */
	struct sem  *sem_base;      /* ptr to first semaphore in array */
	struct sem_queue *sem_pending;      /* pending operations to be processed */
	struct sem_queue **sem_pending_last;    /* last pending operation */
	struct sem_undo *undo;          /* undo requests on this array */
	unsigned short  sem_nsems;      /* no. of semaphores in array */
};

信号量数据结构的第一个成员也是ipc_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;
};

信号量相关函数

创建

创建信号量集我们需要用semget函数 semget函数的函数原型如下:

  int semget(key_t key, int nsems, int semflg);
  1. 创建信号量集也需要使用ftok函数生成一个key值 这个key值作为semget函数的第一个参数
  2. semget函数的第二个参数nsems 表示创建信号量的个数
  3. semget函数的第三个参数 与创建共享内存时使用的shmget函数的第三个参数相同
  4. 信号量集创建成功时 semget函数返回的一个有效的信号量集标识符(用户层标识符)

删除

删除信号量集我们需要用semctl函数 semctl函数的函数原型如下:

int semctl(int semid, int semnum, int cmd, ...);

操作

对信号量集进行操作我们需要用semop函数 semop函数的函数原型如下:

int semop(int semid, struct sembuf *sops, unsigned nsops);

进程互斥

进程间通信通过共享资源来实现 这虽然解决了通信的问题 但是也引入了新的问题 那就是通信进程间共用的临界资源 若是不对临界资源进行保护 就可能产生各个进程从临界资源获取的数据不一致等问题。

保护临界资源的本质是保护临界区 我们把进程代码中访问临界资源的代码称之为临界区 信号量就是用来保护临界区的 信号量分为二元信号量和多元信号量。

比如当前有一块大小为100字节的资源 我们若是一25字节为一份 那么该资源可以被分为4份 那么此时这块资源可以由4个信号量进行标识

在这里插入图片描述

信号量本质是一个计数器 在二元信号量中 信号量的个数为1(相当于将临界资源看成一整块) 二元信号量本质解决了临界资源的互斥问题 以下面的伪代码进行解释

在这里插入图片描述

根据以上代码 当进程A申请访问共享内存资源时 如果此时sem为1(sem代表当前信号量个数) 则进程A申请资源成功 此时需要将sem减减 然后进程A就可以对共享内存进行一系列操作 但是在进程A在访问共享内存时 若是进程B申请访问该共享内存资源 此时sem就为0了 那么这时进程B会被挂起 直到进程A访问共享内存结束后将sem加加 此时才会将进程B唤起 然后进程B再对该共享内存进行访问操作

在这种情况下 无论什么时候都只会有一个进程在对同一份共享内存进行访问操作 也就解决了临界资源的互斥问题

实际上 代码中计数器sem减减的操作就叫做P操作 而计数器加加的操作就叫做V操作 P操作就是申请信号量 而V操作就是释放信号量

在这里插入图片描述

system V IPC联系

通过对system V系列进程间通信的学习 可以发现共享内存、消息队列以及信号量

虽然它们内部的属性差别很大 但是维护它们的数据结构的第一个成员确实一样的 都是ipc_perm类型的成员变量

这样设计的好处就是 在操作系统内可以定义一个struct ipc_perm类型的数组 此时每当我们申请一个IPC资源 就在该数组当中开辟一个这样的结构

在这里插入图片描述

也就是说在内核当中只需要将所有的IPC资源的ipc_perm成员组织成数组的样子 然后用切片的方式获取到该IPC资源的起始地址 然后就可以访问该IPC资源的每一个成员了

猜你喜欢

转载自blog.csdn.net/meihaoshy/article/details/129625525