• 进程间通信 --- system V三种通信方式(图文案例讲解)


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

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

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

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

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

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

    🧸 人的心态决定姿态!

    🚀 本文章CSDN首发!

    目录

    0. 前言

    1. system V共享内存

    1.1 共享内存示意图

    1.2 共享内存函数

    1.3 共享内存数据结构

    2. system V消息队列  -  了解

    8. system V信号量  - 了解

    进程互斥


    0. 前言

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

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

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

    1. system V共享内存

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

    1.1 共享内存示意图

    1.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)保证它们可以实现互斥。

    1.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 是不可以的。前面也已经说过了。

    2. system V消息队列  -  了解

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

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

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

    8. system V信号量  - 了解

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

    进程互斥

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

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

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

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

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

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

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

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

            到这里 system V 的三种方式就讲解完了。

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

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

  • 相关阅读:
    【springboot】9、Rest风格请求及视图解析
    深度解析 MetaArena 游戏引擎,如何让 GameFi 应用更具生命力?
    anaconda里的jupyter打不开怎么回事
    Maven进阶-属性与资源文件
    计算机网络期末复习-Part2
    点云模板匹配
    基于Dockerfile搭建LNMP环境
    基于Django的博客系统之增加类别导航栏(六)
    整体格局:国企、民营、外资各自竞优几何
    [算法应用]关键路径算法的简单应用
  • 原文地址:https://blog.csdn.net/weixin_69725192/article/details/126050584