[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

相关文章

windows和Linux使用命令行计算文件的MD5值

《windows和Linux使用命令行计算文件的MD5值》在Windows和Linux系统中,您可以使用命令行(终端或命令提示符)来计算文件的MD5值,文章介绍了在Windows和Linux/macO... 目录在Windows上:在linux或MACOS上:总结在Windows上:可以使用certuti

Linux之systemV共享内存方式

《Linux之systemV共享内存方式》:本文主要介绍Linux之systemV共享内存方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、工作原理二、系统调用接口1、申请共享内存(一)key的获取(二)共享内存的申请2、将共享内存段连接到进程地址空间3、将

SpringBoot中配置文件的加载顺序解读

《SpringBoot中配置文件的加载顺序解读》:本文主要介绍SpringBoot中配置文件的加载顺序,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录SpringBoot配置文件的加载顺序1、命令⾏参数2、Java系统属性3、操作系统环境变量5、项目【外部】的ap

快速修复一个Panic的Linux内核的技巧

《快速修复一个Panic的Linux内核的技巧》Linux系统中运行了不当的mkinitcpio操作导致内核文件不能正常工作,重启的时候,内核启动中止于Panic状态,该怎么解决这个问题呢?下面我们就... 感谢China编程(www.chinasem.cn)网友 鸢一雨音 的投稿写这篇文章是有原因的。为了配置完

Linux命令之firewalld的用法

《Linux命令之firewalld的用法》:本文主要介绍Linux命令之firewalld的用法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux命令之firewalld1、程序包2、启动firewalld3、配置文件4、firewalld规则定义的九大

Linux之计划任务和调度命令at/cron详解

《Linux之计划任务和调度命令at/cron详解》:本文主要介绍Linux之计划任务和调度命令at/cron的使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux计划任务和调度命令at/cron一、计划任务二、命令{at}介绍三、命令语法及功能 :at

Linux下如何使用C++获取硬件信息

《Linux下如何使用C++获取硬件信息》这篇文章主要为大家详细介绍了如何使用C++实现获取CPU,主板,磁盘,BIOS信息等硬件信息,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下... 目录方法获取CPU信息:读取"/proc/cpuinfo"文件获取磁盘信息:读取"/proc/diskstats"文

Linux内核参数配置与验证详细指南

《Linux内核参数配置与验证详细指南》在Linux系统运维和性能优化中,内核参数(sysctl)的配置至关重要,本文主要来聊聊如何配置与验证这些Linux内核参数,希望对大家有一定的帮助... 目录1. 引言2. 内核参数的作用3. 如何设置内核参数3.1 临时设置(重启失效)3.2 永久设置(重启仍生效

Vue3组件中getCurrentInstance()获取App实例,但是返回null的解决方案

《Vue3组件中getCurrentInstance()获取App实例,但是返回null的解决方案》:本文主要介绍Vue3组件中getCurrentInstance()获取App实例,但是返回nu... 目录vue3组件中getCurrentInstajavascriptnce()获取App实例,但是返回n

在Spring Boot中浅尝内存泄漏的实战记录

《在SpringBoot中浅尝内存泄漏的实战记录》本文给大家分享在SpringBoot中浅尝内存泄漏的实战记录,结合实例代码给大家介绍的非常详细,感兴趣的朋友一起看看吧... 目录使用静态集合持有对象引用,阻止GC回收关键点:可执行代码:验证:1,运行程序(启动时添加JVM参数限制堆大小):2,访问 htt