Linux 内核学习(14) --- linux x86-32 虚拟地址空间

目录

x86-32 地址空间

Linux内核一般将处理器的虚拟地址空间划分为两个部分,底部比较大的部分用于用户进程,顶部则专用于内核,虽然(在两个用户进程之间的)上下文切换期间会改变下半部分,但虚拟地址空间的内核部分总是保持不变

也就是在进程切换的过程中,页表的用户部分会被更新,反映出新的内存布局,内核部分的页表项保持不变 ,因此无需刷新 TLBTranslation Lookaside Buffer)中与内核相关的条目

Linux将虚拟地址空间划分为:0-3G 为用户空间,3-4G 为内核空间

用户地址空间

保留区

位于虚拟地址空间的最低部分,未赋予物理地址,任何对它的引用都是非法的,用于捕捉使用空指针和小整数型指针引用内存的异常情况

它并不是一个单一的内存区域,而是对地址空间中受到操作系统保护而禁止用户进程访问的地址区域的总称

大多数操作系统中,极小的地址通常都是不允许访问的,

NULL,C语言将无效指针赋值为 0,也是因为 0 地址上正常情况下不会存放有效的可访问数据

因为历史原因 前面 128.28125MB 属于保留空间

代码段 .text

代码段也称正文段或文本段,通常用于存放程序执行代码(即 CPU 执行的机器指令)

一般 C 语言执行语句都编译成机器代码保存在代码段,通常代码段是可共享的,因此频繁执行的程序只需要在内存中拥有一份拷贝即可

代码段通常属于只读,以防止其他程序意外地修改其指令(对该段的写操作将导致段错误), 某些架构也允许代码段为可写即允许修改程序,
代码段指令根据程序设计流程依次执行,对于顺序指令,只会执行一次(每个进程);若有反复,则需使用跳转指令;若进行递归,则需要借助栈来实现

代码段指令中包括操作码*和 操作对象**(或对象地址引用),若操作对象是立即数(具体数值),将直接包含在代码中;若是局部数据,将在栈区分配空间,然后引用该数据地址;

若位于 BSS 段和 DATA 数据段,同样引用该数据地址,代码段最容易受优化措施影响

初始化数据段 .data

数据段通常用于存放程序中已初始化且初值不为 0 的全局变量和静态局部变量,数据段属于静态内存分配(静态存储区),可读可写

数据段保存在目标文件中(在嵌入式系统里一般固化在镜像文件中),其内容由程序初始化
DATA 段与 BSS 段的区别如下:

  • BSS 段不占用物理文件尺寸,但占用内存空间;数据段占用物理文件,也占用内存空间。对于大型数组如 int ar0[10000] = {1, 2, 3, ...}int ar1[10000]ar1 放在 BSS 段,只记录共有 10000*4 个字节需要初始化为 0,而不是像 ar0 那样记录每个数据1、2、3...

运行时数据段和 BSS 段的整个区段通常称为数据区,某些资料中 数据段 指代数据段 + BSS段 + 堆

未初始化数据段 .bss

BSS(Block Started by Symbol) 段中通常存放程序中以下符号:

  • 未初始化的全局变量和静态局部变量
  • 初始值为 0 的全局变量和静态局部变量(依赖于编译器实现)
  • 未定义且初值不为 0 的符号(该初值即common block的大小)

C语言中,未显式初始化的静态分配变量被初始化为 0 (算术类型)或空指针(指针类型),

由于程序加载时,BSS 会被操作系统清零,所以未赋初值或初值为0的全局变量都在 BSS 中,
BSS 段仅为未初始化的静态分配变量预留位置,在目标文件中并不占据空间,这样可减少目标文件体积

但程序运行时需为变量分配内存空间,故目标文件必须记录所有未初始化的静态分配变量大小总和 (通过start_bssend_bss地址写入机器代码)

当加载器(loader)加载程序时,将为 BSS 段分配的内存初始化为 0

在嵌入式软件中,进入 main() 函数之前 BSS 段被 C 运行时系统映射到初始化为全零的内存(效率较高)

注意: bss 段通过start_bssend_bss地址和代码段相关联

注意,尽管均放置于 BSS 段,但初值为 0 的全局变量是强符号,而未初始化的全局变量是弱符号,若其他地方已定义同名的强符号(初值可能非0),

则弱符号与之链接时不会引起重定义错误,但运行时的初值可能并非期望值(会被强符号覆盖)

因此,定义全局变量时,若只有本文件使用,则尽量使用 static 关键字修饰;否则需要为全局变量定义赋初值(哪怕 0 值),保证该变量为强符号,

以便链接时发现变量名冲突,而不是被未知值覆盖

某些编译器将未初始化的全局变量保存在 common 段,链接时再将其放入 BSS 段,在编译阶段可通过 -fno-common 选项来禁止将未初始化的全局变量放入 common

此外,由于目标文件不含 BSS 段,故程序烧入存储器(Flash)后 BSS 段地址空间内容未知,

U-Boot 启动过程中,将 U-BootStage2代码(通常位于 lib_xxxx/board.c 文件)搬迁(拷贝)到 SDRAM 空间后必须人为添加清零 BSS 段的代码,

而不可依赖于 Stage2 代码中变量定义时赋 0 值

堆 heap

堆用于存放进程运行时动态分配的内存段,可动态扩张或缩减,堆中内容是匿名的,不能按名字直接访问,只能通过指针间接访问

当进程调用 malloc/new 等函数分配内存时,新分配的内存动态添加到堆上(扩张)

当调用 free/delete 等函数释放内存时,被释放的内存从堆中剔除(缩减)

分配的堆内存是经过字节对齐的空间,以适合原子操作,堆管理器通过链表管理每个申请的内存

由于堆申请和释放是无序的,最终会产生内存碎片,堆内存一般由应用程序分配释放,回收的内存可供重新使用

若程序员不释放,程序结束时操作系统可能会自动回收

堆的末端由 break 指针标识,当堆管理器需要更多内存时,可通过系统调用 br k和 sbrk 来移动 break 指针以扩张堆,一般由系统自动调用

使用堆时经常出现两种问题:

  1. 释放或改写仍在使用的内存(内存破坏)
  2. 未释放不再使用的内存(内存泄漏),当释放次数少于申请次数时,可能已造成内存泄漏
内存映射段(mmap)

此处,内核可以将文件的内容直接映射到内存,任何应用程序都可通过 Linuxmmap()系统调用或 Windows的CreateFileMapping/MapViewOfFile 请求这种映射

内存映射是一种方便高效的文件 I/O 方式, 因而被用于装载动态共享库,用户也可创建匿名内存映射,该映射没有对应的文件,可用于存放程序数据

Linux 中,若通过 malloc 请求一大块内存,C 运行库将创建一个匿名内存映射,而不使用堆内存
大块 意味着比阈值 MMAP_THRESHOLD 还大,缺省为 128KB,可通过 mallopt() 调整

栈 Stack

栈又称堆栈,由编译器自动分配释放,行为类似数据结构中的栈(先进后出),堆栈主要有三个用途

  1. 为函数内部声明的非静态局部变量(C语言中称 自动变量 )提供存储空间

  2. 记录函数调用过程相关的维护性信息,称为栈帧(Stack Frame )或过程活动记录(Procedure Activation Record), 它包括函数返回地址,不适合装入寄存器的函数参数及一些寄存器值的保存

除递归调用外,堆栈并非必需,因为编译时可获知局部变量,参数和返回地址所需空间,并将其分配于 BSS 段(也就是函数返回地址,不适合装入寄存器的函数参数,寄存器的保存值)

临时存储区,用于暂存长算术表达式部分计算结果或 alloca() 函数分配的栈内内存

  1. 持续地重用栈空间有助于使活跃的栈内存保持在 CPU 缓存中,从而加速访问,进程中的每个线程都有属于自己的栈,向栈中不断压入数据时,若超出其容量就会耗尽栈对应的内存区域,从而触发一个页错误
    此时若栈的大小低于堆栈最大值 RLIMIT_STACK(通常是 8M),则栈会动态增长,程序继续运行,映射的栈区扩展到所需大小后,不再收缩

Linux中 ulimit -s 命令可查看和设置堆栈最大值,当程序使用的堆栈超过该值时, 发生栈溢出(Stack Overflow),程序收到一个段错误(Segmentation Fault)

内核地址空间

直接映射区 896M

所谓的直接映射区,就是这一块空间是连续的,和物理内存是非常简单的映射关系,其实就是虚拟内存地址减去 3G,就得到物理内存的位置
__pa(vaddr) 返回与虚拟地址 vaddr 相关的物理地址;
__va(paddr) 则计算出对应于物理地址 paddr 的虚拟地址

c 复制代码
// PAGE_OFFSET => 3G  0x0c0000000
#define __va(x)      ((void *)((unsigned long)(x)+PAGE_OFFSET))
#define __pa(x)    __phys_addr((unsigned long)(x))
#define __phys_addr(x)    __phys_addr_nodebug(x)
#define __phys_addr_nodebug(x)  ((x) - PAGE_OFFSET)

896M 还需要仔细分解,在系统启动的时候,物理内存的前 1M 已经被占用了,从 1M 开始加载内核代码段,然后就是内核的全局变量、BSS 等,也是 ELF 里面涵盖的,

这样内核的代码段,全局变量,BSS 也就会被映射到 3G 后的虚拟地址空间里面。具体的物理内存布局可以查看

c 复制代码
cat /proc/iomem
.....
00100000-bffdbfff : System RAM
  01000000-01a02fff : Kernel code
  01a03000-021241bf : Kernel data
  02573000-02611fff : Kernel bss
  25000000-34ffffff : Crash kernel
.....

在内核运行的过程中,如果碰到系统调用创建进程,内核的进程管理代码会将实例创建在 3G3G+896M 的虚拟空间中,当然也会被放在物理内存里面的前 896M 里面,相应的页表也会被创建

如果涉及内核栈的分配,内核的进程管理的代码会将内核栈创建在 3G3G+896M 的虚拟空间中,

当然也就会被放在物理内存里面的前 896M 里面,相应的页表也会被创建

高端内存(HIGH_MEMORY)

x86-32 下特有的(x64 下没有这个东西),因为内核虚拟空间只有 1G 无法管理全部的内存空间

当内核想访问高于 896MB 物理地址内存时,从 0xF8000000 ~ 0xFFFFFFFF 地址空间范围内找一段相应大小空闲的逻辑地址空间,借用一会借用这段逻辑地址空间,建立映射到想访问的那段物理内存(即填充内核 PTE 页面表),临时用一会,用完后归还

这样别人也可以借用这段地址空间访问其他物理内存,实现了使用有限的地址空间,访问所有所有物理内存

VMALLOC_OFFSET

系统会在 low memoryVMALLOC 区域留 8M,防止访问越界。因此假如理论上 vmalloc size300M,实际可用的也是只有 292M

c 复制代码
include/asm-x86/pgtable_32.h  
#define VMALLOC_OFFSET (8*1024*1024) 

这个缺口可用作针对任何内核故障的保护措施,如果访问越界地址(即无意地访问物理上不存在的内存区),则访问失败并生成一个异常,报告该错误

如果 vmalloc 区域紧接着直接映射,那么访问将成功而不会注意到错误,在稳定运行的情况下,肯定不需要这个额外的保护措施,但它对开发尚未成熟的新内核特性是有用的

VMALLOC

虚拟内存中连续、但物理内存中不连续的内存区,可以在 vmalloc 区域分配,该机制通常用于用户过程,

内核自身会试图尽力避免非连续的物理地址,通常会成功,因为大部分大的内存块都在启动时分配给内核,那时内存的碎片尚不严重,

但在已经运行了很长时间的系统上,在内核需要物理内存时,就可能出现可用空间不连续的情况,此类情况,主要出现在动态加载模块时

c 复制代码
include/asm-x86/pgtable_32.h 
#define VMALLOC_START (((unsigned long) high_memory + \ 
 2*VMALLOC_OFFSET-1) & ~(VMALLOC_OFFSET-1)) 
#ifdef CONFIG_HIGHMEM 
#define VMALLOC_END (PKMAP_BASE-2*PAGE_SIZE) 
#else 
#define VMALLOC_END (FIXADDR_START-2*PAGE_SIZE) 
#endif

vmalloc 区域的起始地址,取决于在直接映射物理内存时,使用了多少虚拟地址空间内存(因此也依赖于上文的 high_memory 变量)

内核还考虑到下述事实,即两个区域之间有至少为 VMALLOC_OFFSET 的一个缺口,而且 vmalloc 区域从可被 VMALLOC_OFFSET 整除的地址开始

VMALLOC_STARTVMALLOC_END 之间称为内核动态映射空间,也即内核想像用户态进程一样 malloc 申请内存,在内核里面可以使用 vmalloc

假设物理内存里面,896M1.5G 之间已经被用户态进程占用了,并且映射关系放在了进程的页表中,内核 vmalloc 的时候,只能从分配物理内存 1.5G 开始,就需要使用这一段的虚拟地址进行映射,映射关系放在专门给内核自己用的页表里面

物理内存

物理内存的前 4KiB 是第一个页帧,一般会忽略,通常保留给 BIOS 使用,接下来的 640KiB 原则是可以用的,但是也不能用于内核加载,

原因是 该区域之后紧邻的区域由系统保留,用于映射各种 ROM(通常是系统 BIOS 和显卡 ROM

因为不能向映射的 ROM 区域写入数据,但是内核一定会装载在一个连续的内存区中,如果要从 4KiB 处作为起始位置装载内核镜像,则要求内核必须要小于 640KiB

在x86架构中内存有三种区域: ZONE_DMAZONE_NORMALZONE_HIGHMEM, 不同类型的区域适合不同需要,

在32位系统中结构中,1G(内核空间)/3G(用户空间) 地址空间划分时,三种类型的区域如下:
ZONE_DMA 内存开始的 16MB
ZONE_NORMAL 16MB ~ 896MB
ZONE_HIGHMEM 896MB ~ 结束

  • ZONE_DMA
    该区域的物理页面专门供 I/O 设备的 DMA 使用,之所以需要单独管理 DMA 的物理页面,是因为 DMA 使用物理地址访问内存,不经过 MMU
    并且需要连续的缓冲区,所以为了能够提供物理上连续的缓冲区,必须从物理地址空间专门划分一段区域用于 DMA

DMA 技术就是我们在主板上放一块独立的芯片。在进行内存和 I/O 设备的数据传输的时候,我们不再通过 CPU 来控制数据传输,而直接通过 DMA 控制器(DMA Controller,简称 DMAC

  • ZONE_NORMAL
    ZONE_NORMAL 的范围是 16M~896M,该区域的物理页面是内核能够直接使用的,属于直接映射区

  • ZONE_HIGHMEM896M~结束)

    是系统中剩下的可用内存,但因为内核的地址空间有限,这部分内存不直接映射到内核

相关推荐
Lam㊣3 小时前
Centos 7 系统docker:更换镜像源
linux·docker·centos
FL16238631294 小时前
win11+WSL+Ubuntu-xrdp+远程桌面闪退+黑屏闪退解决
linux·运维·ubuntu
副露のmagic4 小时前
更弱智的算法学习 day28
学习
石头5304 小时前
Kubernetes监控全栈解决方案:从零搭建Prometheus+Grafana监控体系
linux
ha20428941944 小时前
Linux操作系统学习记录之---TcpSocket
linux·网络·c++·学习
AOwhisky4 小时前
Linux逻辑卷管理:从“固定隔间”到“弹性存储池”的智慧
linux·运维·服务器
凉、介5 小时前
深入 QEMU Guest Agent:虚拟机内外通信的隐形纽带
c语言·笔记·学习·嵌入式·虚拟化
崇山峻岭之间5 小时前
Matlab学习记录31
开发语言·学习·matlab
石像鬼₧魂石5 小时前
22端口(OpenSSH 4.7p1)渗透测试完整复习流程(含实战排错)
大数据·网络·学习·安全·ubuntu