了解Linux内核内存映射

news2024/12/25 0:44:26

【推荐阅读】

深入linux内核架构--进程&线程

路由选择协议——RIP协议

轻松学会linux下查看内存频率,内核函数,cpu频率

纯干货,linux内存管理——内存管理架构(建议收藏)

概述Linux内核驱动之GPIO子系统API接口

一. 内存映射原理

由于所有用户进程总的虚拟地址空间比可用的物理内存大很多,因此只有最常用的部分才与物理页帧关联。这不是问题,因为大多数程序只占用实际可用内存的一小部分。在将磁盘上的数据映射到进程的虚拟地址空间的时,内核必须提供数据结构,以建立虚拟地址空间的区域和相关数据所在位置之间的关联,linux软件系统多级页表映射机制

注:上图中的最右侧page,代表软件层面的页帧率,并非真正的物理内存。真正的物理内存也会分页,名称为页框,页帧到页框的转换则是有MMU自动完成的。以下讨论均不考虑MMU(MMU完成的工作作为黑箱看待)

二. Linux的页表实现

一级页表

一个32位逻辑地址空间的计算机系统,总地址4G字节,页大小为4KB,则有4G/4K = 1M个页,那么页表中需要存放1M个条目(每个条目代表一个页)。假设每个条目占4B,则需要4M字节内存来存放页表。

每个进程都需要管理所有4G内存,所以每个进程都要4M来存放页表,极其浪费。

二级页表

虚拟地址到物理页地址的转换过程如下:

  • 结合在CR3寄存器中存放的页目录(page directory, PGD)的这一页的物理地址,再加上从虚拟地址中抽出高10位叫做页目录表项(内核也称这为pgd)的部分作为偏移, 即定位到可以描述该地址的pgd;
  • 从该pgd中可以获取可以描述该地址的页表的物理地址,再加上从虚拟地址中抽取中间10位作为偏移, 即定位到可以描述该地址的pte;
  • 在这个pte中即可获取该地址对应的页的物理地址, 加上从虚拟地址中抽取的最后12位,即形成该页的页内偏移, 即可最终完成从虚拟地址到物理地址的转换。

其中 虚拟地址的组成:

DIRECTORY [22:31] 可表示1024个页目录(PGD)

TABLE[12:21] 可表示1024个页表(PTE)

OFFSET[22:31] 可表示4096个物理内存

因此最大映射物理内存大小为 102410244096 = 4G/Byte

三级页表

当X86引入物理地址扩展(Pisycal Addrress Extension, PAE)后,可以支持大于4G的物理内存(36位),但虚拟地址依然是32位,原先的页表项不适用,它实际多4 bytes被扩充到8 bytes,这意味着,每一页现在能存放的pte数目从1024变成512了(4k/8)。相应地,页表层级发生了变化,Linus新增加了一个层级,叫做页中间目录(page middle directory, PMD)

办法是针对使用2级页表的架构,把PMD抽象掉,即虚设一个PMD表项。这样在page table walk过程中,PGD本直接指向PTE的,现在不了,指向一个虚拟的PMD,然后再由PMD指向PTE。这种抽象保持了代码结构的统一。

该方法其实就是在原有虚拟地址组成中的几位作为PMD索引:

|31| -----PGD-----|29|-----PMD-----| 20|-----PTE-----|11|----OFFSET|-----|0|

四级页表

硬件在发展,3级页表很快又捉襟见肘了,原因是64位CPU出现了, 比如X86_64, 它的硬件是实实在在支持4级页表的。它支持48位的虚拟地址空间(不过Linux内核最开始只使用47位)。如下:

需注意软件的页表映射依赖于硬件所支持的映射级别,目前ARM64支持2/3/4 级映射,假如ARM配置的映射级别为3级,那么linux的映射表中 PGD=PUD,即实际为三级映射。

文件索引节点

设文件索引节点中有7 个地址项,其中4 个地址项是直接地址索引,2 个地址项是一级间

接地址索引,1 个地址项是二级间接地址索引,每个地址项大小为4B,若磁盘索引块和磁盘数据块

大小均为256B,请计算可表示的单个文件最大长度。

每个索引块上可以存放的索引项为256B/4B=26 =64个。直接索引的数据块有4 块;两个一级

间接索引指向的数据块有2×26=27 = 128个;一个二级间接索引指向的数据块有1×26×26=212 个。所以单个文件最大可以有4+27+212(=4228)个数据块。

文件大小为 4228×256B=(4+27+212)×28=33KB+1MB=1057KB

三. 虚拟空间分布

linux将整个虚拟空间分为用户空间与内核空间,且每个用户进程均占用一份完整的用户虚拟空间

上图为64位系统的虚拟地址空间分布用户空间大小为512G,32位系统用户空间结束地址为0xc0000000,大小为3G.

用户空间

用户虚拟地址空间大小为3G,每个应用程序均独占完整的3G虚拟空间。这里应该可以看出,真是的物理内存不可能等于虚拟内存,因为同一时间会有众多进程运行,每一进程均拥有3G内存,而实际物理内存不可能满足所有进程的需求。此时上面说到的内存映射便发挥了作用。

  • 一般而言,一个进程虽然拥有3G虚拟内存访问权限,但是实际需要的内存可能很少,另外一个进程大部分的主体,比如代码段,只读数据段等并不需要实时驻留在内存中,因为这些内容在大多数时间是“死的”。因此内存映射的第一个作用就是:
  • 只会将某一进程此刻需要的内存大小映射到物理内存,其它暂时不需要的内容交换到硬盘存储即可。当进程需要使用在硬盘中的内容或者需要动态申请内存时,操作系统会利用缺页操作,触发一次内存映射,将另外的物理内存映射进虚拟内存,供程序使用,这样对于进程而言,则认为内存总是够用的。
  • 各个进程均拥有3G虚拟内存,那么操作系统是如何做到各进程所使用的实际物理内存不会互相占用呢?实际上,各个进程均有自己的内存映射表。任意一个时刻,在一个CPU上只有一个进程在运行。所以对于此CPU来讲,在这一时刻,整个系统只存在一个4GB的虚拟地址空间,这个虚拟地址空间是面向此进程的。当进程发生切换的时候,虚拟地址空间也随着切换。由此可以看出,每个进程都有自己的虚拟地址空间,只有此进程运行的时候,其虚拟地址空间才被运行它的CPU所知。在其它时刻,其虚拟地址空间对于CPU来说,是不可知的。所以尽管每个进程都可以有4 GB的虚拟地址空间,但在CPU眼中,只有一个虚拟地址空间存在。虚拟地址空间的变化,随着进程切换而变化。

内核空间

内核空间具有相对独立的特性。即内核的虚拟地址空间范围是自己独有的,不与任何用户进程共享(内核实质也是一个进程)。这样可保证内核空间的安全性。但是由于内核虚拟地址空间较小0xc000000~0xFFFFFFFF 仅有1G大小,这一大小往往小于实际的物理内存,因此内核空间需要额外的方式来访问到所有物理内存。

由上图可知,内核空间又分为三大部分:

  • ZONE_DMA :0XC000000 + 16M 线性映射区
  • 该区域的物理页面专门供I/O设备的DMA使用。之所以需要单独管理DMA的物理页面,是因为DMA使用物理地址访问内存,不经过MMU,并且需要连续的缓冲区,所以为了能够提供物理上连续的缓冲区,必须从物理地址空间专门划分一段区域用于DMA。例如dma_alloc_coherent函数获取的内存就是ZONE_DMA内存
  • ZONE_NORMAL :0XC100000 + 880M 线性映射区
  • 该区域的物理页面是内核能够直接使用的,比如内核程序中代码段、全局变量以及kmalloc获取的堆内存等。从此处获取内存一般是连续的,但是不能太大。
  • ZONE_HIGHMEM :0xF8000000 + 28M

该区域比较负责又可细分为三部分:

  • 非连续内存区
  • 非连续内存分配是指将物理地址不连续的页框映射到线性地址连续的线性地址空间,主要应用于大容量的内存分配。采用这种方式分配内存的主要优点是避免了外部碎片,而缺点是必须打乱内核页表,而且访问速度较连续分配的物理页框慢。函数vmalloc即是通过该部分虚拟地址来映射物理内存。
  • 永久内核映射区
  • 如果是通过 alloc_page() 获得了高端内存对应的 page,如何给它找个线性空间?
  • 内核专门为此留出一块线性空间,从 PKMAP_BASE 到 FIXADDR_START ,用于映射高端内存。在 2.6内核上,这个地址范围是 4G-8M 到 4G-4M 之间。这个空间起叫”内核永久映射空间”或者”永久内核映射空间”。这个空间和其它空间使用同样的页目录表,对于内核来说,就是 swapper_pg_dir,对普通进程来说,通过 CR3 寄存器指向。通常情况下,这个空间是 4M 大小,因此仅仅需要一个页表即可,内核通过来 pkmap_page_table 寻找这个页表。通过 kmap(),可以把一个 page 映射到这个空间来。由于这个空间是 4M 大小,最多能同时映射 1024 个 page。因此,对于不使用的的 page,及应该时从这个空间释放掉(也就是解除映射关系),通过 kunmap() ,可以把一个 page 对应的线性地址从这个空间释放出来。
  • 临时固定映射区
  • fixmap和pkmap的最大区别是fixmap不会被阻塞,因此可以在中断上下文中使用。实际上每次fixmap都是可以成功执行映射的,原因是fixmap不会动态申请映射,它是以固定映射的方式进行的,如果我们传入了一个映射type,它就肯定会执行对应的映射,而把之前的映射冲刷掉,因此fixmap的映射关系是靠内核代码来进行维护的,所以内核需要保证同一个映射区不被重复使用,从而出现错误。

前面我们解释了高端内存的由来。 Linux将内核地址空间划分为三部分ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM,高端内存HIGH_MEM地址空间范围为 0xF8000000 ~ 0xFFFFFFFF(896MB~1024MB)。那么如内核是如何借助128MB高端内存地址空间是如何实现访问可以所有物理内存?

当内核想访问高于896MB物理地址内存时,从0xF8000000 ~ 0xFFFFFFFF地址空间范围内找一段相应大小空闲的逻辑地址空间,借用一会。借用这段逻辑地址空间,建立映射到想访问的那段物理内存(即填充内核PTE页面表),临时用一会,用完后归还。这样别人也可以借用这段地址空间访问其他物理内存,实现了使用有限的地址空间,访问所有所有物理内存。

例如内核想访问2G开始的一段大小为1MB的物理内存,即物理地址范围为0×80000000 ~ 0x800FFFFF。访问之前先找到一段1MB大小的空闲地址空间,假设找到的空闲地址空间为0xF8700000 ~ 0xF87FFFFF,用这1MB的逻辑地址空间映射到物理地址空间0×80000000 ~ 0x800FFFFF的内存。

四.内核空间与用户空间交互

系统调用

系统调用的可实现进程由用户态切换至内核态。但是通过上面分析已经了解,用户进程与内核进程相互分离的,那么在进行系统调用时参数传递是如何实现的呢?

解决这一办法的思路是:数据拷贝。因为无论是用户地址空间还是内核地址空间均为虚拟地址空间,因此在做参数传递时,只需要将在用户地址空间存放的参数拷贝至内核地址空间即可。完成拷贝的函数就是大家熟知的copy_from_user() ©_to_user().

上图表示某一用户进程将一组数据传递给内核,内核经过处理后将数据返回至用户进程。该过程涉及到数据在两块内存区域的转移:用户虚拟内存地址对应的物理内存与内核虚拟内存地址对应的物理内存。

mmap函数

函数原型:

void *mmap{
	void *addr;	//映射区首地址,传NULL
	size_t length;	//映射区的大小
		//会自动调为4k的整数倍
		//不能为0
		//一般文件多大,length就指定多大
	int prot;	//映射区权限
		//PROT_READ 映射区比必须要有读权限
		//PROT_WRITE
		//PROT_READ | PROT_WRITE
	int flags;	//标志位参数
		//MAP_SHARED 修改了内存数据会同步到磁盘
		//MAP_PRIVATE 修改了内存数据不会同步到磁盘
	int fd;	//要映射的文件对应的fd
	off_t offset;  //映射文件的偏移量,从文件的哪里开始操作
		//映射的时候文件指针的偏移量
		//必须是4k的整数倍
		//一般设置为0

用户空间mmap调用

用户空间读写文件时,需经过内核,数据拷贝多了一次。通过mmap函数,可以建立用户虚拟空间到文件所在物理页的直接映射建立该映射后,可以像直接操作内存一样读写文件(比如读写数组),减少一次用户到内核的数据拷贝。

 fd = open(argv[1], O_RDWR);
    /* 将文件映射至进程的地址空间 ,mmaped 为文件所在物理页对应的虚拟内存地址*/
    mmaped = (char *)mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    /* 映射完后, 关闭文件也可以操纵内存 */
    close(fd);
    mmaped[5] = '$';
    msync(mmaped, sb.st_size, MS_SYNC)

内核空间mmap实现

直接打开一个磁盘文件,实际是调用了文件系统提供的mmap版本。但是对于开发linux驱动的工程师来说,需自己实现对应设备文件的mmap函数,例如framebuffer这种设备需要较高频率大数据读写,因此不能容忍内核空间到用户空间的数据拷贝,因此驱动需开发自己的mmap函数供用户进程调用,驱动mmap实现流大致为:

  1. 通过kmalloc, get_free_pages, vmalloc等分配一段虚拟地址
  2. 如果是使用kmalloc, get_free_pages分配的虚拟地址,那么使用virt_to_phys()将其转化为物理地址,再将得到的物理地址通过”phys>>PAGE_SHIFT”获取其对应的物理页面帧号。或者直接使用virt_to_page从虚拟地址获取得到对应的物理页面帧号。如果是使用vmalloc分配的虚拟地址,那么使用vmalloc_to_pfn获取虚拟地址对应的物理页面的帧号。
  3. 对每个页面调用SetPageReserved()标记为保留才可以。
  4. 通过remap_pfn_range为物理页面的帧号建立页表,并映射到用户空间。
 //内存分配
 buffer = (unsigned char *)kmalloc(PAGE_SIZE,GFP_KERNEL);  
 //将该段内存设置为保留
 SetPageReserved(virt_to_page(buffer));
 //得到物理地址
 phys = virt_to_phys(buffer);
 //将用户空间的一个vma虚拟内存区映射到以page开始的一段连续物理页面上
 remap_pfn_range(vma,
                 vma->vm_start,
                 phys >> PAGE_SHIFT,//第三个参数是页帧号,由物理地址右移PAGE_SHIFT得>到
                 vma->vm_end - vma->vm_start,
                 vma->vm_page_prot)

mmap函数并非实现用户空间与内核空间的交互,而是用户空间试图绕过内核空间,直接操作物理页

四. 完整的linux系统下内存分布图

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/73929.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

shadow阴影属性

shadow阴影属性 盒子阴影CSS中新增了盒子阴影,我们可以使用box-shadow属性为盒子添加阴影 源代码 box-shadow: h-shadow v-shadow blur spread color inset; h-shadow 必需,水平阴影的位置,允许负值 v-shadow…

AI 揭晓答案,2022世界杯冠军已出炉

卡塔尔世界杯,究竟谁能捧起大力神杯,就让我们用机器学习预测一下吧! 文章目录数据源技术提升数据集构建功能开发数据分析模型世界杯模拟结论数据源 为了构建机器学习模型,我们需要来自团队的数据。首先需要一些能够说明球队表现的…

Java学习之多态参数

目录 多态参数 父类-Employee类 子类-Worker类 子类-Manager类 Test类-要求1 main类-PolyParameter 在main类中调用Test类的showEmpAnnual(Employee e) 方法 运行结果 Test类-要求2 代码 main方法内调用 分析 运行结果 多态参数 方法定义的形参类型是父类&#xff0…

青竹画材创业板IPO被终止:年营收4.15亿 文投基金是股东

雷递网 雷建平 12月8日河北青竹画材科技股份有限公司(简称:“青竹画材”)日前IPO被终止。青竹画材曾计划募资4.1亿元,其中,3.08亿元用于美术画材产能扩建项目,2317.35万元用于研发中心项目,7966…

Selenium+python常见问题,闪退,找不到元素

1、闪退问题: 由于缺少浏览器对应驱动。谷歌:chromedriver驱动;火狐:geckodriver驱动 查看Chrome版本 查找Chrome和ChromeDriver的对应关系 打开Chrome,在设置 – 关于Chrome下载对应ChromeDriver ChromeDriver下载…

本地存储:localStorage,sessionStorage,和cookie。区别

localStorage: 特点: 永久存储支持跨页面通讯,也就是在其他页面同样可以获取到你存好的数据。只能存储字符串类型的数据,不能存储复杂数据类型 sessionStorage: 特点: 临时存储,会话级别&am…

68. 关于 SAP ABAP 报表的多语言显示问题

文章目录 SAP 标准程序的多语言显示支持自开发 Screen 的字段,如何实现多语言显示呢?总结最近一个朋友向我发起知乎咨询,询问 SAP ABAP 报表的字段文本,到底是存储在哪些数据库表里的。 随着后来笔者同这位朋友的深入沟通,得知这位朋友询问的问题背后,实际上包含了一个重…

Windows系统还原失败该怎么办?

系统还原是Windows系统中一个强大的实用程序,它允许您将系统回退到以前的时间点及状态。该功能可以用来撤销对系统所做的一系列更改,比如,当您执行了驱动程序的更新后,计算机出现了异常,您就可以通过系统还原的操作将系…

【微服务】1、一篇文章了解 Eureka 注册中心(理论加代码)

Eureka 的作用和案例使用一、Eureka 的作用(注册中心)二、Eureka 代码(1) 搭建 Eureka 注册中心① 创建项目,引入 spring-cloud-starter-netflix-eureka-server 依赖【eureka 服务端依赖】② 编写 eureka 服务端启动类(启动类添加…

WSO2使用Jenkins进行CI/CD

WSO2使用Jenkins进行CI/CD1. Tool Install1.0 Port 1.1 wso2 Install 1.2 Tool Install(git/vim/jenkins)2. Jenkins CI/CD2.1 Apictl Command2.1 Apictl Command5.2.1 Setting up the environment5.2.2 Setup repository5.2.3 Setup JFrog Artifactory5.2.4 Configure Jenkins…

计算机网络复习(四)

4-15.一个3200位长的TCP报文传到IP层,加上160位的首部后成为数据报。下面的互联网由两个局域网通过路由器连接起来。但第二个局域网所能传送的最长数据帧中的数据部分只有1200位。因此数据报在路由器必须进行分片。试问第二个局域网向其上层要传送多少比特的数据&am…

JavaScript进阶教程——原始类型与引用类型、对象拷贝(克隆)

文章目录序原始类型与引用类型基本概念两种类型的区别赋值比较函数传参对象拷贝对象克隆的使用场景:对象拷贝的分类:浅拷贝深拷贝序 解决JavaScript中比较烧脑的问题: 原始类型与引用类型闭包原型对象this关键字bind、apply、call异步编程 …

VMware Workstation 17.0 Pro SLIC Unlocker for Windows

VMware_Dell_2.6_BIOS-EFI64_Mod;macOS Unlocker,支持 macOS Ventura 请访问原文链接:VMware Workstation 17.0 Pro SLIC & Unlocker for Windows & Linux,查看最新版。原创作品,转载请保留出处。 作者主页&…

Win11安装Linux子系统提示错误代码0x800701bc怎么办?

Win11安装Linux子系统提示错误代码0x800701bc怎么办?最近有用户想要在自己的电脑上安装一个Linux子系统来使用,但是在安装的过程中却出现了一些问题,提示错误代码0x800701bc。那么这个情况要怎么去处理,一起来看看解决的操作步骤吧…

GitHub 被超火的 ChatGPT 霸榜!

本期推荐开源项目目录:1. ChatGPT2. 基于 Node.js 的 ChatGPT3. Mac 版的 ChatGPT4. Chrome 插件的 ChatGPT最近 ChatGPT 火爆全球,估计各位的朋友圈都被这东西刷屏了吧。Chat GPT 是 OpenAI 推出的基于 GPT-3 技术的聊天机器人。它能自动理解用户发的消…

小度机器人3D数字勋章盲盒发售!抽盲盒,赢奖金!参与合成得稀有!

人气王小度机器人3D数字勋章盲盒来了!玩法升级,福利更多!参与活动,抽超限量稀有款,赢京东卡福利,更能合成珍藏款勋章!【活动介绍】小度机器人是百度公司推出的智能交互机器人,依托百…

C++语法——详解智能指针的概念、实现原理、缺陷

目录 一.智能指针的概念 (一).智能指针的历史 (二).智能指针的使用 插曲.auto_ptr ①unique_ptr ②shared_ptr ③weak_ptr 二.智能指针的实现 三.智能指针的缺陷 (一).循环引用 (二&…

ChatGPT被玩坏了

大家好,欢迎来到 Crossin的编程教室 ! 体验了一下最近火出圈的 ChatGPT,聊聊使用感受。 ChatGPT让我下岗?原理就不展开说了,因为我也不懂,写出来估计大家也都看不懂,就简单复制一段网上的介绍&…

Unity 如何实现卡片循环滚动效果

文章目录简介定义卡片的摆放规则调整卡片的层级关系调整卡片的尺寸大小动态调整位置、层级和大小移动动画按钮事件简介 功能需求如图所示,点击下一个按钮,所有卡片向右滚动,其中最后一张需要变更为最前面的一张,点击上一个按钮&a…

案例:用户信息列表展示

1. 需求:用户信息的增删改查操作2. 设计:1. 技术选型:ServletJSPMySQLJDBCTempleatDuirdBeanUtilStomcat2. 数据库设计:create database day17; -- 创建数据库use day17; -- 使用数据库create table user( -- 创建表id in…