当前位置: 首页 > 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   4096

int 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   4096

int 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;
}

在proc1.c中每隔一秒写入一个字符

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   4096

int 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;
}

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

编译后在后台先运行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)mmap()/munmap()函数实现共享内存
(2)cat命令参看文件内容
(3)用户空间,内核空间
(4)内存映射的概念与用户/内核空间的映射
(5)页表的概念
(6)ftruncate()函数改变文件大小

结束语

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

相关文章:

  • Java万级并发场景-实战解决
  • AI大模型原理可视化工具:深入浅出理解大语言模型的工作原理
  • 机器学习02——RNN
  • 【2025年五一数学建模竞赛A题】完整思路和代码
  • 代码随想录动态规划part02
  • 【信息系统项目管理师】高分论文:论信息系统项目的范围管理(电网公司保供电可视化系统)
  • 图像处理算法面经1
  • 产品需求设计评审会:三步精准定位需求核心
  • std::enable_shared_from_this 模板类的作用是什么?
  • KEGG注释脚本kofam2kegg.py--脚本010
  • 小程序页面传值的多种方式
  • SQL语言
  • 力扣hot100_技巧_python版本
  • Multisim使用说明详尽版--(2025最新版)
  • 高效爬虫:一文掌握 Crawlee 的详细使用(web高效抓取和浏览器自动化库)
  • CS5346 - Interactivity in Visualization 可视化中的交互
  • Java 架构设计:从单体架构到微服务的转型之路
  • 大语言模型深度思考与交互增强
  • 策略模式随笔~
  • 适合单片机裸机环境的运行的软件定时器框架
  • 兴业银行一季度净赚超237亿降逾2%,营收降逾3%
  • 秦洪看盘|上市公司业绩“排雷”近尾声,A股下行压力趋缓
  • 以“最美通缉犯”为噱头直播?光明网:违法犯罪不应成网红跳板
  • 君亭酒店:2024年营业收入约6.76亿元, “酒店行业传统增长模式面临巨大挑战”
  • 俄罗斯延长非法滞留外国人限期离境时间至9月
  • “自己生病却让别人吃药”——抹黑中国经济解决不了美国自身问题