当前位置: 首页 > news >正文

linux多线(进)程编程——番外1:内存映射与mmap

前言

在修真世界之外,无数异世界,其中某个叫地球的异世界中,一群人对共享内存的第二种使用方式做出了讲解。

内核空间与用户空间

内存空间的划分

Linux操作系统下一个进程的虚拟地址空间被分为用户空间内核空间
Linux 内核空间在内存管理中是统一管理的。它通常位于虚拟内存空间的高地址部分。对于 32 位系统,用户空间和内核空间的划分通常是 3GB(用户空间)和 1GB(内核空间)。对于 64 位系统,虚拟内存空间更大,内核空间也相应地更大。
内核空间用于存放内核代码、内核数据结构(如进程控制块、文件系统数据结构等)和内核栈等。用户空间用于存放用户编写的程序代码以及代码中的变量等。
一个进程的内存空间分配

内核空间与用户空间的映射

内核空间的映射是通过页表来实现的。页表是内存管理单元(MMU)用来把虚拟地址转换为物理地址的映射表。
通过页表完成映射
内核空间中存储着和内核有关的代码,这是我们要思考一个问题,当不同进程切换时,如何处理内核空间才是最方便的?
答案就是共用内核空间,在 Linux 内核中,内核空间的页表是全局的,也就是说,它对所有进程都是一样的。不同进程的内核空间都会映射到一片物理内存上,避免每次进程切换时进行重复的地址映射浪费资源。实际的进程切换时,只有用户空间的页表发生变化,把用户空间的虚拟内存映射到物理地址上。如下图所示,图中省略了页表,但是大家要知道是页表在中间完成的映射
在这里插入图片描述
因为内核空间的页表是全局的,所以不同进程的内核空间映射的物理内存是相同的。例如,当进程 A 和进程 B 都访问内核空间中的某个特定的虚拟地址时,这个虚拟地址通过内核空间的页表映射到相同的物理内存地址。不过,虽然内核空间映射的物理内存是相同的,但内核会通过一些机制来保护内核空间不被进程随意访问。例如,CPU 的保护机制会限制用户态进程对内核空间的访问权限,只有在内核态(如系统调用或中断处理时)才能访问内核空间,这样可以防止进程对内核空间的非法操作。

mmap映射实现共享内存

不知道大家看了上面的讲解是不是会在脑海中产生一个想法?我不妨用读心术将你们的想法读出来:
如果我在程序中可以直接操作属于内核空间的地址,是不是就可以直接实现共享内存了呢?
实际上,linux系统为大家提供了这种方法:mmap。

通过mmap实现共享内存

在 Linux 中,mmap 是一种强大的内存映射技术,用于将文件或其他对象映射到进程的地址空间中,从而实现高效的文件操作和内存共享。
函数原型:

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);	// 映射
int munmap(void *addr, size_t length);		// 解除映射

参数说明:
start:映射的起始地址,通常设置为 NULL,让系统自动选择。
length:映射的字节数。
prot:映射区域的保护标志,如 PROT_READ(可读)、PROT_WRITE(可写)等。
flags:映射的选项,如 MAP_SHARED(共享映射)或 MAP_PRIVATE(私有映射)。
fd:要映射的文件描述符。
offset:文件中的偏移量,必须是页大小的整数倍。
addr:映射的虚拟地址,为mmap的返回值。

当使用mmap函数后,系统通过文件描述符fd找到对应的文件在硬盘上的位置,将这块区域映射到用户空间里。当flags为MAP_SHARED时即可实现共享内存的映射。
当我们访问这块空间是,操作系统会发起一个缺页异常(不是字面意思上的异常,类似一个中断)。内核通过映射关系将文件内容从磁盘拷贝到物理内存中,供进程访问。

mmap与管道的区别

管道与mmap都是使用文件完成的进程间通信,两者有一定的相似性,那么他们的区别是什么呢?
mmap更加高效,除了向共享内存中写入数据时需要刷新文件,读取时只要通过指针操作即可,这个操作发生在用户态。管道的写入与读取需要通过write()与read()函数。有相关知识的同学应该知道,这两个函数身份不一般,它们会发起系统调用让程序由用户态转化为内核态。
总之一句话,mmap更加高效。

代码案例

使用mmap技术写入文件

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>#define MMAP_SIZE   4096int main() {int fd = open("demo_file", O_RDWR | O_CREAT,  0777);ftruncate(fd, MMAP_SIZE);		// 拓展文件的大小char* addr = (char*)mmap(NULL, 4096, PROT_WRITE, MAP_SHARED, fd, 0);strcpy(addr, "hello, world!\n");munmap(addr, MMAP_SIZE);close(fd);return 0;
}

这里用到了一个新的文件函数:ftruncate(),用于改变文件的大小容量,文件大小要大于mmap映射的大小。
可以看到这里我们新建了一个文件叫demo_file。
运行后我们的当前源文件所在目录下会新建一个文件,我们使用cat指令查看他的内容,cat命令是用来查看文件中的文本的一个shell指令,大家可以学习一下。

cat demo_file	# cat [文件相对路径]

输出结果为:

lol@hyl:~/work/linux_study/Shared_memory/mmap_fun$ cat demo_file
hello, world!

可以看到我们成功的写入了数据,相比与使用write函数,这种方法更加简单方便,可以直接通过指针操作文件中的内存,此外与管道类似,由于文件是属于操作系统的,独立于进程之外的,因此在其他进程中也可以操作这个文件实现通信。。

接下来我们要实现两个进程的通信了,还是写两个程序。
proc1.c:每隔一秒写入一个字符

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>#define MMAP_SIZE   4096int main() {int fd = open("demo_file", O_RDWR | O_CREAT,  0777);ftruncate(fd, MMAP_SIZE);char* addr = (char*)mmap(NULL, 4096, PROT_WRITE, MAP_SHARED, fd, 0);for(int i = 0; i < MMAP_SIZE; i = i+1) {addr[i] = i%26 + 'a';sleep(1);}munmap(addr, MMAP_SIZE);close(fd);return 0;
}

proc2.c:每隔一秒读取一个字符

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>#define MMAP_SIZE   4096int main() {int fd = open("demo_file", O_RDWR | O_CREAT,  0777);ftruncate(fd, MMAP_SIZE);char* addr = (char*)mmap(NULL, 4096, PROT_WRITE, MAP_SHARED, fd, 0);for(int i = 0; i < MMAP_SIZE; i++) {printf("%c\n", addr[i]);sleep(1);}munmap(addr, MMAP_SIZE);close(fd);return 0;
}

编译后在后台先运行p1并让出终端(之前讲过,在命令后加一个&),并直接运行p2。

lockin@qingfenfuqin:~/work/linux_study/Shared_memory/mmap_fun$ ./p1&
[1] 10883		# 这里10883是p1进程的pid,这个进程在后台运行
lockin@qingfenfuqin:~/work/linux_study/Shared_memory/mmap_fun$ ./p2

输出为:

a
b
c
d
e
f
...

测试成功了!!!

小结

本节知识点:
(1)用户空间,内核空间
(2)内存映射的概念与用户/内核空间的映射
(3)页表的概念
(4)mmap()/munmap()函数实现共享内存
(5)ftruncate()函数改变文件大小
(6)cat命令参看文件内容

结束语

番外篇结束,该回到修真界了。


http://www.mrgr.cn/news/98392.html

相关文章:

  • 欧拉服务器操作系统部署deekseep(Ollama+DeekSeep+open WebUI)
  • 数据库索引深度解析:原理、类型与高效使用实践
  • ARCGIS PRO DSK 利用两期地表DEM数据计算工程土方量
  • 在轨道交通控制系统中如何实现μs级任务同步
  • 2025年第十六届蓝桥杯省赛真题解析 Java B组(简单经验分享)
  • cline 提示词工程指南-架构篇
  • [Python基础速成]2-模块与包与OOP
  • Windows系统docker desktop安装(学习记录)
  • java锁机制(CAS和synchronize)的实现原理和使用方法
  • Domain Adaptation领域自适应
  • 科目四 学习笔记
  • 智能云图库-1-项目初始化
  • 祁连山国家公园shp格式数据
  • Python 机器学习实战 第6章 机器学习的通用工作流程实例
  • 大数据面试问答-Spark
  • 嵌入式程序设计英语
  • Spring Security6 从源码慢速开始
  • HarmonyOS:使用Refresh组件实现页面下拉刷新上拉加载更多
  • PVE 8.4.1 安装 KDE Plasma 桌面环境 和 PVE换源
  • linux中查看.ypc二进制文件