【Linux取经路】进程通信——共享内存

【Linux取经路】进程通信——共享内存

码农世界 2024-05-24 前端 72 次浏览 0个评论

文章目录

  • 一、直接原理
    • 1.1 共享内存的的申请
    • 1.2 共享内存的释放
    • 二、代码演示
      • 2.1 shmget
        • 2.1.1 详谈key——ftok
        • 2.2 创建共享内存样例代码
        • 2.3 获取共享内存——进一步封装
        • 2.4 共享内存挂接——shmat
        • 2.5 共享内存去关联——shmdt
        • 2.6 释放共享内存——shmctl
        • 2.7 开始通信
          • 2.7.1 processb 基础代码编写
          • 2.7.2 通信代码编写
          • 三、共享内存的特点
            • 3.1 共享内存 VS 管道
            • 四、拓展内容
              • 4.1 查看共享内存的属性
              • 4.2 借助管道实现共享内存的同步与互斥
              • 五、结语

                一、直接原理

                1.1 共享内存的的申请

                共享的原理和动态库的共享原理一致,共享内存的申请主要分为以下三步:

                • 操作系统在物理内存上申请一块空间。

                • 将申请到的空间,通过页表挂接到进程地址空间的共享区。

                • 返回起始虚拟地址,供程序中使用。

                  1.2 共享内存的释放

                  去关联,释放共享内存。申请、挂接、去关联、释放这些动作都是由操作系统来做的,进程不能自己去做,进程中可以通过 malloc 去申请空间,但是因为进程独立性的存在,一个进程自己 malloc 申请的空间,只属于当前进程,不能由多个进程共享。

                  系统中可能同时有多组进程 都需要通信,因此系统中可能存在多个共享内存,所以操作系统要把这多个共享内存管理起来。先描述,再组织,操作系统中一定有一个内核结构体是用来描述共享内存的。

                  二、代码演示

                  2.1 shmget

                  shmget 函数用来申请一块共享内存。

                  • key:一个数字,是几不重要,关键在于它必须在内核中具有唯一性,能够让不同的共享内存具有唯一性标识。
                  • size:创建共享内存的大小,单位是字节。一般建议是4096的整数倍,如果传的是4097,操作系统实际上申请的空间大小是 4096*2,虽然操作系统多申请了,但是多的部分用户不能使用,用了会被判定为越界。
                  • shmflg:标记位,常用选项有 IPC_CREAT :如果申请的共享内存不存在,就创建,存在,就获取并返回。IPC_EXCL :如果申请的共享内存存在,就出错返回。IPC_CREAT | IPC_EXCL :如果申请的共享内存不存在,就创建,存在就出错返回。这俩选项一起使用保证了,如果我们申请成功了一个共享内存,这个共享内存一定是一个新的。IPC_EXCL 不单独使用。其次,共享内存的权限也通过这个标志位进行传递。
                  • 返回值:创建成功,返回共享内存标识符;创建失败,返回-1。

                    2.1.1 详谈key——ftok

                    无论是创建共享内存还是使用共享内存,都需要调用该函数。第一个进程可以通过 key 创建共享内存,第二个之后的进程,只需要拿着同一个 key 就可以和第一个进程看到同一个共享内存。key 在共享内存的描述对象中,第一次创建共享内存的时候,就必须有一个 key 了。使用 ftok 函数生成一个 key。

                    • pathname:路径名。
                    • proj_id:项目 ID。
                    • 返回值:生成成功 key 被返回;生成失败 -1 被返回(路径名如果不存在的话是有可能生成失败的)。

                      只要这两个参数一样,那么两个进程就可以得到同一个 key 值。

                      **key值为什么是通过用户传参来生成的,而不是操作系统直接生成的?**因为操作系统不知道哪两个进程需要通信,假设 A 进程和 B 进程进行通信,在 A 进程中操作系统随机生成了一个 key 给 A 进程,此时 B 进程也需要知道 key 值,但是操作系统是不知道 A 进程要和 B 进程进行通信,所以操作系统没办法将这个 key 值交给 B 进程,只有程序员(写代码的人)才知道 A、B 进程之间需要通信。其实 ftok 函数相当于是两个通信进程之间的一种约定,只要它们约定好同一个 pathname 和 proj_id,那么这两个进程就能得到同一个 key。

                      2.2 创建共享内存样例代码

                      #ifndef __COMM_HPP__
                      #define __COMM_HPP__
                      #include 
                      #include 
                      #include 
                      #include 
                      #include 
                      #include "log.hpp"
                      #include 
                      #include 
                      using namespace std;
                      const int size = 4096;
                      const string path_name = "/home/wcy";
                      const int proj_id = 0x6666;
                      Log log;
                      key_t GetKey() // 获取 key
                      {
                          key_t k = ftok(path_name.c_str(), proj_id);
                          if(k < 0)
                          {
                              // 获取 key 失败
                              log(Fatal, "ftok error: %s", strerror(errno));
                              exit(1);
                          }
                          log(Info, "ftok sucess, key is: %d", k);
                          return k;
                      }
                      int GetShareMem() // 创建共享内存
                      {
                          key_t key = GetKey();
                          int shmid = shmget(key, size, IPC_CREAT|IPC_EXCL);
                          if(shmid < 0)
                          {
                              log(Fatal, "creat share memeory error: %s", strerror(errno));
                              exit(2);
                          }
                          log(Info, "creat share memory success, shmid is: %d", shmid);
                          return shmid;
                      }
                      #endif
                      

                      key 和 shmid:

                      key 是在操作系统内部来唯一标识一块共享内存,是给操作系统来使用的;shmid 是给进程使用的,用来表示资源的唯一性。虽然共享内存属于文件系统,但是 shmid 和文件描述符的兼容性做的并不好,共享内存给自己单独设置了一个类似于文件描述符表的东西。

                      **共享内存的生命周期是随内核的,用户不主动关闭,共享内存会一致存在。**只有内核重启或者用户主动释放,共享内存才会被释放。

                      查看操作系统中所有的共享内存:ipcs -m

                      • perms:权限位
                      • nattch:和当前共享内存关联的进程个数。

                        命令行中删除共享内存:ipcrm -m shmid。指令是由用户输入的的,在用户层统一使用 shmid。

                        2.3 获取共享内存——进一步封装

                        int GetShareMemHelper(int flag)
                        {
                            key_t key = GetKey();
                            int shmid = shmget(key, size, flag);
                            if(shmid < 0)
                            {
                                log(Fatal, "creat share memeory error: %s", strerror(errno));
                                exit(2);
                            }
                            log(Info, "creat share memory success, shmid is: %d", shmid);
                            return shmid;
                        }
                        // 创建共享内存
                        int CreatMem()
                        {
                            return GetShareMemHelper(IPC_CREAT|IPC_EXCL|0666);
                        }
                        // 获取共享内存
                        int GetMem()
                        {
                            return GetShareMemHelper(IPC_CREAT); // 这里也可以传0
                        } 
                        

                        2.4 共享内存挂接——shmat

                        shmat:将某个共享内存挂接到当前进程的地址空间中。

                        • shmid:共享内存的标识符。
                        • shmaddr:指向挂接在地址空间的什么位置,因为我们也不知道挂接在什么位置,所以一般设为 nullptr 即可。
                        • shmflg:设置当前进程对该共享内存的权限,一般设置成0,表示采用共享内存自身的权限。
                        • **返回值:**共享内存挂接在地址空间中的地址。
                          // processa
                          #include "comm.hpp"
                          #include 
                          int main()
                          {
                              // 创建共享内存
                              int shmid = CreatMem();
                              log(Debug, "creat sharemem done...");
                              sleep(5);
                              // 挂接共享内存
                              char *sharmem = (char*)shmat(shmid, nullptr, 0);
                              log(Debug, "%d attch success", shmid);
                              sleep(5);
                              log(Debug, "processa quit...");
                              return 0;
                          }
                          

                          2.5 共享内存去关联——shmdt

                          shmdt:去掉某个共享内存与当前进程的关联。

                          • shmaddr:就是 shmat 函数返回的那个地址。
                            // processa
                            #include "comm.hpp"
                            #include 
                            int main()
                            {
                                // 创建共享内存
                                int shmid = CreatMem();
                                log(Debug, "creat sharemem done...");
                                sleep(5);
                                // 挂接共享内存
                                char *shamem = (char*)shmat(shmid, nullptr, 0);
                                log(Debug, "%d attch done, to 0x%x", shmid, shamem);
                                sleep(5);
                                // 去关联
                                shmdt(shamem);
                                log(Debug, "%d deattch done", shmid);
                                log(Debug, "processa quit...");
                                return 0;
                            }
                            

                            2.6 释放共享内存——shmctl

                            shmctl:用来释放一个共享内存。

                            • cmd:操作选项。IPC_STAT:将内核中共享内存的属性拷贝到 buf 里面。IPC_RMID:删除共享内存。
                            • struct shmid_ds *buf:语言层面用来描述一个共享内存的结构体,里面保存了共享内存的部分属性。
                            • **返回值:**如果操作是 IPC_RMID,那么删除成功返回0,失败返回-1。
                              // processa
                              #include "comm.hpp"
                              #include 
                              int main()
                              {
                                  // 创建共享内存
                                  int shmid = CreatMem();
                                  log(Debug, "creat sharemem done...");
                                  sleep(5);
                                  // 挂接共享内存
                                  char *shamem = (char*)shmat(shmid, nullptr, 0);
                                  log(Debug, "sharemem %d attch done, to 0x%x", shmid, shamem);
                                  sleep(5);
                                  // 去关联
                                  shmdt(shamem);
                                  log(Debug, "sharemem %d deattch done", shmid);
                                  sleep(5);
                                  // 释放共享内存
                                  int ret = shmctl(shmid, IPC_RMID, NULL);
                                  if(ret < 0)
                                      log(Debug, "sharemem delete error: %s", strerror(errno));
                                  else
                                      log(Debug, "sharemem delete success...");
                                  sleep(5);
                                  log(Debug, "processa quit...");
                                  return 0;
                              }
                              

                              2.7 开始通信

                              2.7.1 processb 基础代码编写

                              #include "comm.hpp"
                              #include 
                              int main()
                              {
                                  // 获取共享内存
                                  int shmid = GetMem();
                                  log(Debug, "Get sharemem done...");
                                  sleep(5);
                                  // 挂接
                                  char *shmaddr = (char*)shmat(shmid, nullptr, 0);
                                  log(Debug, "sharemem %d attch done, to 0x%x", shmid, shmaddr);
                                  sleep(5);
                                  // 去关联
                                  shmdt(shmaddr);
                                  log(Debug, "sharemem %d deattch done", shmid);
                                  return 0;
                              }
                              

                              2.7.2 通信代码编写

                              processa:

                              #include "comm.hpp"
                              #include 
                              int main()
                              {
                                  // 创建共享内存
                                  int shmid = CreatMem();
                                
                                  // 挂接共享内存
                                  char *shamem = (char*)shmat(shmid, nullptr, 0);
                                  // ipc-cod 通信代码
                                  while(true)
                                  {
                                      cout << "client asy@ " << shamem << endl; // 直接访问共享内存
                                      sleep(1);
                                  }
                                  // 去关联
                                  shmdt(shamem);
                                  
                                  // 释放共享内存
                                  int ret = shmctl(shmid, IPC_RMID, NULL);
                                  return 0;
                              }
                              

                              processb:

                              #include "comm.hpp"
                              #include 
                              int main()
                              {
                                  // 获取共享内存
                                  int shmid = GetMem();
                                  // 挂接
                                  char *shmaddr = (char*)shmat(shmid, nullptr, 0);
                                  // ipc-code 通信代码
                                  while(true)
                                  {
                                      cout << "Please enter:";
                                      fgets(shmaddr, size, stdin);
                                  }
                                  // 去关联
                                  shmdt(shmaddr);
                                  return 0;
                              }
                              

                              一旦有了共享内存,并且挂接到当前进程的地址空间上了,在程序中就把它当做该进程自己的内存空间来使用即可,无需再调用系统调用。一旦有人把数据写入到共享内存,其实我们立马就能看到,不需要经过系统调用,就能直接看到数据。可以把共享内存就当做用户自己 malloc 出来的一块空间。

                              三、共享内存的特点

                              • 共享内存没有同步与互斥之类的保护机制,即写端没有向共享内存中写入,读端可以正常读取。

                              • 共享内存是所有的进程间通信中,速度最快的。原因在于数据拷贝次数少。

                              • 共享内存中的数据,完全是由用户自己维护,操作系统不会帮我们做清空工作。

                                3.1 共享内存 VS 管道

                                管道通信中:数据要拷贝两次。主要原因在于,管道本质上是文件,我们不能通过键盘直接往文件里面进行写入,要想让键盘输入的内容写入的文件中,首先需要在程序中定义一个字符数组(或者 string 对象),暂时存储键盘的输入,然后在将这个数组中的内容写入到文件,这就涉及一次拷贝(将数组中的内容,拷贝到文件缓冲区),其次另一端在进行读取的时候,所有的文件读取操作,都要求定义一段空间,将读取到的内容存储起来,这个过程又会涉及一次拷贝,总体算下来,完成一次管道通信,需要进行两次拷贝。

                                共享内存通信:程序中可以把共享内存当做自己的内存空间来使用,因此对于写端,可以直接从键盘读取数据存储到共享内存中,无需创建字符数组(或者 string 对象)来暂时存储输入的内容;对于读端,可以直接从共享内存中进行读取,然后打印,在没有特殊要求的情况可以不用将共享内存中的数据存储起来。

                                四、拓展内容

                                4.1 查看共享内存的属性

                                通过 shmctl 函数区获取共享内存的属性。struct shmid_ds 结构体就是用户层面去描述一个共享内存的结构体。

                                int main()
                                {
                                    // 创建共享内存
                                    int shmid = CreatMem();
                                    struct shmid_ds shmds; // 用来存储共享内存的属性
                                    // 挂接共享内存
                                    char *shamem = (char*)shmat(shmid, nullptr, 0);
                                    // ipc-cod 通信代码
                                    while(true)
                                    {
                                        cout << "client asy@ " << shamem << endl; // 直接访问共享内存
                                        
                                        // 打印共享内存的属性
                                        shmctl(shmid, IPC_STAT, &shmds);
                                        // cout << "__key: " << shmds.shm_perm.__key << endl;
                                        printf("0x%x\n", shmds.shm_perm.__key);
                                        cout << "shm_atime: " << shmds.shm_atime << endl;
                                        cout << "shm_cpid: " << shmds.shm_cpid << endl;
                                        cout << "shm_nattch: " << shmds.shm_nattch << endl;
                                        sleep(1);
                                    }
                                    // 去关联
                                    shmdt(shamem);
                                    // 释放共享内存
                                    int ret = shmctl(shmid, IPC_RMID, NULL);
                                    return 0;
                                }
                                

                                4.2 借助管道实现共享内存的同步与互斥

                                processa:

                                #include "comm.hpp"
                                #include 
                                int main()
                                {
                                    // 创建共享内存
                                    int shmid = CreatMem();
                                    Init init; // 创建有名管道
                                    // 打开管道
                                    int fd = open(FIFO_FILE, O_RDONLY);
                                    if (fd < 0)
                                    {
                                        perror("open");
                                        exit(FIFO_OPEN_ERR);
                                    }
                                    struct shmid_ds shmds;
                                    // 挂接共享内存
                                    char *shamem = (char *)shmat(shmid, nullptr, 0);
                                    // ipc-cod 通信代码
                                    while (true)
                                    {
                                        char ch;
                                        int n = read(fd, &ch, sizeof(ch));
                                        if (n == 0)
                                            break;
                                        else if (n > 0)
                                        {
                                            cout << "client asy@ " << shamem; //<< endl; // 直接访问共享内存
                                        }
                                        else
                                        {
                                            log(Fatal, "read error: %s\n", strerror(errno));
                                            exit(FIFO_READ_ERR);
                                        }
                                    }
                                    // 去关联
                                    shmdt(shamem);
                                    // 释放共享内存
                                    int ret = shmctl(shmid, IPC_RMID, NULL);
                                    // 关闭管道
                                    close(fd);
                                    return 0;
                                }
                                

                                processb:

                                #include "comm.hpp"
                                #include 
                                int main()
                                {
                                    // 获取共享内存
                                    int shmid = GetMem();
                                    // 打开管道
                                    int fd = open(FIFO_FILE, O_WRONLY);
                                    if(fd < 0)
                                    {
                                        perror("open");
                                        exit(FIFO_OPEN_ERR);
                                    }
                                    // 挂接
                                    char *shmaddr = (char*)shmat(shmid, nullptr, 0);
                                    // ipc-code 通信代码
                                    while(true)
                                    {
                                        cout << "Please enter:";
                                        fgets(shmaddr, size, stdin);
                                        write(fd, "c", 1);
                                    }
                                    // 去关联
                                    shmdt(shmaddr);
                                    // 关闭管道
                                    close(fd);
                                    return 0;
                                }
                                

                                在没有管道的情况下,processa 进程一直在不间断的读取共享内存中的数据,现在创建一个管道,在 processa 进程读取共享内存之前,想让它从管道中读取,只有读到了特定的信号,才能去共享内存中进行读取。processb 进程在向共享内存中写入数据之后,向管道中写入一个字符,以此为信号,通知 processa 进程,现在共享内存中有数据了,你可以去读取了。在 processb 进程没有向共享内存中写入数据的时候,此时管道为空,读端就会阻塞,也就是 processa 进程就会被阻塞住,以此来实现同步与互斥。

                                五、结语

                                今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!

转载请注明来自码农世界,本文标题:《【Linux取经路】进程通信——共享内存》

百度分享代码,如果开启HTTPS请参考李洋个人博客
每一天,每一秒,你所做的决定都会改变你的人生!

发表评论

快捷回复:

评论列表 (暂无评论,72人围观)参与讨论

还没有评论,来说两句吧...

Top