[linux][内存] 实例观察 linux 内存懒加载 和 写时拷贝

2024-03-12 12:28

本文主要是介绍[linux][内存] 实例观察 linux 内存懒加载 和 写时拷贝,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1 内存懒加载

linux 中写应用程序的时候,使用 malloc() 申请的内存,比如使用  malloc() 申请了 1MB 的内存,系统是立即分配了内存吗 ?

不是立即分配,而是懒加载。linux 中用户态的内存是懒加载的,不是申请之后就立即分配,而是在第一次访问的时候才会分配。

懒加载的优点:

(1)避免内存资源浪费,如果应用申请了内存但是一直没有使用,如果内存是立即分配的话就会导致很多内存资源浪费。懒加载类似于单例设计模式中的懒汉式。

(2)减少初始化开销,提升应用启动速度。在进程启动的时候,不需要立即给所有的虚拟内存分配物理内存,这样可以减少初始化开销。

懒加载缺点:

如果应用访问内存的时候,内存有已经加载的,有没加载的,那么两种情况下访问内存所消耗的时间就是不确定的。懒加载影响程序运行的确定性。

2 /proc/self/pagemap

通过 /proc/self/pagemap 可以将虚拟地址转化为物理地址。这个文件只能进程本身才有权限访问。关于 /proc/self/pagemap 的介绍在如下文件中。

Documentation/admin-guide/mm/pagemap.rst

从介绍中可以看出来,文件中的每一项是一个 8 字节的数据。bit63 用来表示虚拟内存有没有分配物理内存,bit 0-54 用来表示物理内存页号。

 * ``/proc/pid/pagemap``.  This file lets a userspace process find out which

   physical frame each virtual page is mapped to.  It contains one 64-bit

   value for each virtual page, containing the following data (from

   ``fs/proc/task_mmu.c``, above pagemap_read):

    * Bits 0-54  page frame number (PFN) if present

    * Bits 0-4   swap type if swapped

    * Bits 5-54  swap offset if swapped

    * Bit  55    pte is soft-dirty (see

      :ref:`Documentation/admin-guide/mm/soft-dirty.rst <soft_dirty>`)

    * Bit  56    page exclusively mapped (since 4.2)

    * Bits 57-60 zero

    * Bit  61    page is file-page or shared-anon (since 3.5)

    * Bit  62    page swapped

    * Bit  63    page present

如下代码,可以获取虚拟地址对应的物理地址。

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdint.h>unsigned long GetPhysicalAddrOfVirtual(unsigned long virtual_addr) {int page_size = getpagesize(); // 页大小,一般是 4KBunsigned long virtual_page_index = virtual_addr / page_size; // 虚拟地址页编号unsigned long page_offset = virtual_addr % page_size; // 虚拟地址页内偏移unsigned long virtual_offset = virtual_page_index * sizeof(uint64_t); // 虚拟地址在 pagemap 中对应的表项uint64_t entry = 0;int fd = open("/proc/self/pagemap", O_RDONLY); // 打开文件if (fd < 0) {perror("open /proc/self/pagemap failed: ");return 0;}if (lseek(fd, virtual_offset, SEEK_SET) < 0) { // 定位到虚拟地址对应的页表项perror("seek error: ");return 0;}if (read(fd, &entry, sizeof(uint64_t)) != sizeof(uint64_t)) {perror("read entry error: ");return 0;}if ((((uint64_t)1 << 63) & entry) == 0){ // 使用 bit 63 来判断物理页是否存在printf("page is not present\n");return 0;}uint64_t phy_page_index = (((uint64_t)1 << 55) - 1) & entry; // 获取物理页编号unsigned long physical_addr = (phy_page_index * page_size) + page_offset; // 获取物理地址return physical_addr;
}int main() {char *p = (char *)malloc(4096);p[0] = 1;p[2000] = 1;printf("virtual addr = %p, physical addr = %p\n", p, (void *)GetPhysicalAddrOfVirtual((unsigned long)(void *)p));return 0;
}

3 内存懒加载代码

 如下是示例代码,代码中的变量有两个,一个是申请内存的方式,包括 malloc(),mmap() 匿名映射,mmap() 基于 fd 映射,这 3 中申请内存的方式;一个是内存访问的方式,一个是读,一个是写。

从实验结果可以得出如下两点:

(1)malloc,mmap 匿名映射,mmap fd 映射,这 3 种方式申请的内存都是懒加载方式,因为在访问之前获取物理是否存在,是不存在的。

(2)内存读和写两种操作都会使得给虚拟内存分配物理页,因为内存访问之后获取物理页是否存在,是存在的。

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdint.h>
#include <sys/mman.h>int PhysicalPageExist(unsigned long virtual_addr)
{int page_size = getpagesize();unsigned long virtual_page_index = virtual_addr / page_size;unsigned long page_offset = virtual_addr % page_size;unsigned long virtual_offset = virtual_page_index * sizeof(uint64_t);uint64_t entry = 0;int fd = open("/proc/self/pagemap", O_RDONLY);if (fd < 0) {perror("open /proc/self/pagemap failed: ");return 0;}if (lseek(fd, virtual_offset, SEEK_SET) < 0) {perror("seek error: ");return 0;}if (read(fd, &entry, sizeof(uint64_t)) != sizeof(uint64_t)) {perror("read entry error: ");return 0;}if ((((uint64_t)1 << 63) & entry) == 0){printf("page is not present\n");return 0;}return 1;
}char *MmapFd() {const char *file_name = "mfile";int fd = open(file_name, O_RDWR | O_CREAT);if (fd == -1) { perror("open");return NULL;}ftruncate(fd, 1024 * 1024);void *p = mmap(NULL, 1024 * 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (p == MAP_FAILED) {perror("mmap");close(fd);return NULL;;}close(fd);return (char *)p;
}char *MmapAnon() {size_t size = 1024 * 1024;void *p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);if (p == MAP_FAILED) {perror("mmap");return NULL;}return (char *)p;
}char *Malloc() {return (char *)malloc(1024 * 1024);
}int main() {// char *p = Malloc();// char *p = MmapAnon();char *p = MmapFd();if (p == NULL) {printf("malloc memory failed");return 0;}for (int i = 0; i < 256; i++) {printf("before write, memory %p loaded %d\n", p + i * 4096, PhysicalPageExist((unsigned long)(void *)(p + i * 4096)));}for (int i = 0; i < 256; i++) {// p[i * 4096] = 100;printf("p[%d * 4096] = %d\n", i, p[i * 4096]);}for (int i = 0; i < 256; i++) {printf("after write, memory %p loaded %d\n", p + i * 4096, PhysicalPageExist((unsigned long)(void *)(p + i * 4096)));}return 0;
}

4 写时拷贝代码

写时拷贝发生在 fork() 的时候,fork() 创建的子进程和父进程共享内存资源,当子进程写的时候,才会给子进程分配新的内存。

如下是写时拷贝的验证代码,从代码运行结果,可以得出如下三点:

(1)fork() 之后,内存写之前,子进程和父进程的内存是共享的。写之前,在父子进程中分别打印出 g_data 的物理地址是相同的,可以证明这点。

(2)父进程写的话,父进程的内存是新分配的,原来的内存给子进程用;子进程写的话,子进程的内存是新分配的,原来的内存给父进程使用。并不是只有子进程写的时候,才会分配内存。

(3)写时拷贝,只有写的时候才会分配新的内存,读的时候不会分配新内存。这点和上节说的内存懒加载的规律是不一样的。

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdint.h>unsigned long GetPhysicalAddrOfVirtual(unsigned long virtual_addr) {int page_size = getpagesize(); // 页大小,一般是 4KBunsigned long virtual_page_index = virtual_addr / page_size; // 虚拟地址页编号unsigned long page_offset = virtual_addr % page_size; // 虚拟地址页内偏移unsigned long virtual_offset = virtual_page_index * sizeof(uint64_t); // 虚拟地址在 pagemap 中对应的表项uint64_t entry = 0;int fd = open("/proc/self/pagemap", O_RDONLY); // 打开文件if (fd < 0) {perror("open /proc/self/pagemap failed: ");return 0;}if (lseek(fd, virtual_offset, SEEK_SET) < 0) { // 定位到虚拟地址对应的页表项perror("seek error: ");return 0;}if (read(fd, &entry, sizeof(uint64_t)) != sizeof(uint64_t)) {perror("read entry error: ");return 0;}if ((((uint64_t)1 << 63) & entry) == 0){ // 使用 bit 63 来判断物理页是否存在printf("page is not present\n");return 0;}uint64_t phy_page_index = (((uint64_t)1 << 55) - 1) & entry; // 获取物理页编号unsigned long physical_addr = (phy_page_index * page_size) + page_offset; // 获取物理地址return physical_addr;
}int g_data = 10;
int main() {printf("pid = %d, g_data = %d, g_data vaddr = %p, g_data paddr = %p\n",getpid(), g_data, &g_data, GetPhysicalAddrOfVirtual(&g_data));pid_t pid = fork();if (pid == 0) {printf("1, child process pid = %d, g_data = %d, g_data vaddr = %p, g_data paddr = %p\n",getpid(), g_data, &g_data, GetPhysicalAddrOfVirtual(&g_data));// 子进程修改,父进程 sleep 2s 之后再读取// sleep(1);// g_data = 20;// 父进程修改,子进程 sleep 2s 之后再读取sleep(2);printf("2, child process pid = %d, g_data = %d, g_data vaddr = %p, g_data paddr = %p\n",getpid(), g_data, &g_data, GetPhysicalAddrOfVirtual(&g_data));} else if (pid > 0) {printf("1, parent process pid = %d, g_data = %d, g_data vaddr = %p, g_data paddr = %p\n",getpid(), g_data, &g_data, GetPhysicalAddrOfVirtual(&g_data));sleep(1);g_data = 20; // 写// printf("read g_data = %d\n", g_data); // 读// sleep(2);printf("2, parent process pid = %d, g_data = %d, g_data vaddr = %p, g_data paddr = %p\n",getpid(), g_data, &g_data, GetPhysicalAddrOfVirtual(&g_data));} else {printf("fork error\n");}return 0;
}

这篇关于[linux][内存] 实例观察 linux 内存懒加载 和 写时拷贝的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/801248

相关文章

Linux join命令的使用及说明

《Linuxjoin命令的使用及说明》`join`命令用于在Linux中按字段将两个文件进行连接,类似于SQL的JOIN,它需要两个文件按用于匹配的字段排序,并且第一个文件的换行符必须是LF,`jo... 目录一. 基本语法二. 数据准备三. 指定文件的连接key四.-a输出指定文件的所有行五.-o指定输出

Linux jq命令的使用解读

《Linuxjq命令的使用解读》jq是一个强大的命令行工具,用于处理JSON数据,它可以用来查看、过滤、修改、格式化JSON数据,通过使用各种选项和过滤器,可以实现复杂的JSON处理任务... 目录一. 简介二. 选项2.1.2.2-c2.3-r2.4-R三. 字段提取3.1 普通字段3.2 数组字段四.

Linux kill正在执行的后台任务 kill进程组使用详解

《Linuxkill正在执行的后台任务kill进程组使用详解》文章介绍了两个脚本的功能和区别,以及执行这些脚本时遇到的进程管理问题,通过查看进程树、使用`kill`命令和`lsof`命令,分析了子... 目录零. 用到的命令一. 待执行的脚本二. 执行含子进程的脚本,并kill2.1 进程查看2.2 遇到的

Linux云服务器手动配置DNS的方法步骤

《Linux云服务器手动配置DNS的方法步骤》在Linux云服务器上手动配置DNS(域名系统)是确保服务器能够正常解析域名的重要步骤,以下是详细的配置方法,包括系统文件的修改和常见问题的解决方案,需要... 目录1. 为什么需要手动配置 DNS?2. 手动配置 DNS 的方法方法 1:修改 /etc/res

MyBatis延迟加载与多级缓存全解析

《MyBatis延迟加载与多级缓存全解析》文章介绍MyBatis的延迟加载与多级缓存机制,延迟加载按需加载关联数据提升性能,一级缓存会话级默认开启,二级缓存工厂级支持跨会话共享,增删改操作会清空对应缓... 目录MyBATis延迟加载策略一对多示例一对多示例MyBatis框架的缓存一级缓存二级缓存MyBat

Linux创建服务使用systemctl管理详解

《Linux创建服务使用systemctl管理详解》文章指导在Linux中创建systemd服务,设置文件权限为所有者读写、其他只读,重新加载配置,启动服务并检查状态,确保服务正常运行,关键步骤包括权... 目录创建服务 /usr/lib/systemd/system/设置服务文件权限:所有者读写js,其他

Linux下利用select实现串口数据读取过程

《Linux下利用select实现串口数据读取过程》文章介绍Linux中使用select、poll或epoll实现串口数据读取,通过I/O多路复用机制在数据到达时触发读取,避免持续轮询,示例代码展示设... 目录示例代码(使用select实现)代码解释总结在 linux 系统里,我们可以借助 select、

Linux挂载linux/Windows共享目录实现方式

《Linux挂载linux/Windows共享目录实现方式》:本文主要介绍Linux挂载linux/Windows共享目录实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录文件共享协议linux环境作为服务端(NFS)在服务器端安装 NFS创建要共享的目录修改 NFS 配

linux系统中java的cacerts的优先级详解

《linux系统中java的cacerts的优先级详解》文章讲解了Java信任库(cacerts)的优先级与管理方式,指出JDK自带的cacerts默认优先级更高,系统级cacerts需手动同步或显式... 目录Java 默认使用哪个?如何检查当前使用的信任库?简要了解Java的信任库总结了解 Java 信

Linux命令rm如何删除名字以“-”开头的文件

《Linux命令rm如何删除名字以“-”开头的文件》Linux中,命令的解析机制非常灵活,它会根据命令的开头字符来判断是否需要执行命令选项,对于文件操作命令(如rm、ls等),系统默认会将命令开头的某... 目录先搞懂:为啥“-”开头的文件删不掉?两种超简单的删除方法(小白也能学会)方法1:用“--”分隔命