Linux内存地址管理

2023-11-19

Linux系统中的每个内存地址都是虚拟的,它们不直接指向任何物理内存地址。每当访问内存位置时,可以执行转换机制以匹配相应的物理内存,所以我们在程序中必须用虚拟地址来访问数据。

注:下面所说的内核空间和用户空间这样的术语指的都是虚拟地址空间

系统内存布局

在Linux系统中,每个进程都有自己独立的虚拟地址空间。它是一种内存沙箱,存在于进程的生命周期中。在32位系统上,该地址空间大小是4GB。针对每一个进程,4GB的地址空间被分割成两个部分:

  • 内核空间虚拟地址
  • 用户空间虚拟地址

分割方式依赖于特殊的内核配置选项CONFIG_PAGE_OFFSET,这个选项定义内核虚拟地址部分在进程地址空间的起始位置。典型的进程虚拟地址空间布局如下图:
在这里插入图片描述
内核空间和用户空间所使用的地址都是虚拟地址,不同的是,访问内核空间地址需要特权模式,当CPU运行用户空间代码时,活动进程被认为运行在用户模式下,当CPU运行内核空间代码时,活动进程被认为运行在内核模式下。

内核与每个进程共享其地址空间,原因如下:因为每个进程在给定的时刻都使用系统调用,这将涉及内核。将内核的虚拟内存地址映射到每个进程的虚拟地址空间能够避免每次进入(或者退出)内核时内存地址切换产生的开销。这就是内核地址空间被永久映射到每个进程顶部的原因—加快系统调用对内核的访问。

每个进程地址空间顶部都是内核的虚拟地址空间,这一部分每个进程都是相同的。

内存管理单元把内存组织为大小固定的单元—页面,内存页(虚拟页)指的是连续虚拟内存块,内核数据结构也使用相同的名称页面来表示内存页。帧(页面帧)指一段固定长度的连续物理内存块,操作系统在其上映射内存页。每个页面帧都有一个号码,叫做页面帧号(PFN)。

内核地址的低端和高端内存概念

Linux内核具有自己的虚拟地址空间。比如32位的x86,内核的虚拟地址空间是1GB大小,分成两个部分:

  • 低端内存或LOWMEM:第一个896MB
  • 高端内存或HIGHMEM:顶部的128MB

在这里插入图片描述

低端内存

内核地址空间的第一个896MB空间构成低端内存区域。在启动早期,内核永久映射这896MB的空间。该映射产生的地址为逻辑地址,这些都是虚拟地址,但是减去固定的偏移量后就可以将其转换为物理地址。因为映射是永久的,并且事先知道。大多数内核内存函数返回低端内存。事实上,为了满足不同的用途,内核内存被划分为区域,LOWMEM的第一个16MB内存保留为DMA使用。内核空间可以确定3种不同的内存区域:

  • ZONE_DMA:包含的内存页面帧在0~16MB,用于直接内存访问(DMA)
  • ZONE_NORMAL:包含的内存页面帧为16MB~896MB,常规使用
  • ZONE_HIGHMEM:包含的内存页面帧位于896MB及其以上

这就是说,512MB的系统上,不存在以上的划分。

逻辑地址的另一个定义:线性映射到物理地址上的内核空间中的地址,可以用偏移量或者应用位掩码将其转为物理地址,使用__pa(地址)宏可以将逻辑地址(内核中的虚拟地址)转换为物理地址,使用__va(地址)可以做相反的操作。

高端内存

内核地址空间顶部顶部128MB称为高端内存区域,内核用它临时映射1GB以上的物理内存,当需要访问896MB以上的物理内存时,内核会使用这128MB创建到其虚拟地址空间的临时映射,也就是将需要访问数据的物理页映射到这128MB内核虚拟地址空间来,从而实现访问所有物理页面的目标。可以把高端内存定义为逻辑地址存在的内存,但不会将其永久映射到内核地址空间。896MB以上的物理内存按需映射到HIGHMEM区域的128MB。

访问高端内存的映射由内核动态创建,访问后销毁,这使高内存访问速度变慢,64位系统上不存在高端内存这一概念。

地址转换和MMU

每次访问内存位置时,由CPU完成从虚拟地址到物理地址的转换。该机制称为地址转换,这由CPU中的内存管理单元(MMU)来执行。MMU转换的都是虚拟地址,所以访问数据,必须是虚拟地址,不能是物理地址,否则访问不了数据。

对于虚拟内存,内存组织为固定大小的页,而物理内存则按帧组织,页面表(PTE)概念的引入是为了管理页面和帧之间的映射。页面分部在表间,因此每个PTE的表项对于一个页面和帧之间的映射,然后给每个进程一组页面表来描述其整个内存空间。

Linux中的四级分页模型

Linux采用了一种同时适用于32位和64位系统的普通分页模型。从2.6.11版本开始,采用了四级分页模型:请添加图片描述
上图展示的4种页表分别被称为:

  • 页全局目录(Page Global Directory,PGD)
  • 页上级目录(Page Upper Directory,PUD)
  • 页中间目录(Page Middle Directory,PMD)
  • 页表(Page Table,PTE)

虚拟地址被分为5个部分,每个页表项指向一个页框,每一部分的大小与具体的计算机体系结构有关。

MMU如何知道进程页面表?很简单,MMU不存储任何地址。但CPU有一个特殊的寄存器,称为页面表基址寄存器(PTBR)或转换基址寄存器0(TTBR0),它指向进程1级页面表(PGD)的基址。这正是struct mm_struct的字段pgd指向的地址:current->mm.pgd == TTBR0.上面图中的cr3保存的就是该值

虚拟地址字段

下面宏简化了页表处理:

  • PAGE_SHIFT:指定Offset字段的位数,这个宏由PAGE_SIZE使用返回页的大小。最后,PAGE_MASK宏用以屏蔽Offset字段的所有位。
  • PMD_SHIFT:指定虚拟地址的offset字段和table字段的总位数,PMD_MASK宏用于屏蔽Offset字段与Table字段的所有位
  • PUD_SHIFT:确定页上级目录项能映射的区域大小的对数,PUD_MASK宏用于屏蔽Offset字段,Table字段、Middle Air字段和Upper Air字段的所有位
  • PGDIR_SHIFT:确定页全局目录项能映射的区域大小的对数。PGDIR_MASK宏用于屏蔽Offset、Table、Middle Air及Upper Air字段的所有位
  • PTRS_PER_PTE,PTRS_PER_PMD,PTRS_PER_PUS以及PTRS_PER_PGD:用于计算页表、页中间目录、页上级目录和页全局目录表中表现的个数。

页表处理

pte_t、pmd_t、pud_t和pgd_t分别描述页表项、页中间目录项、页上级目录项和页全局目录项。

五个类型转换宏(__pte,__pmd,__pgd和__pgprot)把一个无符号整数转换成所需的类型。另外的五个类型转换宏(pte_val,pmd_val,pud_val,pgd_val和pgport_val)执行相反的转换。

如果相应的表项值位0,那么,宏pte_none、pmd_none、pud_none、和pgd_none产生的值为1,否则产生的值为0

将虚拟地址转换物理地址

进程访问的都是用户空间内虚拟地址,内核访问的都是内核虚拟地址,进程用不了内核虚拟地址,同样内核用不了进程内虚拟地址。如果我们将用户空间地址传给内核,内核必须先将找到该用户空间虚拟地址对应的物理地址,再将物理地址转换成内核虚拟地址,内核才能访问数据

代码地址:http://edsionte.com/techblog/archives/1966

static void get_pgtable_macro(void)
{
    printk("PAGE_OFFSET = 0x%lx\n", PAGE_OFFSET);
    printk("PGDIR_SHIFT = %d\n", PGDIR_SHIFT);
    printk("PUD_SHIFT = %d\n", PUD_SHIFT);
    printk("PMD_SHIFT = %d\n", PMD_SHIFT);
    printk("PAGE_SHIFT = %d\n", PAGE_SHIFT);
    printk("PTRS_PER_PGD = %d\n", PTRS_PER_PGD);
    printk("PTRS_PER_PUD = %d\n", PTRS_PER_PUD);
    printk("PTRS_PER_PMD = %d\n", PTRS_PER_PMD);
    printk("PTRS_PER_PTE = %d\n", PTRS_PER_PTE);
    printk("PAGE_MASK = 0x%lx\n", PAGE_MASK);
}
static unsigned long vaddr2paddr(unsigned long vaddr)
{
    pgd_t *pgd;
    pud_t *pud;
    pmd_t *pmd;
    pte_t *pte;
    unsigned long paddr = 0;
    unsigned long page_addr = 0;
    unsigned long page_offset = 0;
    pgd = pgd_offset(current->mm, vaddr);
    printk("pgd_val = 0x%lx\n", pgd_val(*pgd));
    printk("pgd_index = %lu\n", pgd_index(vaddr));
    if (pgd_none(*pgd)) {
        printk("not mapped in pgd\n");
        return -1;
    }
    pud = pud_offset(pgd, vaddr);
    printk("pud_val = 0x%lx\n", pud_val(*pud));
    if (pud_none(*pud)) {
        printk("not mapped in pud\n");
        return -1;
    }
    pmd = pmd_offset(pud, vaddr);
    printk("pmd_val = 0x%lx\n", pmd_val(*pmd));
    printk("pmd_index = %lu\n", pmd_index(vaddr));
    if (pmd_none(*pmd)) {
        printk("not mapped in pmd\n");
        return -1;
    }
    pte = pte_offset_kernel(pmd, vaddr);
    printk("pte_val = 0x%lx\n", pte_val(*pte));
    printk("pte_index = %lu\n", pte_index(vaddr));
    if (pte_none(*pte)) {
        printk("not mapped in pte\n");
        return -1;
    }
    //页框物理地址机制 | 偏移量
    page_addr = pte_val(*pte) & PAGE_MASK;
    page_offset = vaddr & ~PAGE_MASK;
    paddr = page_addr | page_offset;
    printk("page_addr = %lx, page_offset = %lx\n", page_addr, page_offset);
        printk("vaddr = %lx, paddr = %lx\n", vaddr, paddr);
    return paddr;
}
static int __init v2p_init(void)
{
    unsigned long vaddr = 0;
    printk("vaddr to paddr module is running..\n");
    get_pgtable_macro();
    printk("\n");
    vaddr = (unsigned long)vmalloc(1000 * sizeof(char));
    if (vaddr == 0) {
        printk("vmalloc failed..\n");
        return 0;
    }
    printk("vmalloc_vaddr=0x%lx\n", vaddr);
    vaddr2paddr(vaddr);
    printk("\n\n");
    vaddr = __get_free_page(GFP_KERNEL);
    if (vaddr == 0) {
        printk("__get_free_page failed..\n");
        return 0;
    }
    printk("get_page_vaddr=0x%lx\n", vaddr);
    vaddr2paddr(vaddr);
    return 0;
}
static void __exit v2p_exit(void)
{
    printk("vaddr to paddr module is leaving..\n");
        vfree((void *)vaddr);
        free_page(vaddr);
}

整个程序的结构如下:

  1. get_pgtable_macro()打印当前系统分页机制中的一些宏。

  2. 通过vmalloc()在内核空间中分配内存,调用vaddr2paddr()将虚拟地址转化成物理地址。

  3. 通过__get_free_pages()在内核空间中分配页框,调用vaddr2paddr()将虚拟地址转化成物理地址。

  4. 分别通过vfree()和free_page()释放申请的内存空间。

vaddr2paddr()的执行过程如下:

  1. 通过pgd_offset计算页全局目录项的线性地址pgd,传入的参数为内存描述符mm和线性地址vaddr。接着打印pgd所指的页全局目录项。

  2. 通过pud_offset计算页上级目录项的线性地址pud,传入的参数为页全局目录项的线性地址pgd和线性地址vaddr。接着打印pud所指的页上级目录项。

  3. 通过pmd_offset计算页中间目录项的线性地址pmd,传入的参数为页上级目录项的线性地址pud和线性地址vaddr。接着打印pmd所指的页中间目录项。

  4. 通过pte_offset_kernel计算页表项的线性地址pte,传入的参数为页中间目录项的线性地址pmd和线性地址vaddr。接着打印pte所指的页表项。

  5. pte_val(*pte)先取出页表项,与PAGE_MASK相与的结果是得到要访问页的物理地址;vaddr&~PAGE_MASK用来得到线性地址offset字段;两者或运算得到最终的物理地址。

  6. 打印物理地址。

我们可以获得物理地址了,就可以使用__pa(地址)宏可以将物理地址转换为逻辑地址(内核中的虚拟地址),使用__va(地址)可以做相反的操作

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux内存地址管理 的相关文章

  • 尝试安装 LESS 时出现“请尝试以 root/管理员身份再次运行此命令”错误

    我正在尝试在我的计算机上安装 LESS 并且已经安装了节点 但是 当我输入 node install g less 时 出现以下错误 并且不知道该怎么办 FPaulMAC bin paul npm install g less npm ER
  • 无法使用 wget 在 CentOS 机器上安装 oracle jdk

    我想在CentOS上安装oracle java jdk 8 我无法安装 java jdk 因为当我尝试使用命令安装 java jdk 时 root ADARSH PROD1 wget no cookies no check certific
  • Linux 内核标识符中前导和尾随下划线的含义是什么?

    我不断遇到一些小约定 比如 KERNEL Are the 在这种情况下 是内核开发人员使用的命名约定 还是以这种方式命名宏的语法特定原因 整个代码中有很多这样的例子 例如 某些函数和变量以 甚至 这有什么具体原因吗 它似乎被广泛使用 我只需
  • Linux 可执行文件与 OS X“兼容”吗?

    如果您在基于 Linux 的平台上用 C 语言编译一个程序 然后将其移植以使用 MacOS 库 它会工作吗 来自编译器的核心机器代码在 Mac 和 Linux 上兼容吗 我问这个问题的原因是因为两者都是 基于 UNIX 的 所以我认为这是真
  • 如何在shell中输出返回码?

    我正在尝试通过调用自定义 shell 脚本sh bin sh c myscript sh gt log txt 2 gt 1 echo 该命令的输出是创建的后台进程的 PID 我想指导 bin sh保存返回码myscript sh到某个文件
  • 在 Linux 上使用多处理时,TKinter 窗口不会出现

    我想生成另一个进程来异步显示错误消息 同时应用程序的其余部分继续 我正在使用multiprocessingPython 2 6 中的模块来创建进程 我试图用以下命令显示窗口TKinter 这段代码在Windows上运行良好 但在Linux上
  • ubuntu:升级软件(cmake)-版本消歧(本地编译)[关闭]

    Closed 这个问题是无关 help closed questions 目前不接受答案 我的机器上安装了 cmake 2 8 0 来自 ubuntu 软件包 二进制文件放置在 usr bin cmake 中 我需要将 cmake 版本至少
  • 将 jar 作为 Linux 服务运行 - init.d 脚本在启动应用程序时卡住

    我目前正在致力于在 Linux VM 上实现一个可运行的 jar 作为后台服务 我已经使用了找到的例子here https gist github com shirish4you 5089019作为工作的基础 并将 start 方法修改为
  • docker容器大小远大于实际大小

    我正在尝试从中构建图像debian latest 构建后 报告的图像虚拟大小来自docker images命令为 1 917 GB 我登录查看尺寸 du sh 大小为 573 MB 我很确定这么大的尺寸通常是不可能的 这里发生了什么 如何获
  • jq中如何分组?

    这是 json 文档 name bucket1 clusterName cluster1 name bucket2 clusterName cluster1 name bucket3 clusterName cluster2 name bu
  • 在脚本内使用不带密码的 sudo

    由于某种原因 我需要作为用户在没有 sudo 的情况下运行脚本 script sh 该脚本需要 root 权限才能工作 我认为将 sudo 放入 script sh 中是唯一的解决方案 让我们举个例子 script sh bin sh su
  • 如何使用 GOPATH 的 Samba 服务器位置?

    我正在尝试将 GOPATH 设置为共享网络文件夹 当我进入 export GOPATH smb path to shared folder I get go GOPATH entry is relative must be absolute
  • 当 grep "\\" XXFile 我得到“尾随反斜杠”

    现在我想查找是否有包含 字符的行 我试过grep XXFile但它暗示 尾随反斜杠 但当我尝试时grep XXFile没关系 谁能解释一下为什么第一个案例无法运行 谢谢 区别在于 shell 处理反斜杠的方式 当你写的时候 在双引号中 sh
  • “make install”将库安装在 /usr/lib 而不是 /usr/lib64

    我正在尝试在 64 位 CentOS 7 2 上构建并安装一个库 为了这个目的我正在跑步 cmake DCMAKE BUILD TYPE Release DCMAKE INSTALL PREFIX usr DCMAKE C COMPILER
  • Linux:如何设置进程的时区?

    我需要设置在 Linux 机器上启动的各个进程的时区 我尝试设置TZ变量 在本地上下文中 但它不起作用 有没有一种方法可以使用与系统日期不同的系统日期从命令行运行应用程序 这可能听起来很愚蠢 但我需要一种sandbox系统日期将被更改的地方
  • 快速像素绘图库

    我的应用程序以每像素的方式生成 动画 因此我需要有效地绘制它们 我尝试过不同的策略 库 但结果并不令人满意 尤其是在更高分辨率的情况下 这是我尝试过的 SDL 好的 但是慢 OpenGL 像素操作效率低下 xlib 更好 但仍然太慢 svg
  • 如何在c linux中收听特定接口上的广播?

    我目前可以通过执行以下操作来收听我编写的简单广播服务器 仅广播 hello int fd socket PF INET SOCK DGRAM 0 struct sockaddr in addr memset addr 0 sizeof ad
  • NUMA 在虚拟内存中是如何表示的?

    有许多资源 https en wikipedia org wiki Non uniform memory access从硬件角度描述NUMA的架构性能影响 http practical tech com infrastructure num
  • 监控子进程的内存使用情况

    我有一个 Linux 守护进程 它分叉几个子进程并监视它们是否崩溃 根据需要重新启动 如果父进程可以监视子进程的内存使用情况 以检测内存泄漏并在超出一定大小时重新启动子进程 那就太好了 我怎样才能做到这一点 您应该能够从 proc PID
  • 捕获实时流量时如何开启纳秒精度?

    如何告诉 libpcap v1 6 2 将纳秒值存储在struct pcap pkthdr ts tv usec 而不是微秒值 捕获实时数据包时 Note This question is similar to How to enable

随机推荐

  • 【云原生之k8s】K8s 管理工具 kubectl 详解(二)

    K8S模拟项目 Kubectl是管理k8s集群的命令行工具 通过生成的json格式传递给apiserver进行创建 查看 管理的操作 帮助信息 root localhost bin kubectl help kubectl controls
  • mysqlbinglog基于即时点还原

    mysqlbinglog基于即时点还原 mysqlbinlog介绍 要想从二进制日志恢复数据 你需要知道当前二进制日志文件的路径和文件名 一般可以从选项文件 即my cnf or my ini 取决于你的系统 中找到路径 mysql5 7开
  • SAR成像系列:【3】合成孔径雷达(SAR)的二维回波信号与简单距离多普勒(RD)算法 (附matlab代码)

    合成孔径雷达发射信号以线性调频信号 LFM 为基础 目前大部分合成孔径雷达都是LFM体制 为了减轻雷达重量也采用线性调频连续波 FMCW 体制 为了获得大带宽亦采用线性调频步进频 FMSF 体制 1 LFM信号 LFM的主要特点在于可以使载
  • 操作系统内存管理——分区、页式、段式、段页式管理

    1 内存管理方法 内存管理主要包括虚地址 地址变换 内存分配和回收 内存扩充 内存共享和保护等功能 2 连续分配存储管理方式 连续分配是指为一个用户程序分配连续的内存空间 连续分配有单一连续存储管理和分区式储管理两种方式 2 1 单一连续存
  • 谈谈Qt信号与槽

    关于Qt信号与槽 Qt信号与槽本质类似观察者模式 观察者模式 Observer Pattern 定义对象间的一种一对多依赖关系 使得每当一个对象状态发生改变时 其相关依赖对象皆得到通知并被自动更新 观察者模式又叫做发布 订阅 Publish
  • 5G Capital一年,“首都标准”初现

    在北京生活许多年 如果问我什么时候京味浓度最高 答案可能是下了飞机 走上出租车的那一刻 北京司机连闲聊都是一副见过世面的样子 你研究人工智能 我觉得吧交通管理就该这样那样 高铁咱都造出来了 什么高科技我看中国人很快就能搞出来 冬奥会场馆建得
  • scatter函数绘制散点图——MATLAB

    1 scatter X Y 在矢量X和Y指定的位置显示彩色圆 如 scatter 1 2 3 4 4 5 6 7 效果如图 默认彩色圆为蓝色空心圆 2 scatter X Y S S确定每个标记的面积 S可以是与X和Y相同长度的矢量或标量
  • Gibbs 采样基本原理和仿真

    Gibbs 采样基本原理和仿真 文章目录 Gibbs 采样基本原理和仿真 1 基本概念 1 1 Gibbs采样算法 1 2 Markov链 1 2 1 Markov链的定义 1 2 2 Markov链的细致平稳条件 1 2 3 Markov
  • 初学者怎么高效率学习c语言?

    想学C语言我们首先的了解C语言是什么 它是一门面向过程的 抽象化的通用程序设计语言 广泛应用于底层开发 C语言能以简易的方式编译以及处理低级存储器 C语言是仅产生少量的机器语言以及不需要任何运行环境支持就可以运行的高效率程序设计语言 尽管C
  • ubuntu 11配置hadoop

    最近没事 研究下ubuntu 配置hadoop ubuntu版本 64 bit 11 04 hadoop版本 hadoop1 2 1 一 在Ubuntu下创建hadoop用户组和用户 1 创建hadoop用户组 sudo addgroup
  • Samy XSS Worm 分析

    Samy Worm MySpace com允许用户通过控制标签的style属性 samy构造css xss MySpace过滤了很多关键字 利用拆分法绕过 div标签如下 div div 其中expr字符串的内容为如下javascript代
  • 软件质量保证与测试技术实验报告(二)黑盒测试用例设计

    1 实验名称 黑盒测试用例设计 2 实验目的 学会用等价类划分法和边界值法设计测试用例 进行功能测试 3 实验内容 题目1 NextDate程序的功能是按年 月 日的顺序输入一个日期 输出为输入日期后一天的日期 请使用等价类和边界值法对Ne
  • windows内核驱动开发(WDK环境搭建)

    去官网下载WDK安装包和Visual Studio 下载 Windows 驱动程序工具包 WDK Windows drivers Microsoft Docs 首先安装Visual Studio 这个就不用我介绍了怎么安装了 下面直接下载步
  • JESD204B(RX)协议接口说明。

    解释一下Vivado IP协议中的Shared Logic in Example 与 Shared Logic in Core 首先 什么是Shared Logic 字面意思很好理解 就是共享逻辑 主要包括时钟 复位等逻辑 当选择Share
  • grafana elasticsearch es 创建变量variable时,query里的查询语句是对的,但是预览没有数据

    问题 图中的query输入框中输入正确 并且es中有rulename字段 rulename也有值 但是此处预览里没有值 按F12看了grafana的请求体和响应体才发现 rulename是text类型的 不能进行聚集 所以这里查不到数据 解
  • -离散数学-期末练习题解析

    一 选择题 二 填空题 三 计算题 四 简答题 五 证明题 六 应用题 一 选择题 下列句子中 是命题 A 2是常数 B 这朵花多好看啊 C 请把们关上 D 下午有会吗 A 命题是能判断真假的陈述句 B是感叹句 C是祈使句 D是疑问句 令p
  • sqlserver开启sql登录方式!

    安装sqlserver的时候只有windows登录 但有时也要用到sqlserver登录的方式 总不可能重新安装sqlserver吧 1 先用windows登录sqlserver 依次单击 安全性 gt 登录名 gt sa 右键打开sa的属
  • Android_UI开发总结(一):RadioButton与RadioGroup使用

    关于RadioButton与RadioGroup的API详解 gt https www cnblogs com Im Victor p 6238437 html 下面记录在使用RadioButton和RadioGroup中遇到的三点问题 1
  • MPLS原理和配置实验

    一 MPLS背景 90年代初 互联网流量快速增长 而由于当时硬件技术的限制 路由器采用最长匹配算法逐跳转发数据包 成为网络数据转发的瓶颈 快速路由技术成为当时研究的一个热点 在各种方案中 IETF确定MPLS协议作为标准的协议 MPLS采用
  • Linux内存地址管理

    文章目录 系统内存布局 内核地址的低端和高端内存概念 低端内存 高端内存 地址转换和MMU Linux中的四级分页模型 虚拟地址字段 页表处理 将虚拟地址转换物理地址 Linux系统中的每个内存地址都是虚拟的 它们不直接指向任何物理内存地址