聊聊跨进程共享内存的内部工作原理

在 Linux 系统的进程虚拟内存中,一个重要的特性就是不同进程的地址空间是隔离的。A 进程的地址 0x4000 和 B 进程的 0x4000 之间没有任何关系。这样确确实实是让各个进程的运行时互相之间的影响降到了最低。某个进程有 bug 也只能自己崩溃,不会影响其它进程的运行。

但是有时候我们想要跨进程传递一些数据。因为进程虚拟内存地址是隔离的。所以目前业界最常用的做法是让进程之间通过 127.0.0.1 或者是 Unix Domain Socket 等本机网络手段进行数据的传输。这个方案在传输的数据量较小的时候工作是很不错的。

但如果进程间想共享的数据特别大,比如说几个 GB,那如果使用网络 IO 方案的话,就会涉及到大量的内存拷贝的开销,导致比较低的程序性能。这是可以采用进程间共享内存的方法来在通信时避免内存拷贝。

那么问题来了,不同进程之间的虚拟地址是隔离的,共享内存又是如何突破这个限制的呢?我们今天就来深入地了解下共享内存的内部工作原理。

一、共享内存的使用方式

共享内存发送方进程的开发基本过程是调用 memfd_create 创建一个内存文件。然后通过 mmap 系统调用为这个内存文件申请一块共享内存。然后这个内存文件就可以写入数据了。最后把这个文件的句柄通过 Unix Domain Socket 的方式给接收方进程发送过去。

下面是发送方的核心代码。

int main(int argc, char **argv) {
 // 创建内存文件
 fd = memfd_create("Server memfd", ...);

 // 为内存文件申请 MAP_SHARED 类型的内存
 shm = mmap(NULL, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

 // 向共享内存中写入数据
 sprintf(shm, "这段内容是保存在共享内存里的,接收方和发送方都能根据自己的fd访问到这块内容");

 // 把共享内存文件的句柄给接收方进程发送过去
 struct msghdr msgh;
 *((int *) CMSG_DATA(CMSG_FIRSTHDR(&msgh))) = fd;
 sendmsg(conn, &msgh, 0);
 ......
}

共享内存接收方的工作过程是先用 Unix Domain Socket 连接上服务器,然后使用 recvmsg 就可以收到发送方发送过来的文件句柄。

int main(int argc, char **argv) {
 // 通过 Unix Domain Socket 连接发送方
 connect(conn, (struct sockaddr *)&address, sizeof(struct sockaddr_un));

 // 通过连接取出发送方发送过来的内存文件句柄
 int size = recvmsg(conn, &msgh, 0);
 fd = *((int *) CMSG_DATA(cmsgh));

 // 读取共享文件中的内容
 shm = mmap(NULL, shm_size, PROT_READ, MAP_PRIVATE, fd, 0);
 printf("共享内存中的文件内容是: %s\n", shm);
 ......
}

这样这两个进程都各自有一个文件句柄,在底层上是指向同一个内存文件的。这样就实现了发送方和接收方之间的内存文件共享了。

但我们上面介绍的是开发基本过程。按照我们开发内功修炼公众号的风格,这还不算完,我们是要把它最底层的原理真正的弄通透才算的。所以接下来我们再深入地分析 memfd_create、 mmap、以及 Unix Domain socket sendmsg 和 recvmsg 的底层工作原理,来看看它们是如何配合来实现跨进程共享内存的。

二、共享内存文件原理

在发送方发送文件之前,需要先通过 memfd_create 来创建一个内存文件,然后再使用 mmap 为其分配内存。

2.1 创建内存文件

其中 memfd_create 函数是一个系统调用。内核中它的主要逻辑有两个,一是调用 get_unused_fd_flags 申请一个没使用过的文件句柄,二是调用 shmem_file_setup 创建一个共享内存文件。

我们来看 memfd_create 的源码。

// file:mm/memfd.c
SYSCALL_DEFINE2(memfd_create,
  const char __user *, uname,
  unsigned int, flags)
{
 ...
 // 申请一个未使用过的文件句柄
 fd = get_unused_fd_flags((flags & MFD_CLOEXEC) ? O_CLOEXEC : 0);

 // 创建一个共享内存的文件
 file = shmem_file_setup(name, 0, VM_NORESERVE);

 fd_install(fd, file);
 return fd;
}

其中在 shmem_file_setup 函数中又调用了 __shmem_file_setup。

// file:mm/shmem.c
static struct file *__shmem_file_setup(struct vfsmount *mnt, const char *name, ...)
{
 ...
 // 申请一个 inode
 inode = shmem_get_inode(mnt->mnt_sb, NULL, S_IFREG | S_IRWXUGO, 0,
    flags);
 inode->i_flags |= i_flags;
 inode->i_size = size;

 ...
 // 创建一个文件
 res = alloc_file_pseudo(inode, mnt, name, O_RDWR,
    &shmem_file_operations);
 return res;
}

我们都知道磁盘文件在内核的实现中是由 inode 和 struct file 对象一起组成的。其实共享内存文件也一样,__shmem_file_setup 中就是先申请了一个 inode,然后再调用 alloc_file_pseudo 创建一个文件。值得注意的是,这个文件并非是磁盘上的文件,而只是在内存里的。

2.2 mmap申请内存

mmap 也是一个系统调用,注意我们在开篇处调用它的时候传入的第三个 flag 参数是 MAP_SHARED。这表示的是要通过 mmap 申请一块跨进程可共享的内存出来。mmap 的实现入口在 arch/x86/kernel/sys_x86_64.c

//file:arch/x86/kernel/sys_x86_64.c
SYSCALL_DEFINE6(mmap, unsigned long, addr, ...)
{
 return ksys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
}

接下来的这个函数的调用链路如下

SYSCALL_DEFINE6(mmap
-> ksys_mmap_pgoff
---> vm_mmap_pgoff
------> do_mmap_pgoff
--------> do_mmap

在 do_mmap 函数中,对输入的 MAP_SHARED 进行了处理。

//file:mm/mmap.c
unsigned long do_mmap(struct file *file, unsigned long addr,
   unsigned long len, unsigned long prot,
   unsigned long flags, vm_flags_t vm_flags,
   unsigned long pgoff, unsigned long *populate,
   struct list_head *uf)
{
 struct mm_struct * mm = current->mm;
 ...

 // 如果包含 MAP_SHARED,则对要申请的虚拟内存设置一个 VM_SHARED
 switch (flags & MAP_TYPE) {
  case MAP_SHARED:
  case MAP_SHARED_VALIDATE:
   vm_flags |= VM_SHARED | VM_MAYSHARE; 
   ... 
 } 
 ... 

 addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
 ......
}

如果 flag 包含了 MAP_SHARED,则对要申请的虚拟内存设置一个 VM_SHARED。该标记指明的是要申请一个可以跨进程共享的内存块。接下来进入 mmap_region 中申请虚拟内存。

//file:mm/mmap.c
unsigned long mmap_region(struct file *file, ...)
{
 struct mm_struct *mm = current->mm;
 ......

 // 申请虚拟内存vma
 vma = vm_area_alloc(mm);

 // vma初始化
 vma->vm_start = addr;
 vma->vm_end = addr + len;
 vma->vm_flags = vm_flags;
 vma->vm_page_prot = vm_get_page_prot(vm_flags);
 vma->vm_pgoff = pgoff;
 ......

 // 加入到进程的虚拟内存 vma 链表中来
 vma_link(mm, vma, prev, rb_link, rb_parent);
}

进程的虚拟内存地址空间在内核底层中就是由这样一个个的 vma 来组成的。每一个 vma 都声明的是进程虚拟地址中的某一段地址范围已经分配出去了。在 mmap_region 函数中申请了 vma,并在内核中将其管理了起来。

这里注意我们在申请共享内存的时候,给 vma 是带了 VM_SHARED 标记的。带了这个标记的 vma和普通的虚拟内存不一样。后面在发生缺页中断申请物理内存的时候,在不同的进程间是可以对应到同一块物理内存的。所以可以实现进程间的共享。

所以真正让进程之间可以共享内存的是这个带 VM_SHARED 的 vma。

 资料直通车:Linux内核源码技术学习路线+视频教程内核源码

学习直通车:Linuxc/c++高级开发【直播公开课】

零声白金VIP体验卡:零声白金VIP体验卡(含基础架构/高性能存储/golang/QT/音视频/Linux内核)

三、发送方发送文件句柄

发送方在使用 memfd_create 创建出来内存文件,并用 mmap 为其申请可跨进程共享的内存后。接着就可以通过 Unix Domain Socket 中对应的 sendmsg 方法将这个共享内存文件的句柄发送出来。如下是发送的代码示例。

static void send_fd(int conn, int fd) {
    struct msghdr msgh;
    struct iovec iov;
    ...

    // 把文件句柄放到消息中来
    *((int *) CMSG_DATA(CMSG_FIRSTHDR(&msgh))) = fd;

    // 发送出去
    sendmsg(conn, &msgh, 0);
}

sendmsg 又是一个内核提供的系统调用,它位于 net/socket.c 文件中。

//file:net/socket.c
SYSCALL_DEFINE3(sendmsg, int, fd, struct user_msghdr __user *, msg, unsigned int, flags)
{
 return __sys_sendmsg(fd, msg, flags, true);
}

该函数的调用路径如下

SYSCALL_DEFINE3(sendmsg, ...)
-> __sys_sendmsg
---> ___sys_sendmsg
-----> ____sys_sendmsg
-------> sock_sendmsg
---------> sock_sendmsg_nosec
-----------> unix_stream_sendmsg

在 unix_stream_sendmsg 中执行了真正的发送。

//file:net/unix/af_unix.c 
static int unix_stream_sendmsg(struct socket *sock, struct msghdr *msg, ...)
{
 // 把文件描述符指向的文件信息复制到 scm_cookie 中
 struct scm_cookie scm;
 scm_send(sock, msg, &scm, false);

 // 不断构建数据包发送,直到发送完毕
    while (sent < len) {
     // 申请一块缓存区
     skb = sock_alloc_send_pskb(sk, size - data_len, data_len,
        msg->msg_flags & MSG_DONTWAIT, &err,
        get_order(UNIX_SKB_FRAGS_SZ));

     // 拷贝数据到 skb
     err = unix_scm_to_skb(&scm, skb, !fds_sent);
     err = skb_copy_datagram_from_iter(skb, 0, &msg->msg_iter, size);
     
     // 直接把 skb 放到对端的接收队列中
     skb_queue_tail(&other->sk_receive_queue, skb);
  
  //发送完毕回调
  other->sk_data_ready(other);
  sent += size;
     ...
    }
}

在 unix_stream_sendmsg 中申请了个 skb 缓存区,然后把要发送的文件句柄等数据都塞到里面,最后调用 skb_queue_tail 直接把 skb 放到 Unix Domain Socket 连接另一端的接收队列中了。

这里注意文件句柄只有在当前进程内才是有意义的。如果直接发送 fd 出去,接收方是没有办法使用的。所以在 scm_send 函数中,重要的逻辑是把 fd 对应的 struct file 的指针给找了出来,放到待发送的数据里面了。只有 file 这种内核级的对象接收方才能使用。

scm_send
-> __scm_send
---> scm_fp_copy

在 scm_fp_copy 中根据 fd 把 file 给找了出来。它的指针会被放到发送数据中

//file:net/core/scm.c
static int scm_fp_copy(struct cmsghdr *cmsg, struct scm_fp_list **fplp)
{
 ...
 //把每一个要发送的 fd 对应的 file 给找出来
 for (i=0; i< num; i++)
 {
  int fd = fdp[i];
  struct file *file;

  if (fd < 0 || !(file = fget_raw(fd)))
   return -EBADF;
  *fpp++ = file;
  fpl->count++;
 }
}

四、接收方接收文件

接下来接收方就可以通过 recvmsg 来接收发送方发送过来的文件了。recvmsg 系统会调用到 unix_stream_read_generic 中,然后在这个函数中把 skb 给取出来。

下面是接收函数核心 unix_stream_read_generic 的源码。

//file:net/unix/af_unix.c
static int unix_stream_read_generic(struct unix_stream_read_state *state,
        bool freezable)
{
 do {
  // 拿出一个 skb
  last = skb = skb_peek(&sk->sk_receive_queue);
  ...
 }
 ...
 if (state->msg)
  scm_recv(sock, state->msg, &scm, flags);
 return copied ? : err;
}

在 skb 拿出来后,还需要调用 scm_recv 来把 skb 中包含的文件给找出来。在 scm_recv 中调用 scm_detach_fds。

//file:net/core/scm.c
void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm)
{

 for (i = 0; i < fdmax; i++) {
  err = receive_fd_user(scm->fp->fp[i], cmsg_data + i, o_flags);
  if (err < 0)
   break;
 }
 ...
}

在 scm->fp->fp[i] 中包含的是发送方发送过来的 struct file 指针。这样文件就取出来了。当然 struct file 是个内核态的对象,用户没有办法使用。所以还需要再为其在新的进程中申请一个文件句柄,然后返回。

//file:fs/file.c
int __receive_fd(struct file *file, int __user *ufd, unsigned int o_flags)
{
 //申请一个新的文件描述符
 new_fd = get_unused_fd_flags(o_flags);
 ...

 //关联文件
 fd_install(new_fd, get_file(file));
 return new_fd;
}

五、总结

共享内存发送方进程的开发过程基本分 memfd_create 创建内存文件、mmap 申请共享内存、Unix Domain Socket 发送文件句柄三步。

  • 第一步,memfd_create 系统调用的主要逻辑有两个,一是调用 get_unused_fd_flags 申请一个没使用过的文件句柄,二是调用 shmem_file_setup 创建一个共享内存文件。
  • 第二步,mmap 系统调用在调用它的时候传入的第三个 flag 参数是 MAP_SHARED,该参数是申请一块跨进程可共享访问的物理内存。
  • 第三步,接着通过 Unix Domain Socket 中对应的 sendmsg 方法将这个共享内存文件的句柄发送出去。在发送时,把文件句柄对应的 struct file 指针找到并放到要封装的 skb 数据包中了。

接收方进程的主要实现原理是 recvmsg 系统调用。在这个系统调用中,内核会把发送方发送过来的 struct file 指针取出来,然后再在当前进程下为其申请一个新的文件句柄。这个文件句柄返回给用户进程后,用户进程就可以用它来和另外一个进程共享地访问同一块内存了。

总体来看,共享内存本质上共享的是内核对象 struct file,通过在不同的进程之间使用同一个 struct file 来实现的共享。当然也得需要在虚拟内存对象 vma 带上 VM_SHARED 标记来支持。

原文作者:开发内功修炼

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/236480.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vector类

> 作者简介&#xff1a;დ旧言~&#xff0c;目前大二&#xff0c;现在学习Java&#xff0c;c&#xff0c;c&#xff0c;Python等 > 座右铭&#xff1a;松树千年终是朽&#xff0c;槿花一日自为荣。 > 目标&#xff1a;熟悉vector库 > 毒鸡汤&#xff1a;从人生低谷…

【交流】PHP生成唯一邀请码

目录 前言&#xff1a; 1.随机生成&#xff0c;核对user表是否已存在 代码&#xff1a; 解析&#xff1a; 缺点&#xff1a; 2.建表建库&#xff0c;每次从表中随机抽取一条&#xff0c;用完时扩充 表结构 表视图 代码 解析 缺点 结论&#xff1a; 前言&#xff1a; …

Amazon 亚马逊内推

点击关注公众号&#xff0c;分享 WLB、大厂内推&#xff0c;面经、热点新闻&#xff0c;可内推公司90&#xff0c;累计帮助8000 靠谱的内推君 专注于WLB、大厂精选内推&#xff0c;助力每位粉丝拿到满意的Offer&#xff01; 公司简述 亚马逊公司&#xff08;Amazon&#xff…

基于单片机远程温控检测系统

**单片机设计介绍&#xff0c;基于单片机远程温控检测系统&#xff08;含上位机&#xff09; 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的远程温控检测系统可以用于远程监测和控制温度&#xff0c;实现远程温度监…

UDP报文格式详解

✏️✏️✏️各位看官好&#xff0c;今天给大家分享的是 传输层的另外一个重点协议——UDP。 清风的CSDN博客 &#x1f6e9;️&#x1f6e9;️&#x1f6e9;️希望我的文章能对你有所帮助&#xff0c;有不足的地方还请各位看官多多指教&#xff0c;大家一起学习交流&#xff0…

江苏中服产业党总支一行莅临蓝海创意云参观交流

12月5日上午&#xff0c;江苏中服产业党总支及直播产业“两新”党支部一行莅临蓝海创意云参观交流&#xff0c;蓝海创意云相关领导热情接待&#xff0c;双方就直播业务进行了深度沟通&#xff0c;未来将携手合作共同推动直播产业的创新发展。 在蓝海创意云一楼展厅&#xff0c;…

2024年网络安全竞赛-网站渗透

网站渗透 (一)拓扑图 1.使用渗透机对服务器信息收集,并将服务器中网站服务端口号作为flag提交; 使用nmap工具对靶机进行信息收集 2.使用渗透机对服务器信息收集,将网站的名称作为flag提交; 访问页面即可 3.使用渗透机对服务器渗透,将可渗透页面的名称作为flag提交…

【计算机网络】HTTP响应报文Cookie原理

目录 HTTP响应报文格式 一. 状态行 状态码与状态码描述 二. 响应头 Cookie原理 一. 前因 二. Cookie的状态管理 结束语 HTTP响应报文格式 HTTP响应报文分为四部分 状态行&#xff1a;包含三部分&#xff1a;协议版本&#xff0c;状态码&#xff0c;状态码描述响应头&a…

3-Mybatis

文章目录 项目源码地址Mybatis概述什么是Mybatis&#xff1f;Mybatis导入知识补充数据持久化持久层 第一个Mybatis程序&#xff1a;数据的增删改查查项目名称创建环境编写代码1、目录结构2、核心配置文件&#xff1a;resources/mybatis-config.xml3、mybatis工具类&#xff1a;…

AtCoder ABC周赛2023 11/4 (Sat) E题题解

目录 原题截图&#xff1a; 原题翻译 题目大意&#xff1a; 主要思路&#xff1a; 代码&#xff1a; 原题截图&#xff1a; 原题翻译 题目大意&#xff1a; 给你一个数组&#xff0c;给你一个公式&#xff0c;让你选k个元素&#xff0c;用公式算出最终得分。 主要思路&am…

【论文精读】GAIA: A Benchmark for General AI Assistants

GAIA: A Benchmark for General AI Assistants 前言Abstract1 Introduction2 Related work3 GAIA3.1 A convenient yet challenging benchmark for general AI assistants3.2 Evaluation3.3 Composition of GAIA3.4 Building and extending GAIA 4 LLMs results on GAIA5 Discu…

堆排序算法及实现

1、堆排序定义 堆是一棵顺序存储的完全二叉树。 其中每个结点的关键字都不大于其孩子结点的关键字&#xff0c;这样的堆称为小根堆。其中每个结点的关键字都不小于其孩子结点的关键字&#xff0c;这样的堆称为大根堆。 举例来说&#xff0c;对于n个元素的序列{R0, R1, ... ,…

重温经典struts1之常用标签

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 前言 上一篇&#xff0c;我们学习了struts的基本概念&#xff0c;怎样搭建struts开发环境&#xff0c;从编写formbean&#xff0c;action到jsp页面&#xff0c;以及怎样将他…

用 Python 脚本实现电脑唤醒后自动拍照 截屏并发邮件通知

背景 背景是这样的, 我的家里台式机常年 休眠, 并配置了 Wake On Lan (WOL) 方便远程唤醒并使用。 但是我发现, 偶尔台式机会被其他情况唤醒, 这时候我并不知道, 结果白白运行了好几天, 浪费了很多电。 所以我的需求是这样的&#xff1a; &#x1f914; 电脑唤醒后(可能是开…

spider小案例~https://industry.cfi.cn/BCA0A4127A4128A4141.html

一、获取列表页信息 通过抓包发现列表页信息非正常返回&#xff0c;列表信息如下图&#xff1a; 通过观察发现列表页信息是通过unes函数进行处理的&#xff0c;我们接下来去看下该函数 该函数是对列表页的信息先全局替换"~"为"%u"&#xff0c;然后再通过…

人工智能(pytorch)搭建模型22-基于pytorch搭建SimpleBaseline(人体关键点检测)模型,并详细介绍该网络模型与代码实现

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下人工智能(pytorch)搭建模型22-基于pytorch搭建SimpleBaseline(人体关键点检测)模型&#xff0c;并详细介绍该网络模型与代码实现。本文将介绍关于SimpleBaseline模型的原理&#xff0c;以及利用pytorch框架搭建模型…

阿里面试:如何保证RocketMQ消息有序?如何解决RocketMQ消息积压?

尼恩说在前面 在40岁老架构师 尼恩的读者交流群(50)中&#xff0c;最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格&#xff0c;遇到很多很重要的面试题&#xff1a; 如何保证RocketMQ消息有序&#xff1f;如何解决RocketMQ消息…

Linux高级系统编程- 消息队列 与 内存共享

消息队列 消息队列是消息的链表&#xff0c;存放在内存中&#xff0c;由内核维护 特点&#xff1a; 1、消息队列中的消息是有类型的。 2、消息队列中的消息是有格式的。 3、消息队列可以实现消息的随机查询。消息不一定要以先进先出的次序读取&#xff0c;编程时 可以按消息的…

Python中的并发编程(3)线程池、锁

concurrent.futures 提供的线程池 concurrent.futures模块提供了线程池和进程池简化了多线程/进程操作。 线程池原理是用一个任务队列让多个线程从中获取任务执行&#xff0c;然后返回结果。 常见的用法是创建线程池&#xff0c;提交任务&#xff0c;等待完成并获取结果&…

Nginx正则表达式

目录 1.nginx常用的正则表达式 2.location location 大致可以分为三类 location 常用的匹配规则 location 优先级 location 示例说明 优先级总结 3.rewrite rewrite功能 rewrite跳转实现 rewrite执行顺序 语法格式 rewrite示例 实例1&#xff1a; 实例2&#xf…