• 【Linux】进程间通信(学习复习兼顾)


    🏠 大家好,我是 兔7 ,一位努力学习C++的博主~💬

    🍑 如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步👀

    🚀 如有不懂,可以随时向我提问,我会全力讲解~

    🔥 如果感觉博主的文章还不错的话,希望大家关注、点赞、收藏三连支持一下博主哦~!

    🔥 你们的支持是我创作的动力!

    🧸 我相信现在的努力的艰辛,都是为以后的美好最好的见证!

    🧸 人的心态决定姿态!

    🚀 本文章CSDN首发!

    目录

    0. 前言

    1. 进程间通信介绍

    1.1 进程间通信目的

    那么进程间通信是如何做到的呢?

    进程通信的本质

    1.2 进程间通信发展

    1.3 进程间通信分类

    2. 管道

    2.1 什么是管道

    3. 匿名管道

    3.1 用fork来共享管道原理

    3.2 站在文件描述符角度-深度理解管道

    3.3 站在内核角度-管道本质

    3.4 管道特点

    4. 管道读写规则

    5. 命名管道

    6. system V共享内存

    6.1 共享内存示意图

    6.2 共享内存函数

    6.3 共享内存数据结构

    7. system V消息队列  -  了解

    8. system V信号量  - 了解

    进程互斥


    0. 前言

            此博客为博主以后复习的资料,所以大家放心学习,总结的很全面,每段代码都给大家发了出来,大家如果有疑问可以尝试去调试。

            大家一定要认真看图,图里的文字都是精华,好多的细节都在图中展示、写出来了,所以大家一定要仔细哦~

            感谢大家对我的支持,感谢大家的喜欢, 兔7 祝大家在学习的路上一路顺利,生活的路上顺心顺意~!

    1. 进程间通信介绍

    1.1 进程间通信目的

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

    数据传输:

            cat log.txt 一运行就会变成进程,一瞬间就把文件里的内容显示出来了,这叫做一个一个进程本身会把数据打印出来,而我们通过 | (管道) 的方式交给下一个进程,这就叫做进程传输。它们两个的目的是为了进程数据协同的,就相当于一个进程需要将它的数据发送给另一个进程(有点像流水线)。

    资源共享:

            比方说下棋吧,两个人要想下棋本身就是通过数据在通信,那么棋盘就是共享的资源,所以棋盘的数据肯定是要共享的。

    通知事件:

            进程之间的通信肯定带有某种意义,比方说一个进程给另一个进程输入 a ,就有可能是做一种操作,b 又是一种操作,这里后面只将管道的通信,通信的方式还有后面会讲的通过信号通信。


    那么进程间通信是如何做到的呢?

    1. 进程运行的时候是具有独立性的!(数据方面)
    2. 进程间通信,一般一定要借助第三方(OS)资源(第三方指的是进程双方)。
    3. 通信的本质就是 "数据的拷贝" 。
      进程A  ->  数据 "拷贝" 给 OS  ->  OS数据 "拷贝" 给进程B
      而且"拷贝"的前提肯定是IS一定要提供一段内存区域,能够被双方进程看到!

    进程通信的本质

            让不同的进程看到同一份资源(内存、文件内核缓冲等)

            而且资源是又谁(OS中的哪些模块)提供,就有了不同的进程间通信方式。

    1.2 进程间通信发展

    • 管道 System
    • V进程间通信
    • POSIX进程间通信(后面讲)

    1.3 进程间通信分类

    管道:

    • 匿名管道pipe
    • 命名管道

    System V IPC:

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

    POSIX IPC:

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

    2. 管道

    2.1 什么是管道

    • 管道是Unix中最古老的进程间通信的形式。
    • 我们把从一个进程连接到另一个进程的一个数据流称为一个"管道"
    • 管道只能够进行单向通信!

            具体上面的细节这里就不过多赘述了,因为前面讲这块的时候这幅图已经讲过很多次了。


            这里要说明,管道虽然用的是文件的方案,其实OS一定不会把数据刷新到磁盘,因为要是刷新到磁盘,肯定又会由IO参与,那么效率就会降低,而且这么做也没有必要。

            所以其实像我们这里的文件是一批把数据不会写到磁盘上的文件,换句话说:文件分为磁盘类的文件和内存类的文件,它们两个不一定完全相关,也不一定非得是一一对应的,有些文件只会在内存当中存在,不会在磁盘中存在。

    3. 匿名管道

    #include

    功能:创建一无名管道

    原型

    int pipe(int fd[2]);

    参数

    fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端

    返回值:成功返回0,失败返回错误代码

            这个数组(int pipe(int fd[2]);)的参数是输出型参数!

    1. int pipe(int fd[2])
    2. {
    3. fd[0] = open(read);
    4. fd[1] = open(write);
    5. ...
    6. }

            用 0 下标和 1 下标将 open 的文件描述符保存起来,然后通过参数把两个文件描述符返回,这就叫做输出型参数,所以我们调这个函数的目的是为了得到某些返回值!

    3.1 用fork来共享管道原理

    3.2 站在文件描述符角度-深度理解管道

            这套管道称之为匿名管道(没有名字)!

            我们通过看到文件描述符,就可以理解,一个文件被打开了两次。

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 #include
    5. 5 #include
    6. 6
    7. 7 // child->write father->read
    8. 8 int main()
    9. 9 {
    10. 10 int fd[2] = {0};
    11. 11 if(pipe(fd) < 0){
    12. 12 perror("pipe");
    13. 13 exit(-1);
    14. 14 }
    15. 15
    16. 16 pid_t id = fork();
    17. 17 if(id == 0){
    18. 18 //child
    19. 19 close(fd[0]);// 子进程要写关闭读
    20. 20 }
    21. 21
    22. 22 //father
    23. 23 close(fd[1]);// 父进程要写读关闭写
    24. 24 waitpid(id, NULL, 0);
    25. 25
    26. 26 return 0;
    27. 27 }

            这样我们就建立完成了进程间通信的管道信道。

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 #include
    5. 5 #include
    6. 6 #include
    7. 7
    8. 8 // child->write father->read
    9. 9 int main()
    10. 10 {
    11. 11 int fd[2] = {0};
    12. 12 if(pipe(fd) < 0){
    13. 13 perror("pipe");
    14. 14 exit(-1);
    15. 15 }
    16. 16
    17. 17 pid_t id = fork();
    18. 18 if(id == 0){
    19. 19 //child
    20. 20 close(fd[0]);// 子进程要写关闭读
    21. 21 const char* msg = "hello father, I am child!";
    22. 22 int count = 30;
    23. 23 while(count){
    24. 24 write(fd[1], msg, strlen(msg));
    25. 25 count--;
    26. 26 sleep(1);
    27. 27 }
    28. 28 }
    29. 29
    30. 30 //father
    31. 31 close(fd[1]);// 父进程要写读关闭写
    32. 32
    33. 33 char buffer[64];
    34. 34 while(1){
    35. 35 ssize_t s = read(fd[0], buffer, sizeof(buffer));
    36. 36 if(s > 0){
    37. 37 buffer[s] = '\0';
    38. 38 printf("child send to father: %s\n",buffer);
    39. 39 }
    40. 40 else if(s == 0){
    41. 41 printf("read file end!\n");
    42. 42 break;
    43. 43 }
    44. 44 else{
    45. 45 printf("read error!\n");
    46. 46 break;
    47. 47 }
    48. 48 }
    49. 49 waitpid(id, NULL, 0);
    50. 50
    51. 51 return 0;
    52. 52 }

            我们可以看到,有两个进程,一个父进程一个子进程,我们的父进程和子进程正在通信。子进程将 "hello father, I am child!" 发送给父进程,父进程再打印出来。

            其中有一个细节:就是当我们在子进程那里写入的时候,strlen() 是没有带上 '\0' 的,因为我们认为写入到文件里的都是一个一个字符,文件中不识别 '\0' 。

            而且现在所写的是进程间通信,而不是进程字符串通信,所以当我们读上来一个字符时,我们可以把字符当成一个个字符统一做某种处理(通过ASCLL进行计算),只不过这里是因为简化,我们将读到的一堆数据当成了字符串(因为我用 char buffer[64] 这个缓冲区接收这块数据),接收之后再将最后一个位置设为  '\0' 。

            所以虽然在 strlen() 中可以 +1 ,也就是带上 '\0' ,但是没那么做,是因为这样做是一个误导性的做法,误导是:进程间通信就只能串字符串。但这是不对的,因为传字符串只是通信的方式之一。

            所以我们这里的做法是串的时候就当作是一个一个字符,只不过我们认为在父进程读到之后将这一个个字符当作字符串,仅此而已~!

            此时虽然完成了通信,但是还是远远不够的。


    父子进程通信可不可以创建全局缓冲区来完成通信呢?

            当然是不可以的!之前说过,没创建子进程前,这个全局变量属于父进程的,创建之后,因为进程具有独立性,在父子进程没有写入的时候,这个全局变量里的数据是共享的,但是写入后,就会发生写时拷贝,此时子进程写入的数据,父进程是看不到的!所以肯定是不可以的!

            在通信的时候也不是用的父子的数据区,它们两个是通过文件指针的方式指向其它文件,这个文件是由OS提供的:进程A  ->  数据 "拷贝" 给 OS  ->  OS数据 "拷贝" 给进程B 。

            所以进程A通过系统调用接口写入,数据只不过是OS中维护的文件罢了,然后B再通过系统调用接口从OS中获取数据,所以进程A和B并没有使用A或B的内部的空间,而是使用OS的文件的空间,所以不需要写时拷贝~!

    3.3 站在内核角度-管道本质

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

    3.4 管道特点

            我们在开始的例子中有没有感到疑惑,为什么子进程是没休眠一秒钟打印一次,然后父进程就也想休眠了一秒钟一样接收一次呢?有的小伙伴可能会说:父进程不是一直在接收么(while),那么只要有了数据,就读取出来,没有数据就一直读取,直到有数据了才真正读的到。

            先说一个概念,在多执行流下(父子),看到的同一份资源,称为临界资源!那么有没有一种可能是:子进程正在写入的时候,父进程就来读取了呢?

            就是比方说,子进程先打印了一个 hello ,但是父进程不知道子进程的存在,父进程认为它就应该读取 hello ,但是子进程还没有写完,这时,父进程就读取了,那么子进程也不知道父进程存在,子进程把 hello 后面的继续写入了。如此往复,父进程读到的就会是一堆的乱码(数据不一致)。

            所以其实临界资源是要被保护起来的

            保护的方式就是通过同步与互斥,不过这里先主要讲  互斥:任何时候只能够有一个人正在使用某种资源。(比方说这个资源我正在使用,那么我就做上一个记号证明这个我在使用)

            所以讲到这里,我们再回到开始,父进程不是一直在 while 循环么,那么是不是父进程一直在运行呢?答案是否定的。

            因为1. 管道内部已经自动提供了互斥与同步机制

            如果有数据,那么就读取出来打印,如果没有数据,read 就会识别到管道里没有数据,那么父进程就不会再读取了,而是阻塞到管道处,等待子进程进行写入,所以并不是父进程 sleep 了,而是因为子进程写的慢,所以导致父进程必须等,就好像也 sleep 了。这种一个等另一个的现象就叫做同步!

    父进程不是 while(1) 么,那么父进程最后是怎么结束的呢?

            其实 2. 如果写端关闭,读端就会 read 返回 0 ,代表文件结束。所以父进程最后因为子进程结束了,而结束了。

            3. 如果打开的进程退出了,文件也就会被释放掉。这里的释放是在进程的角度上是释放的,这个文件并不一定会空间资源全被释放,而是取决于是不是和它相关的进程全退出了,如果全退出了文件才会真正释放。也就是说,进程的生命周期随进程

            这里我们的子进程可以一次写入 3、4、5 ... 个字符,父进程一次也可以读取 1、2、3 ... 个字符,也就是说,它们是按需去写入和读取,所以这其实说明 4. 管道是提供流式服务的

            计算机中还有两个概念:

    • 全双工(人在吵架的时候,两个人一直互相输出)
    • 半双工(人在正常沟通的时候,一个人输出,一个人听)

            所以这里的管道其实 5. 管道是半双工通信

            我们在上面说到的是父子见的通信,那么其实"孙子"和"爷爷"也是能够进行通信的,因为它们链接的可以是一个文件。所以还有一点是 6. 匿名管道,适合具有血缘关系的进程进行进程间通信,常用于父子

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

            那么比如说:一直让子进程写,而父进程休眠(父进程不读),那么管道既然是内存中的一段空间,如果管道满了会是怎么样呢?

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 #include
    5. 5 #include
    6. 6 #include
    7. 7
    8. 8 // child->write father->read
    9. 9 int main()
    10. 10 {
    11. 11 int fd[2] = {0};
    12. 12 if(pipe(fd) < 0){
    13. 13 perror("pipe");
    14. 14 exit(-1);
    15. 15 }
    16. 16
    17. 17 pid_t id = fork();
    18. 18 if(id == 0){
    19. 19 //child
    20. 20 close(fd[0]);// 子进程要写关闭读
    21. 21 const char* msg = "hello father, I am child!";
    22. 22 int count = 30;
    23. 23 while(count){
    24. 24 write(fd[1], msg, strlen(msg));
    25. 25 //count--;
    26. 26 printf(".");
    27. 27 fflush(stdout);
    28. 28 //sleep(1);
    29. 29 }
    30. 30 }
    31. 31
    32. 32 //father
    33. 33 close(fd[1]);// 父进程要写读关闭写
    34. 34
    35. 35 char buffer[64];
    36. 36 while(1){
    37. 37 sleep(1000);
    38. 38 ssize_t s = read(fd[0], buffer, sizeof(buffer));
    39. 39 if(s > 0){
    40. 40 buffer[s] = '\0';
    41. 41 printf("child send to father: %s\n",buffer);
    42. 42 }
    43. 43 else if(s == 0){
    44. 44 printf("read file end!\n");
    45. 45 break;
    46. 46 }
    47. 47 else{
    48. 48 printf("read error!\n");
    49. 49 break;
    50. 50 }
    51. 51 }
    52. 52 waitpid(id, NULL, 0);
    53. 53
    54. 54 return 0;
    55. 55 }

            我们可以看到,当我们写了那么多后,子进程就不写了,换言之,当子进程把管道写满时,写端就不写了,就被挂起了。


            那么如果写端就写了一点,而读端一直读会是什么情况呢?

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 #include
    5. 5 #include
    6. 6 #include
    7. 7
    8. 8 // child->write father->read
    9. 9 int main()
    10. 10 {
    11. 11 int fd[2] = {0};
    12. 12 if(pipe(fd) < 0){
    13. 13 perror("pipe");
    14. 14 exit(-1);
    15. 15 }
    16. 16
    17. 17 pid_t id = fork();
    18. 18 if(id == 0){
    19. 19 //child
    20. 20 close(fd[0]);// 子进程要写关闭读
    21. 21 const char* msg = "hello father, I am child!";
    22. 22 int count = 5;
    23. 23 while(count){
    24. 24 write(fd[1], msg, strlen(msg));
    25. 25 count--;
    26. 26 if(count == 2)
    27. 27 {
    28. 28 sleep(999);
    29. 29 }
    30. 30 fflush(stdout);
    31. 31 sleep(1);
    32. 32 }
    33. 33 }
    34. 34
    35. 35 //father
    36. 36 close(fd[1]);// 父进程要写读关闭写
    37. 37
    38. 38 char buffer[64];
    39. 39 while(1){
    40. 40 ssize_t s = read(fd[0], buffer, sizeof(buffer));
    41. 41 if(s > 0){
    42. 42 buffer[s] = '\0';
    43. 43 printf("child send to father: %s\n",buffer);
    44. 44 }
    45. 45 else if(s == 0){
    46. 46 printf("read file end!\n");
    47. 47 break;
    48. 48 }
    49. 49 else{
    50. 50 printf("read error!\n");
    51. 51 break;
    52. 52 }
    53. 53 }
    54. 54 waitpid(id, NULL, 0);
    55. 55
    56. 56 return 0;
    57. 57 }

            我们会发现,当他读到 3 个后就读不到任何内容了,因为子进程不写了,那么父进程再读的时候就没数据了,那么没数据能不能直接返回?肯定不可能。

            所以既然管道里没数据了,那么父进程就只能等,等管道里有数据再读。

            所以经过上面一对例子能看出:管道是自带同步与互斥机制的

            从读端不读,写端一直写,写满管道,写端就挂起。而读端一直读,写端不写了,读端把管道读完就不读了,因为管道没有数据了,所以读端就一直等。

            换言之,双方是有一个步调协同的一个过程


            如果父进程关闭了 fd[0] ,也就是关闭了读,子进程正常是写入的,那么会说什么情况呢?

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 #include
    5. 5 #include
    6. 6 #include
    7. 7
    8. 8 // child->write father->read
    9. 9 int main()
    10. 10 {
    11. 11 int fd[2] = {0};
    12. 12 if(pipe(fd) < 0){
    13. 13 perror("pipe");
    14. 14 exit(-1);
    15. 15 }
    16. 16
    17. 17 pid_t id = fork();
    18. 18 if(id == 0){
    19. 19 //child
    20. 20 close(fd[0]);// 子进程要写关闭读
    21. 21 const char* msg = "hello father, I am child!";
    22. 22 int count = 5;
    23. 23 while(count){
    24. 24 write(fd[1], msg, strlen(msg));
    25. 25 count--;
    26. 26 if(count == 2)
    27. 27 {
    28. 28 sleep(999);
    29. 29 }
    30. 30 fflush(stdout);
    31. 31 sleep(1);
    32. 32 }
    33. 33 }
    34. 34
    35. 35 //father
    36. 36 close(fd[1]);// 父进程要写读关闭写
    37. 37
    38. 38 char buffer[64];
    39. 39 while(1){
    40. 40 ssize_t s = read(fd[0], buffer, sizeof(buffer));
    41. 41 if(s > 0){
    42. 42 buffer[s] = '\0';
    43. 43 printf("child send to father: %s\n",buffer);
    44. 44 }
    45. 45 else if(s == 0){
    46. 46 printf("read file end!\n");
    47. 47 break;
    48. 48 }
    49. 49 else{
    50. 50 printf("read error!\n");
    51. 51 break;
    52. 52 }
    53. 53 close(fd[0]);
    54. 54 break;
    55. 55 }
    56. 56 sleep(3);
    57. 57 waitpid(id, NULL, 0);
    58. 58 printf("child quit!\n");
    59. 59
    60. 60 return 0;
    61. 61 }

            我先解释一下上面的逻辑,就是子进程先写,然后父进程读了一个后,就退出 while() ,然后等 3 秒后再等待子进程,等子进程退出后父进程打印 "child quit!" 。

            我们可以看到,开始是读到了一串字符串,然后等了 3 秒,然后子进程就退出了。

            其实这里是因为,如果父进程关闭了读文件,那么子进程的写入就没有了任何意义,所以 OS 不会让子进程干这种没有意义和浪费空间、浪费时间的事情,OS 直接就会把子进程给 kill 掉,所以等了 3 秒后会发现子进程退出了!

            那么最后是收到什么信号被 kill 掉的呢,接下来就来测试一下:

            我们运行了两次发现发出的都是 13 号信号 SIGPIPE 。


             那么这里我有点好奇,管道是可以被写满的,那么管道是有多大呢?接下来就来验证一下。

            我们可以看到,这里看到的管道大小好像是 4096 ,那么具体是不是,我们来测试一下:

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 #include
    5. 5 #include
    6. 6 #include
    7. 7
    8. 8 // child->write father->read
    9. 9 int main()
    10. 10 {
    11. 11 int fd[2] = {0};
    12. 12 if(pipe(fd) < 0){
    13. 13 perror("pipe");
    14. 14 exit(-1);
    15. 15 }
    16. 16
    17. 17 pid_t id = fork();
    18. 18 if(id == 0){
    19. 19 //child
    20. 20 close(fd[0]);// 子进程要写关闭读
    21. 21 const char* msg = "hello father, I am child!";
    22. 22 int count = 0;
    23. 23 char a = 'a';
    24. 24 while(1){
    25. 25 write(fd[1], &a, strlen(msg));
    26. 26 count++;
    27. 27 printf("%d\n",count);
    28. 28 }
    29. 29 close(fd[1]);
    30. 30 exit(0);
    31. 31 }
    32. 32
    33. 33 //father
    34. 34 close(fd[1]);// 父进程要写读关闭写
    35. 35
    36. 36 char buffer[64];
    37. 37 while(1){
    38. 38 sleep(999);
    39. 39 ssize_t s = read(fd[0], buffer, sizeof(buffer));
    40. 40 if(s > 0){
    41. 41 buffer[s] = '\0';
    42. 42 printf("child send to father: %s\n",buffer);
    43. 43 }
    44. 44 else if(s == 0){
    45. 45 printf("read file end!\n");
    46. 46 break;
    47. 47 }
    48. 48 else{
    49. 49 printf("read error!\n");
    50. 50 break;
    51. 51 }
    52. 52 close(fd[0]);
    53. 53 break;
    54. 54 }
    55. 55 sleep(3);
    56. 56 int status = 0;
    57. 57 waitpid(id, &status, 0);
    58. 58 printf("child quit!, signal: %d\n", status & 0x7F);
    59. 59
    60. 60 return 0;
    61. 61 }

             我们可以看到,我的系统是只有 2608 ,但其实 2.6 之后的大小是 65536 ,可能是我的系统有点拉跨~

            讲到这里匿名管道就讲解完了,虽然匿名管道可以进行通信,可是它的缺点很严重,就是常常适用于父子,如果是两个毫无关系的进程,那么这个通信就不好用了。

    4. 管道读写规则

    1. 当没有数据可读时

    • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
    • O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。

    2. 当管道满的时候

    • O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
    • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

    3. 如果所有管道写端对应的文件描述符被关闭,则read返回0

    4. 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出

    5. 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。

    6. 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

    5. 命名管道

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

            毫不相干的进程如何通信呢? -> 命名管道

            虽然命名管道看起来很像文件,但是本质上这个文件也是一个内存文件,只不过在内存上只有一个简单的映像,永远都是 0 ,就只占了一个名字罢了。两个进程只需要通过这个名字在内存中打开文件就好了。

            所以命名管道和匿名管道的区别就是:匿名管道是通过创建子进程的方式让不同的进程看到同一块资源,而命名管道是通过文件名的方式让不同的进程看到同一块资源。


            接下来就用命令行式来测试:

            我们可以看到,当我们用脚本每隔 1 秒在 fifo 中写入,然后让另一个进程从 fifo 中读取时会发现,都打印到了另一个进程的屏幕上。而且我们知道,这两个进程之间没有任何关系。

            当我们先管道第二个进程后会发现:

            第一个程序挂掉了,这就是因为,第二个进程是在读,我们上面说过,如果将读的进程关掉,那么写入就没有任何意义,所以OS就会把写的进程给 kill 掉,而且是发送 13 号信号 SIGPIPE 。由因为执行这个脚本的时候是由 bash 执行的,所以被 kill 掉后则就显示为退出了!

            如果我们关闭写的,那么读的就会被终止。

            所以我们通过前面讲的可以总结一下:

    1. 如果一直读不写,那么读的就会一直等管道里有数据再读(读被挂起了)。
    2. 如果一直写不读,当将管道写满时就不写了,等管道有空间了(等其他进程来读了),再继续写(写被挂起了)。
    3. 当正常通信时,关闭读,那么写就没有意义,所以写的进程就会被 kill 掉。
    4. 当正常通信时,关闭写,那么读就会被终止。

            但是我们也看到了,我们通过命令行式的方式使得两个进程之间完成了通信!


            接下来我就要用代码来实现进程间通信了:

            我要完成的目的是在用户端 client 输入数据(从屏幕中读取去),而在客户端 server 接收客户端的数据,然后再从客户端打印出来。

            但是这里需要注意的是,我们要想进行通信,那么就必须可以看到同一份资源,所以这里我就将共同的资源放到了头文件里,然后两个程序就可以看到同一份资源了:

    1. comm.h
    2. 1 #pragma once
    3. 2 #include
    4. 3 #include
    5. 4 #include
    6. 5 #include
    7. 6 #include
    8. 7 #include
    9. 8
    10. 9 #define FILE_NAME "myfifo"
    1. server.c
    2. 1 #include"comm.h"
    3. 2
    4. 3 int main()
    5. 4 {
    6. 5 if(mkfifo(FILE_NAME, 0644) < 0){
    7. 6 perror("myfifo");
    8. 7 return 1;
    9. 8 }
    10. 9
    11. 10 int fd = open(FILE_NAME, O_RDONLY);
    12. 11 if(fd < 0){
    13. 12 perror("open");
    14. 13 return 2;
    15. 14 }
    16. 15
    17. 16 char msg[128];
    18. 17 while(1){
    19. 18 msg[0] = 0;
    20. 19 ssize_t s = read(fd, msg, sizeof(msg)- 1);
    21. 20 if(s > 0){
    22. 21 msg[s] = 0;
    23. 22 printf("client# %s\n", msg);
    24. 23 }
    25. 24 else if(s == 0){
    26. 25 printf("client quit!\n");
    27. 26 break;
    28. 27 }
    29. 28 else{
    30. 29 printf("read error!\n");
    31. 30 break;
    32. 31 }
    33. 32 }
    34. 33 close(fd);
    35. 34
    36. 35 return 0;
    37. 36 }
    1. client.c
    2. 1 #include"comm.h"
    3. 2
    4. 3 int main()
    5. 4 {
    6. 5 int fd = open(FILE_NAME, O_WRONLY);
    7. 6 if(fd < 0){
    8. 7 perror("open");
    9. 8 return 1;
    10. 9 }
    11. 10
    12. 11 char msg[128];
    13. 12 while(1){
    14. 13 msg[0] = 0;
    15. 14 printf("Please Enter# ");
    16. 15 fflush(stdout);
    17. 16 ssize_t s = read(0, msg, sizeof(msg));
    18. 17 if(s > 0){
    19. 18 msg[s] = 0;
    20. 19 write(fd, msg, strlen(msg));
    21. 20 }
    22. 21 }
    23. 22
    24. 23 close(fd);
    25. 24 return 0;
    26. 25 }

            我们可以看到这样的效果。

            而且我们会发现,这两个进程是没有任何关系的,但是它们两个就可以进行通信。而且除了 server 创建一个管道文件,其它的操作和操作普通文件没有区别。

            那么接下来就操作:情况是不是在命名管道开头的那幅图的情况:

            首先先把 server 中读取的操作去掉:

            然后进行下方操作:

            我们可以看到,我已经写了好多消息了,但是我们发现 myfifo 的大小一直都是 0 ,换句话说就是我写的消息已经被写到管道的缓冲区里了,但是 server端 并没有读取,所以写的消息还是在内存里,但是又因为 myfifo 的大小为 0 ,就说明数据并没有刷新到磁盘,也就意味着,双方通信依旧是在内存中通信的,和匿名管道的底层原理是一样的,它们采用的都是文件通信。

            所以这下也就更能理解上面的那副图了吧。


            在上面的图,我们可以发现,虽然是可以完成通信了,但是我们输入 ls pwd 这种命令,他只是当作字符去处理了,但是现在不想这么操作,就像模拟一下 xshell 的基本运行原理,而且我们发现在服务端输出的时候还带有空行:

    1. server.c
    2. 1 #include"comm.h"
    3. 2
    4. 3 int main()
    5. 4 {
    6. 5 if(mkfifo(FILE_NAME, 0644) < 0){
    7. 6 perror("myfifo");
    8. 7 return 1;
    9. 8 }
    10. 9
    11. 10 int fd = open(FILE_NAME, O_RDONLY);
    12. 11 if(fd < 0){
    13. 12 perror("open");
    14. 13 return 2;
    15. 14 }
    16. 15
    17. 16 char msg[128];
    18. 17 while(1){
    19. 18 msg[0] = 0;
    20. 19 ssize_t s = read(fd, msg, sizeof(msg)- 1);
    21. 20 if(s > 0){
    22. 21 msg[s] = 0;
    23. 22 printf("client# %s\n", msg);
    24. 23 if(fork() == 0){
    25. 24 //child
    26. 25 execlp(msg, msg, NULL);
    27. 26 exit(-1);
    28. 27 }
    29. 28 waitpid(-1, NULL, 0);//-1表示等待任意子进程
    30. 29 }
    31. 30 else if(s == 0){
    32. 31 printf("client quit!\n");
    33. 32 break;
    34. 33 }
    35. 34 else{
    36. 35 printf("read error!\n");
    37. 36 break;
    38. 37 }
    39. 38 }
    40. 39 close(fd);
    41. 40
    42. 41 return 0;
    43. 42 }
    1. client.c
    2. 1 #include"comm.h"
    3. 2
    4. 3 int main()
    5. 4 {
    6. 5 int fd = open(FILE_NAME, O_WRONLY);
    7. 6 if(fd < 0){
    8. 7 perror("open");
    9. 8 return 1;
    10. 9 }
    11. 10
    12. 11 char msg[128];
    13. 12 while(1){
    14. 13 //msg[0] = 0;
    15. 14 printf("Please Enter# ");
    16. 15 fflush(stdout);
    17. 16 ssize_t s = read(0, msg, sizeof(msg));
    18. 17 if(s > 0){
    19. 18 msg[s-1] = 0;//注意这里为了输出的时候没空行
    20. 19 write(fd, msg, strlen(msg));
    21. 20 }
    22. 21 }
    23. 22
    24. 23 close(fd);
    25. 24 return 0;
    26. 25 }

            通过 server.c 中 23-28 行创建子进程,让子进程执行命令,并让父进程等待。

            通过 client.c 中低 18 行 msg[s-1] = 0 ,让打印出来的空行去除。空行的原因是我们在 client 端输入的时候,最后要靠回车确定输入完,所以在写入的时候,也会把 "\n" 写进去,所以将 s-1 的位置设为 "\0" 就可以将 "\n" 消除!

            最后我们看到,让我们输入 who pwd top ... ... 的时候,发现在 server 端执行了命令(当然有的命令还是不可以,因为没有带选项),所以这也就像我说的,我们的 client 端就像我们 xshell 终端,这就像进程通信版的 xshell 通信原理,只不过 xshell 是一个我们下载的客户端,它实际上是将我们输入的命令打包,通过网络发给网络上的进程,只不过现在是本地通信。但是我们依然可以通过一个进程遥控另一个进程上的任务。


            所以其实这里我们还可以让进程间进行文本分析(10+20),这也就印证了其实文本间通信不是只能给对方发字符串。

    1. server.c
    2. 1 #include"comm.h"
    3. 2
    4. 3 int main()
    5. 4 {
    6. 5 if(mkfifo(FILE_NAME, 0644) < 0){
    7. 6 perror("myfifo");
    8. 7 return 1;
    9. 8 }
    10. 9
    11. 10 int fd = open(FILE_NAME, O_RDONLY);
    12. 11 if(fd < 0){
    13. 12 perror("open");
    14. 13 return 2;
    15. 14 }
    16. 15
    17. 16 char msg[128];
    18. 17 while(1){
    19. 18 msg[0] = 0;
    20. 19 ssize_t s = read(fd, msg, sizeof(msg)- 1);
    21. 20 if(s > 0){
    22. 21 msg[s] = 0;
    23. 22 printf("client# %s\n", msg);
    24. 23 char* p = msg;
    25. 24 const char* lable = "+-*/%";
    26. 25 int flag = 0;
    27. 26 while(*p){
    28. 27 switch(*p){
    29. 28 case '+':
    30. 29 flag = 0;
    31. 30 break;
    32. 31 case '-':
    33. 32 flag = 1;
    34. 33 break;
    35. 34 case '*':
    36. 35 flag = 2;
    37. 36 break;
    38. 37 case '/':
    39. 38 flag = 3;
    40. 39 break;
    41. 40 case '%':
    42. 41 flag = 4;
    43. 42 break;
    44. 43 }
    45. 44 ++p;
    46. 45 }
    47. 46 char* data1 = strtok(msg, "+-*/%");
    48. 47 char* data2 = strtok(NULL, "+-*/%");
    49. 48 int x = atoi(data1);
    50. 49 int y = atoi(data2);
    51. 50 int z = 0;
    52. 51 switch(flag){
    53. 52 case 0:
    54. 53 z = x + y;
    55. 54 break;
    56. 55 case 1:
    57. 56 z = x - y;
    58. 57 break;
    59. 58 case 2:
    60. 59 z = x * y;
    61. 60 break;
    62. 61 case 3:
    63. 62 z = x / y;
    64. 63 break;
    65. 64 case 4:
    66. 65 z = x % y;
    67. 66 break;
    68. 67 }
    69. 68 printf("%d %c %d = %d\n",x ,lable[flag], y, z);
    70. 69
    71. 70 //if(fork() == 0){
    72. 71 // //child
    73. 72 // execlp(msg, msg, NULL);
    74. 73 // exit(-1);
    75. 74 //}
    76. 75 //waitpid(-1, NULL, 0);//-1表示等待任意子进程
    77. 76 }
    78. 77 else if(s == 0){
    79. 78 printf("client quit!\n");
    80. 79 break;
    81. 80 }
    82. 81 else{
    83. 82 printf("read error!\n");
    84. 83 break;
    85. 84 }
    86. 85 }
    87. 86 close(fd);
    88. 87
    89. 88 return 0;
    90. 89 }

            我们可以看到,这样就完成了这个任务,这样做的原因可能是 client 端一直在接收任务,然后 client 就给 server 端派送计算任务,这样就完成了任务分工。

            所以不要只把进程间通信理解成就是一个进程给另一个进程发字符串,就像一个聊天工具一样,其实不然,其中的 msg 可能是一个任务,是一个信息,是一个命令。

            它的核心应用场景就是一个进程完成任务的时候成本太高了,它可以将任务派发给其它进程,让其它进程帮它去完成,这样就体现了通信的价值,叫做:多进程任务协同


            接下来还有一个操作就是:同时打开两个文件,从一个文件里读,然后再写到另一个文件中,这样就实现了将一个文件里的内容拷贝到另一个文件的任务:

    1. server.c
    2. 1 #include"comm.h"
    3. 2
    4. 3 int main()
    5. 4 {
    6. 5 if(mkfifo(FILE_NAME, 0644) < 0){
    7. 6 perror("myfifo");
    8. 7 return 1;
    9. 8 }
    10. 9
    11. 10 int fd = open(FILE_NAME, O_RDONLY);
    12. 11 if(fd < 0){
    13. 12 perror("open");
    14. 13 return 2;
    15. 14 }
    16. 15
    17. 16 int out = open("file-cp.txt", O_WRONLY|O_CREAT, 0644);
    18. 17 if(out < 0){
    19. 18 perror("open");
    20. 19 exit(4);
    21. 20 }
    22. 21
    23. 22 char msg[128];
    24. 23 while(1){
    25. 24 msg[0] = 0;
    26. 25 ssize_t s = read(fd, msg, sizeof(msg)- 1);
    27. 26 if(s > 0){
    28. 27 write(out, msg, s);
    29. 83 }
    30. 84 else if(s == 0){
    31. 85 printf("client quit!\n");
    32. 86 break;
    33. 87 }
    34. 88 else{
    35. 89 printf("read error!\n");
    36. 90 break;
    37. 91 }
    38. 92 }
    39. 93 close(fd);
    40. 94 close(out);
    41. 95 return 0;
    42. 96 }
    1. client.c
    2. 1 #include"comm.h"
    3. 2
    4. 3 int main()
    5. 4 {
    6. 5 int fd = open(FILE_NAME, O_WRONLY);
    7. 6 if(fd < 0){
    8. 7 perror("open");
    9. 8 return 1;
    10. 9 }
    11. 10
    12. 11 int in = open("file.txt", O_RDONLY);
    13. 12 if(in < 0){
    14. 13 perror("open");
    15. 14 exit(4);
    16. 15 }
    17. 16 char msg[128];
    18. 17 while(1){
    19. 21 dup2(in, 0);
    20. 22 ssize_t s = read(0, msg, sizeof(msg));
    21. 23 //ssize_t s = read(in, msg, sizeof(msg));
    22. 24 if(s == sizeof(msg)){
    23. 25 msg[s-1] = 0;//注意这里为了输出的时候没空行
    24. 26 write(fd, msg, s);
    25. 27 }
    26. 28 else if(s < sizeof(msg)){
    27. 29 write(fd, msg, s);
    28. 30 printf("read end of file!\n");
    29. 31 break;
    30. 32 }
    31. 33 else{
    32. 34 break;
    33. 35 }
    34. 36 }
    35. 37
    36. 38 close(fd);
    37. 39 return 0;
    38. 40 }

            我们可以看到,这样我们就完成了一次文件拷贝,是一个进程将文件的数据拷贝给了另一个进程创建的文件。

            大家会感到奇怪,其实是因为我们现在只是在一台机器上进程进程间通信,但如果将 server 看成我们的 centos 服务器,client 堪称 window xshell ,而我们现在的操作就是将本地文件上传给 centos 服务器,这么一看,是不是就不奇怪了。

            也就是开始我们讲到的 rz 命令!

            其实我们之前就用过管道,但是我们只限于用上,接下来就带大家继续看看我们之前用的管道:

            我们可以看到 | 就是管道,而且可以看到 cat  grep 都是命令,所以 | 左边的命令要运行起来,右边的命令也要运行起来,所以它们两个运行起来之后肯定是两个进程。

            那么这里的管道是命名管道还是匿名管道呢?

            那么接下来就跟着大家验证一下:

            我们就可以看到,这三个 sleep 的 ppid 是一个进程,也就是说,它们的父进程都是同一个进程,所以这三个 sleep 是兄弟关系,但是现在只是有匿名管道的一个前提,也就是是兄弟关系。但是这里还要告诉大家,如果是命名管道的话,我们在运行的时候,在目录下是有像 myfifo 这种在磁盘上对应的一个文件名,但是我们在操作 | 这个管道的时候,是并不存在的,所以这个就是匿名管道啦~!

            以上就是所有管道的内容。

    6. system V共享内存

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

    6.1 共享内存示意图

    6.2 共享内存函数

            我们上面讲的管道通信本质上是基于文件的,OS没有做过多的设计工作。

            而 system V进程间通信是OS特地设计的通信方式(想尽一切办法让不同的进程看到同一份资源)。

            其实这块共享内存也不是你申请就立马给你,操作系统也有资源内存管理机制。

            接下来就要说一下共享内存建立的过程了。

    1. 申请共享内存
    2. 共享内存挂接到地址空间
    3. 去关联共享内存
    4. 释放共享内存

            接下来说一下调用的接口:

    shmget函数

    功能:用来创建共享内存

    原型

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

    参数

              key:这个共享内存段名字

              size:共享内存大小

              shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的

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

             但是再通信双方中,肯定有一个是创建 shm ,一个去获取。

            共享内存的查看方式 ipcs -m

    1. comm.h
    2. 1 #ifndef _COMM_H_
    3. 2 #define _COMM_H_
    4. 3
    5. 4 #include
    6. 5 #include
    7. 6 #include
    8. 7 #include
    9. 8
    10. 9 #define PATHNAME "/home/lzh/2022/July/7.28/shm"
    11. 10 #define PROJ_ID 0x7777
    12. 11 #define SIZE 4096 // 一个内存页的大小
    13. 12
    14. 13 #endif
    1. server.h
    2. 1 #include"comm.h"
    3. 2
    4. 3 int main()
    5. 4 {
    6. 5 key_t k = ftok(PATHNAME, PROJ_ID);
    7. 6 if(k < 0){
    8. 7 printf("frok error!\n");
    9. 8 return 1;
    10. 9 }
    11. 10
    12. 11 printf("%x\n",k);
    13. 12
    14. 13 int shm = shmget(k, SIZE, IPC_CREAT|IPC_EXCL);
    15. 14 if(shm < 0){
    16. 15 perror("shmget");
    17. 16 return 2;
    18. 17 }
    19. 18 return 0;
    20. 19 }

            我们可以看到,当我们运行完 server 后,进程一瞬间就结束了,但是当我们用 ipcs -m 查看的时候,发现它还是存在的:

            共享内存的删除方式 ipcrm -m *

            要注意的是这里不是用 key 去删的哦,需要用 shmid 去删,这样才能删掉。

            我们会发现,再创建的时候 key 是不变的。

            其实这里的 key 是保证了在系统层面上共享内存的唯一性,shm 保证的是在用户层通过这个 id 能够找到这个共享内存资源,这两个之间的关系有点像我们之前将文件时讲到的 fd 和 FILE* 。 fd 对应的是 key, FILE* 对应的是 shmid 。

            当然我们也不能在进程中国创建了,再回来用命令行操作去找,然后再去释放:

    shmctl函数

    功能:用于控制共享内存

    原型

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

    参数

              shmid:由shmget返回的共享内存标识码

              cmd:将要采取的动作(有三个可取值)

              buf:指向一个保存着共享内存的模式状态和访问权限的数据结构

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

    1. server.c
    2. 1 #include"comm.h"
    3. 2
    4. 3 int main()
    5. 4 {
    6. 5 key_t k = ftok(PATHNAME, PROJ_ID);
    7. 6 if(k < 0){
    8. 7 printf("frok error!\n");
    9. 8 return 1;
    10. 9 }
    11. 10
    12. 11 printf("%x\n",k);
    13. 12
    14. 13 int shm = shmget(k, SIZE, IPC_CREAT|IPC_EXCL);
    15. 14 if(shm < 0){
    16. 15 perror("shmget");
    17. 16 return 2;
    18. 17 }
    19. 18
    20. 19 sleep(10);
    21. 20
    22. 21 shmctl(shm, IPC_RMID, NULL);
    23. 22
    24. 23 sleep(10);
    25. 24
    26. 25 return 0;
    27. 26 }

            我们可以看到,经过一段时间,共享内存被创建出来了,又经过一段时间,共享内存就被释放了。

            我们现在有能力创建出这块共享内存了,那么第二步我们就要想着如何进行映射了,也就是将这块共享内存和我们的进程地址空间进行映射,怎么映射本质上就是修改页表,但是现在要做的是在操作上进行映射。

    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,表示连接操作用来只读共享内存

    shmdt函数

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

    原型

             int shmdt(const void *shmaddr);

    参数

             shmaddr: 由shmat所返回的指针

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

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

            我们要挂接的时候,本质上就是将共享内存映射到自己的进程地址空间中,但是是映射到进程地址空间的哪个区域是不知道的,所以我们第二个参数 shmaddr 一般情况下都不用管,直接给 NULL 就好,让操作系统去设置。第三个就是在挂接这块共享内存时设置某些属性,比方说:SHM_RDONLY ,当我们设置为 0 时就默认设置成读写了! 

            其实这里的返回这特别重要

            接下来就进行挂接了:

    1. 1 #include"comm.h"
    2. 2
    3. 3 int main()
    4. 4 {
    5. 5 key_t k = ftok(PATHNAME, PROJ_ID);
    6. 6 if(k < 0){
    7. 7 perror("ftok");
    8. 8 return 1;
    9. 9 }
    10. 10 printf("%x\n",k);
    11. 11 int shmid = shmget(k, SIZE, IPC_CREAT|IPC_EXCL|0644);
    12. 12 if(shmid < 0){
    13. 13 perror("shmget");
    14. 14 return 2;
    15. 15 }
    16. 16
    17. 17 printf("shmid: %d\n", shmid);
    18. 18
    19. 19 printf("attach begin!\n");
    20. 20 sleep(3);
    21. 21 char* mem = shmat(shmid, NULL, 0);
    22. 22 printf("attach end!\n");
    23. 23 sleep(3);
    24. 24
    25. 25 shmctl(shmid, IPC_RMID, NULL);
    26. 26
    27. 27 return 0;
    28. 28 }

            我们可以看到,经过一段时间,就关联上了,又经过一段时间,又释放共享区了。

            但是这样有点不好,应该让一个进程和共享内存先取消关联,一旦它取消了就证明资源释放完了,万一人家还没释放完,你就给共享资源先释放了(人家还有可能正在对资源操作呢)。

    1. 1 #include"comm.h"
    2. 2
    3. 3 int main()
    4. 4 {
    5. 5 key_t k = ftok(PATHNAME, PROJ_ID);
    6. 6 if(k < 0){
    7. 7 perror("ftok");
    8. 8 return 1;
    9. 9 }
    10. 10 printf("%x\n",k);
    11. 11 int shmid = shmget(k, SIZE, IPC_CREAT|IPC_EXCL|0644);
    12. 12 if(shmid < 0){
    13. 13 perror("shmget");
    14. 14 return 2;
    15. 15 }
    16. 16
    17. 17 printf("shmid: %d\n", shmid);
    18. 18
    19. 19 sleep(3);
    20. 20 char* mem = shmat(shmid, NULL, 0);
    21. 21 sleep(3);
    22. 22
    23. 23 sleep(3);
    24. 24 shmdt(mem);
    25. 25 sleep(3);
    26. 26
    27. 27 shmctl(shmid, IPC_RMID, NULL);
    28. 28
    29. 29 return 0;
    30. 30 }

            我们可以看到从创建到挂接到取消挂接再到释放的一整个共享内存使用的生命周期。

            那么使用就和使用堆空间一样的用 mem 这个虚拟内存地址使用。而且是在挂接之后,取消挂接之前的那段使用~

            当我们更改创建的大小的时候,会发现大小就是 4097 ,也就是我让它创建多少,它这里就显示多少,但是其实这里的 size 是要对齐的,或者变成 PAGE_SIZE(4096 bytes)(一页数据) 的整数倍。

            我们知道计算机分配内存的基本单位是字节,访存的基本单位是字节,这话是没错的,但是在操作系统的层面上,在进行内存的申请和释放(一般),尤其是和外设进行IO的时候,内存并不是根据字节去操作的,而是按照页框和页帧为单位的。

            但是我们看到的是 4097 ,不是 4096*2 ,这是因为在操作系统底层分配了两页,但是我们就要了 4097 ,既然我们要了 4097 ,那么操作系统就只让我们看到 4097 ,也就是操作系统绝对不会少给空间,但也不会多给空间,少给了就会出问题,多给了也会出问题哦~

            接下来我们就要实现通信了:

            既然要实现通信,那么就就必须保证两个进程可以看到同一份资源,而且一个创建,另一个就不需要创建了,最后回收的工作也是交给创建的人去回收,那么既然要看到同一份资源,我想大家也知道该怎么做了,当然就是获得同一个 key,那么这时我们也能理解了为什么 ftok 中我们要把 pathname 和 proj_id 写的一样,这样做的目的就是为了获得同一个 key ,进而在操作系统上可以找到彼此。

    1. client.c
    2. 1 #include"comm.h"
    3. 2
    4. 3 int main()
    5. 4 {
    6. 5 key_t k = ftok(PATHNAME, PROJ_ID);
    7. 6 if(k < 0){
    8. 7 perror("ftor");
    9. 8 return 1;
    10. 9 }
    11. 10 int shmid = shmget(k, SIZE, IPC_CREAT);
    12. 11 if(shmid < 0){
    13. 12 perror("shmget");
    14. 13 return 2;
    15. 14 }
    16. 15
    17. 16 char* mem = shmat(shmid, NULL, 0);
    18. 17
    19. 18 while(1){
    20. 19 sleep(1);
    21. 20 }
    22. 21
    23. 22 shmdt(mem);
    24. 23
    25. 24 return 0;
    26. 25 }
    1. server.c
    2. 1 #include"comm.h"
    3. 2
    4. 3 int main()
    5. 4 {
    6. 5 key_t k = ftok(PATHNAME, PROJ_ID);
    7. 6 if(k < 0){
    8. 7 perror("ftok");
    9. 8 return 1;
    10. 9 }
    11. 10 printf("%x\n",k);
    12. 11 int shmid = shmget(k, SIZE, IPC_CREAT|IPC_EXCL|0644);
    13. 12 if(shmid < 0){
    14. 13 perror("shmget");
    15. 14 return 2;
    16. 15 }
    17. 16
    18. 17 printf("shmid: %d\n", shmid);
    19. 18
    20. 19 sleep(5);
    21. 20 char* mem = shmat(shmid, NULL, 0);
    22. 21
    23. 22 while(1){
    24. 23 sleep(1);
    25. 24 }
    26. 25
    27. 26
    28. 27 shmdt(mem);
    29. 28
    30. 29 shmctl(shmid, IPC_RMID, NULL);
    31. 30
    32. 31 return 0;
    33. 32 }

            在我先运行 server 再运行 client 时,先是创建了共享内存,server 先链接,然后再让 client 链接进去,然后就看到这种情况。

            那么接下来就让它们进行通信:

    1. client.c
    2. 1 #include"comm.h"
    3. 2
    4. 3 int main()
    5. 4 {
    6. 5 key_t k = ftok(PATHNAME, PROJ_ID);
    7. 6 if(k < 0){
    8. 7 perror("ftor");
    9. 8 return 1;
    10. 9 }
    11. 10 int shmid = shmget(k, SIZE, IPC_CREAT);
    12. 11 if(shmid < 0){
    13. 12 perror("shmget");
    14. 13 return 2;
    15. 14 }
    16. 15
    17. E> 16 char* mem = shmat(shmid, NULL, 0);
    18. 17
    19. 18 int i = 0;
    20. 19 while(1){
    21. 20 mem[i] = 'A' + i;
    22. 21 sleep(1);
    23. 22 i++;
    24. 23 mem[i] = 0;
    25. 24 }
    26. 25
    27. 26 shmdt(mem);
    28. 27
    29. 28 return 0;
    30. 29 }
    1. server.c
    2. 1 #include"comm.h"
    3. 2
    4. 3 int main()
    5. 4 {
    6. 5 key_t k = ftok(PATHNAME, PROJ_ID);
    7. 6 if(k < 0){
    8. 7 perror("ftok");
    9. 8 return 1;
    10. 9 }
    11. 10 printf("%x\n",k);
    12. 11 int shmid = shmget(k, SIZE, IPC_CREAT|IPC_EXCL|0644);
    13. 12 if(shmid < 0){
    14. 13 perror("shmget");
    15. 14 return 2;
    16. 15 }
    17. 16
    18. 17 printf("shmid: %d\n", shmid);
    19. 18
    20. 19
    21. 20 char* mem = shmat(shmid, NULL, 0);
    22. 21
    23. 22 while(1){
    24. 23 printf("client msg# %s\n",mem );
    25. 24 sleep(1);
    26. 25 }
    27. 26
    28. 27
    29. 28 shmdt(mem);
    30. 29
    31. 30 shmctl(shmid, IPC_RMID, NULL);
    32. 31
    33. 32 return 0;
    34. 33 }

            这就是我们进程间通信的关于共享内存的相关操作。


            接下来就有问题需要大家思考了:

    我们使用管道的时候有没有使用系统调用接口呢?

            pipe[0]  pipe[1]  read()  write()

            所以是有的!

    读写共享内存的时候,有没有使用OS接口?

            没有!当一端对空间进行修改的时候,另一端立马就能看到,不用发生任何拷贝,

            而对于共享内存而言:将A内存的数据直接向共享内存里写,B立马就能看到,也就是说用共享内存进行通信时,就没有 w 、r 这种拷贝了,也就是至少减少了 1 到 2 次的拷贝时间。

            换言之,共享内存是所有进程间通信中速度最快的!其中原因之一1. 拷贝次数少


            我们在前面讲管道的时候说过,如果写端写的很慢,读端就要等,这是因为管道自带同步和互斥。

            那么我们让 server 端一直跑,client 还是每隔一秒进行写。

            因为不会动态图,所以大家去测试一下看看效果吧,具体效果是:虽然 client 写的特别慢,但是 server 不等 client,就像不知道 client 的存在一样,client 一次写一个,而 server 一次读完,但是没有等它,但是它应该是让 client 每隔一秒写一个,server 就该每隔一秒读一个。

            所以共享内存快的原因之二就来了,但是这里是缺点:2. 不提供任何保护机制(同步与互斥)

            没有同步与互斥就会有问题~!就像管道里说到的,有没有可能 client 在写,写到一半的时候, server 就读走了,但是需求是 client 要写完。

            所以其实共享内存就是临界资源,多个进程的临界资源可能就因为共享而导致相互干扰,而共享内存就具有很强的干扰现象。所以这里就要有相关的锁机制来保护同步和互斥问题,但是在这里 system V 的锁不讲,这里我们使用信号量(sem)保证它们可以实现互斥。

    6.3 共享内存数据结构

    1. struct shmid_ds {
    2. struct ipc_perm shm_perm; /* operation perms */
    3. int shm_segsz; /* size of segment (bytes) */
    4. __kernel_time_t shm_atime; /* last attach time */
    5. __kernel_time_t shm_dtime; /* last detach time */
    6. __kernel_time_t shm_ctime; /* last change time */
    7. __kernel_ipc_pid_t shm_cpid; /* pid of creator */
    8. __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
    9. unsigned short shm_nattch; /* no. of current attaches */
    10. unsigned short shm_unused; /* compatibility */
    11. void *shm_unused2; /* ditto - used by DIPC */
    12. void *shm_unused3; /* unused */
    13. };

            所以经过上面的源代码我们得到的结论:

             有的伙伴可能不知道为什么在进程内部不用 key 呢,是因为虽然 key 是可以的,但是 shmid 更简单,因为 shmget 肯定要有返回值,所以这个返回值很重要,而且尽量不要把操作系统内的数据暴露给用户。

            所以这也能解释为什么我们释放共享内存的时候用 ipcrm -m (shmid) 而不是 ipcrm -m (key) 了,因为命令也是用户层,当然用 key 是不可以的。前面也已经说过了。

    7. system V消息队列  -  了解

    • 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
    • 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
    • 特性方面
          IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核

            其实它的函数接口跟内存共享的接口有相似的地方,但是接下来我要带大家看这个东西:

            我们会发现,不管是我们上面学的共享内存还是现在要讲的消息队列还是下面要说的信号量,都有 struct ipc_perm 这个结构体。

    8. system V信号量  - 了解

            信号量主要用于同步和互斥的,下面先来看看什么是同步和互斥。

    进程互斥

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

     要申请一个资源,必须进程直接占有这个资源么?

            肯定不是,比方说看电影买电影票,只要那个座位是你的,即使你不去,那个位置也是你的。

            换句话说:只要申请信号量成功了,就一定有你的资源。

            当我们进行多进程访问共享资源的时候,先要进行P操作,然后访问资源,再进行V操作。每个进程都要这样。

            那么现在就有问题,为什么要加P操作,也就是申请信号量呢?因为这是共享资源啊,但是有没有发现一个问题,进程A或者B在访问共享资源的时候,都必须进行信号量的访问,也就是对计数器--,那么有问题:

            信号量本身也是临界资源!!!因为要访问同一个资源,信号量本身就需要先被进程A、B看到,那么这里就有问题了,信号量本身就是要去保护临界资源,但是我本身就是临界资源,谁来保护我。

            所以PV操作必须保证原子性,要么操作了,要么没操作。

             如上就是 进程间通信 的所有知识,如果大家喜欢看此文章并且有收获,可以支持下 兔7 ,给 兔7 三连加关注,你的关注是对我最大的鼓励,也是我的创作动力~!

            再次感谢大家观看,感谢大家支持!

  • 相关阅读:
    TYPE-C接口桌面显示器:视频与充电的双重革新
    从淘宝数据分析产品需求(商品销量总销量精准月销)
    如何做代币分析:以 BNB 币为例
    C#_Win32_PInvoke源码生成器
    图像操作的基石Numpy
    计算机毕业设计Java自由教学平台(源码+系统+mysql数据库+lw文档)
    5G在油气田智慧勘探井场建设中的应用探讨
    [Spring] Bean生命周期
    jdbc&数据库连接池&jdbcTemplate教程
    小程序 :自定义tabbar (名称后台获取)
  • 原文地址:https://blog.csdn.net/weixin_69725192/article/details/125991937