[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线程之线程的创建、属性、回收、退出、取消方式》文章总结了线程管理核心知识:线程号唯一、创建方式、属性设置(如分离状态与栈大小)、回收机制(join/detach)、退出方法(返回/pthr... 目录1. 线程号2. 线程的创建3. 线程属性4. 线程的回收5. 线程的退出6. 线程的取消7.

Linux下进程的CPU配置与线程绑定过程

《Linux下进程的CPU配置与线程绑定过程》本文介绍Linux系统中基于进程和线程的CPU配置方法,通过taskset命令和pthread库调整亲和力,将进程/线程绑定到特定CPU核心以优化资源分配... 目录1 基于进程的CPU配置1.1 对CPU亲和力的配置1.2 绑定进程到指定CPU核上运行2 基于

golang程序打包成脚本部署到Linux系统方式

《golang程序打包成脚本部署到Linux系统方式》Golang程序通过本地编译(设置GOOS为linux生成无后缀二进制文件),上传至Linux服务器后赋权执行,使用nohup命令实现后台运行,完... 目录本地编译golang程序上传Golang二进制文件到linux服务器总结本地编译Golang程序

Linux下删除乱码文件和目录的实现方式

《Linux下删除乱码文件和目录的实现方式》:本文主要介绍Linux下删除乱码文件和目录的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux下删除乱码文件和目录方法1方法2总结Linux下删除乱码文件和目录方法1使用ls -i命令找到文件或目录

MySQL中的LENGTH()函数用法详解与实例分析

《MySQL中的LENGTH()函数用法详解与实例分析》MySQLLENGTH()函数用于计算字符串的字节长度,区别于CHAR_LENGTH()的字符长度,适用于多字节字符集(如UTF-8)的数据验证... 目录1. LENGTH()函数的基本语法2. LENGTH()函数的返回值2.1 示例1:计算字符串

Linux在线解压jar包的实现方式

《Linux在线解压jar包的实现方式》:本文主要介绍Linux在线解压jar包的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux在线解压jar包解压 jar包的步骤总结Linux在线解压jar包在 Centos 中解压 jar 包可以使用 u

浅析Spring如何控制Bean的加载顺序

《浅析Spring如何控制Bean的加载顺序》在大多数情况下,我们不需要手动控制Bean的加载顺序,因为Spring的IoC容器足够智能,但在某些特殊场景下,这种隐式的依赖关系可能不存在,下面我们就来... 目录核心原则:依赖驱动加载手动控制 Bean 加载顺序的方法方法 1:使用@DependsOn(最直

linux解压缩 xxx.jar文件进行内部操作过程

《linux解压缩xxx.jar文件进行内部操作过程》:本文主要介绍linux解压缩xxx.jar文件进行内部操作,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、解压文件二、压缩文件总结一、解压文件1、把 xxx.jar 文件放在服务器上,并进入当前目录#

Linux系统性能检测命令详解

《Linux系统性能检测命令详解》本文介绍了Linux系统常用的监控命令(如top、vmstat、iostat、htop等)及其参数功能,涵盖进程状态、内存使用、磁盘I/O、系统负载等多维度资源监控,... 目录toppsuptimevmstatIOStatiotopslabtophtopdstatnmon

Android ClassLoader加载机制详解

《AndroidClassLoader加载机制详解》Android的ClassLoader负责加载.dex文件,基于双亲委派模型,支持热修复和插件化,需注意类冲突、内存泄漏和兼容性问题,本文给大家介... 目录一、ClassLoader概述1.1 类加载的基本概念1.2 android与Java Class