[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线程同步/互斥过程详解

《Linux线程同步/互斥过程详解》文章讲解多线程并发访问导致竞态条件,需通过互斥锁、原子操作和条件变量实现线程安全与同步,分析死锁条件及避免方法,并介绍RAII封装技术提升资源管理效率... 目录01. 资源共享问题1.1 多线程并发访问1.2 临界区与临界资源1.3 锁的引入02. 多线程案例2.1 为

Oracle数据库定时备份脚本方式(Linux)

《Oracle数据库定时备份脚本方式(Linux)》文章介绍Oracle数据库自动备份方案,包含主机备份传输与备机解压导入流程,强调需提前全量删除原库数据避免报错,并需配置无密传输、定时任务及验证脚本... 目录说明主机脚本备机上自动导库脚本整个自动备份oracle数据库的过程(建议全程用root用户)总结

MySQL 内存使用率常用分析语句

《MySQL内存使用率常用分析语句》用户整理了MySQL内存占用过高的分析方法,涵盖操作系统层确认及数据库层bufferpool、内存模块差值、线程状态、performance_schema性能数据... 目录一、 OS层二、 DB层1. 全局情况2. 内存占js用详情最近连续遇到mysql内存占用过高导致

Linux如何查看文件权限的命令

《Linux如何查看文件权限的命令》Linux中使用ls-R命令递归查看指定目录及子目录下所有文件和文件夹的权限信息,以列表形式展示权限位、所有者、组等详细内容... 目录linux China编程查看文件权限命令输出结果示例这里是查看tomcat文件夹总结Linux 查看文件权限命令ls -l 文件或文件夹

Android Paging 分页加载库使用实践

《AndroidPaging分页加载库使用实践》AndroidPaging库是Jetpack组件的一部分,它提供了一套完整的解决方案来处理大型数据集的分页加载,本文将深入探讨Paging库... 目录前言一、Paging 库概述二、Paging 3 核心组件1. PagingSource2. Pager3.

idea的终端(Terminal)cmd的命令换成linux的命令详解

《idea的终端(Terminal)cmd的命令换成linux的命令详解》本文介绍IDEA配置Git的步骤:安装Git、修改终端设置并重启IDEA,强调顺序,作为个人经验分享,希望提供参考并支持脚本之... 目录一编程、设置前二、前置条件三、android设置四、设置后总结一、php设置前二、前置条件

最新Spring Security的基于内存用户认证方式

《最新SpringSecurity的基于内存用户认证方式》本文讲解SpringSecurity内存认证配置,适用于开发、测试等场景,通过代码创建用户及权限管理,支持密码加密,虽简单但不持久化,生产环... 目录1. 前言2. 因何选择内存认证?3. 基础配置实战❶ 创建Spring Security配置文件

Linux系统中查询JDK安装目录的几种常用方法

《Linux系统中查询JDK安装目录的几种常用方法》:本文主要介绍Linux系统中查询JDK安装目录的几种常用方法,方法分别是通过update-alternatives、Java命令、环境变量及目... 目录方法 1:通过update-alternatives查询(推荐)方法 2:检查所有已安装的 JDK方

Linux系统之lvcreate命令使用解读

《Linux系统之lvcreate命令使用解读》lvcreate是LVM中创建逻辑卷的核心命令,支持线性、条带化、RAID、镜像、快照、瘦池和缓存池等多种类型,实现灵活存储资源管理,需注意空间分配、R... 目录lvcreate命令详解一、命令概述二、语法格式三、核心功能四、选项详解五、使用示例1. 创建逻

Linux下在线安装启动VNC教程

《Linux下在线安装启动VNC教程》本文指导在CentOS7上在线安装VNC,包含安装、配置密码、启动/停止、清理重启步骤及注意事项,强调需安装VNC桌面以避免黑屏,并解决端口冲突和目录权限问题... 目录描述安装VNC安装 VNC 桌面可能遇到的问题总结描js述linux中的VNC就类似于Window