进程虚拟地址

article/2025/10/1 15:46:12

在前面的章节中,我们主要关注的是内核的虚拟地址空间的管理。从本节开始,我们重点关注管理用户空间的方法,其中由于种种原因,这个比内核地址空间管理更复杂。本节主要围绕以下内容:

用户进程的虚拟地址空间是Linux的一个重要抽象,它向每个运行进程提供了同样的系统,每个应用程序都有自身的地址空间,与所有的应用程序分割开,不会干扰到其他进程内存的内容。
在内核的虚拟地址空间中,只有很少的段可用于各个用户空间进程,这些段彼此有一定的距离,内核需要一些数据结构来有效的管理这些分布的段
地址空间中只有极少的一部分与物理页直接关联,不经常使用的部分,仅当必要时与页帧关联
内核无法信任用户进程,所以各个操作系统用户地址空间的操作伴随着各种检查,以确保程序的权限不会超出应有的限制,进而危及到系统的稳定性和安全性
用户空间的内存分配方法
1. 进程虚拟地址空间
理论上,64Bit地址支持访问空间是[0, 0xFFFF FFFF FFFF FFFF],而实际上现有的应用程序都不会用这么大的地址空间,而现在ARM64芯片上也不支持访问这么大的地址空间,现有的架构最大支持访问48bit的地址空间。而对于进程有用户态和内核态,同样进程地址空间包括用户地址空间和内核地址空间,用户态访问用户地址空间。对于各个进程的虚拟地址空间起始于地址0,延伸到TASK_SIZE - 1,其上是内核地址空间。

在ARM32系统上,地址空间范围为4GB,总的地址空间通常按照3:1划分,各个用户空间进程可用的部分是3GB

在ARM64系统上,64位虚拟地址中,并不是所有位都用上,除了高16位用于区分内核空间和用户空间外,有效位的配置可以是:36, 39, 42, 47。这可决定Linux内核中地址空间的大小。比如以采用4KB的页,4级页表,虚拟地址为48位的系统为例(从ARMv8.2架构开始,支持虚拟地址和物理地址的大小最多为52位),其虚拟地址空间的范围为256TB ,按照1:1的比例划分,内核空间和用户空间各占128TB。

img

对于用户程序只能访问整个地址空间的下半部分,不能访问内核部分。同时无论当前哪个用户进程处于活动状态,虚拟地址空间内核部分的内容总是相同的。

1.1 进程地址空间的布局
一个进程通常由加载一个elf文件启动,而elf文件是由若干segments组成的,同样的,进程地址空间也由许多不同属性的segments组成。虚拟地址空间中包含了若干区域,其分布方式特定于体系结构,但所有的方法都有下列共同的特点,如下图所示

在这里插入图片描述
text段:包含了当前运行进程的二进制代码,其起始地址在IA32体系中中通常为0x08048000,在IA64体系中通常为0x0000000000400000

data段:包含程序显式初始化的全局变量和静态变量,即已初始化且初值不为0的全局变量(也包括静态全局变量)和静态局部变量,这些数据是在程序真正运行前就已经确定的数据,所以可以提前加载到内存保存好。

bss段:未初始化的全局变量和静态变量,这些变量的值是在程序真正运行起来并为其赋值后才能确定的,所以程序加载之初,只需要记录它的内存地址和所需大小。出于历史原因,这段空间也称为 BSS 段。

heap段:存储动态分配的内存中的数据,堆用于存储那些生存期与函数调用无关的数据。如用系统调用 malloc 申请的内存便在堆上,这些申请的内存在不需要时必须手动释放,否则便会出现内存泄漏。

stack段:用于保存局部变量和实现函数/过程调用的上下文,它们的大小都是会在进程运行过程中发生变化的,因此中间留有空隙,heap向上增长,stack向下增长,因为不知道heap和stack哪个会用的多一些,这样设置可以最大限度的利用中间的空隙空间。进程每调用一次函数,都将为该函数分配一个栈帧,栈帧中保存了该函数的局部变量、参数值和返回值。

文件映射段:这个段比较特殊,是mmap()系统调用映射出来的。mmap映射的大小也是不确定的。3GB的虚拟地址空间已经很大了,但heap段, stack段,mmap段在动态增长的过程还是有重叠(碰撞)的可能。为了避免重叠发生,通常将mmap映射段的起始地址选在TASK_SIZE/3(也就是1GB)的位置。如果是64位系统,则虚拟地址空间更加巨大,几乎不可能发生重叠。

我们以最简单的Helloworld程序为例,其内存空间布局如下图所示

在这里插入图片描述

1.2 建立布局
那我们了解了进程在运行过程中的内存空间分布情况,那么如何建立起这种内存空间呢?首先,在Linux系统中运行一个可执行的ELF文件时,内核首先需要识别这个文件,然后解析并装载它以构建进程的内存空间,最后切换到新的进程来运行。

首先,我们来看一下elf文件的格式,Section头表包含了描述文件Sections的信息。每个Section在这个表中有一个入口,每个入口给出了该Section的名字,大小等信息。同时可执行文件有一个头部,里面有一些关键信息,Entry point Address,入口地址,即程序的起点,0x8048300,后面有一些代码,数据

在这里插入图片描述

当我们在linux的shell命令中执行某个elf可执行文件的时候,linux系统是如何装载该ELF并执行的呢?其主要是以下几个步骤:

创建新进程:首先在用户层面,shell进程会调用fork()系统调用创建一个新的进程,然后新的进程调用execve()系统调用来执行指定的ELF。

检查可执行文件的类型:当进入execve()系统调用之后,Linux内核就开始真正的装载工作。在内核中,execve()系统调用相应的入口是sys_execve(),会执行do_execve()查找被执行的文件,如果找到文件,则读取文件的前128个字节,通过来判断该执行文件是哪一种elf文件,例如a.out,java程序,以及脚本开头的文件。

搜索匹配的装载处理过程:do_execve()读取128个字节的文件头部后,调用search_binary_handle()去搜索和匹配合适的可执行文件,最常见的可执行文件及处理过程如下

ELF可执行文件:load_elf_binary
a.out 可执行文件:load_aout_library
可执行脚本程序:load_script()
在装载的过程中,对于可执行文件,应该创建对应的.text段、.data段、stack段等。在Linux中,每个段都用一个vm_area_strcutvm_area_strcut结构体表示,vma是通过一个双向链表串起来,现存的vma按照起始地址依次递增被归入链表中,每个vma是这个链表的一个节点,首先我们来看一个进程有一个struct mm_struct用来描述进程的内存信息

struct mm_struct {
    //指向线性区对象的链表头
    struct vm_area_struct * mmap;        /* list of VMAs */
    //指向线性区对象的红黑树
    struct rb_root mm_rb;
    //指向最后一个引用的线性区对象
    struct vm_area_struct * mmap_cache;    /* last find_vma result */
    //在进程地址空间中搜索有效线性地址区间的方法
    unsigned long (*get_unmapped_area) (struct file *filp,
                unsigned long addr, unsigned long len,
                unsigned long pgoff, unsigned long flags);
    //释放线性区时调用的方法
    void (*unmap_area) (struct vm_area_struct *area);
    // 标识第一个分配的匿名线性区或者是文件内存映射的线性地址
    unsigned long mmap_base;        /* base of mmap area */
    //内核从这个地址开始搜索进程地址空间中线性地址的空闲区间
    unsigned long free_area_cache;        /* first hole */
    //指向页表
    pgd_t * pgd;
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
对于mmap指向的vm_area_struct,其定义如下:

struct vm_area_struct {
    unsigned long vm_start;        //虚拟内存空间的首地址
    unsigned long vm_end;        //虚拟内存空间的尾地址
    //VMA链表的下一个成员和上级成员,进程VMA连接成一个链表
    struct vm_area_struct *vm_next, *vm_prev;
    //将本VMA作为一个节点加入到红黑树中,每个进程的struct mm_struct都有一颗这样的红黑树mm_rb
    struct rb_node vm_rb;
    unsigned long rb_subtree_gap;
    //指向该VMA所属的进程struct mm_struct数据结构
    struct mm_struct *vm_mm;    /* The address space we belong to. */
    pgprot_t vm_page_prot;        /* Access permissions of this VMA. */
    unsigned long vm_flags;        /* Flags, see mm.h. */
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
vm_area_struct结构中包含区域起始和终止地址以及其他相关信息,同时也包含一个vm_ops指针,其内部可引出所有针对这个区域可以使用的系统调用函数。这样,进程对某一虚拟内存区域的任何操作需要用要的信息,都可以从vm_area_struct中获得。mmap函数就是要创建一个新的vm_area_struct结构,并将其与文件的物理磁盘地址相连。

至此,我们可以看出,虚拟内存即为由一个个vm_area_struct结构体,通过链表组装起来的空间,其示意图如下图所示

在这里插入图片描述
用户进程拥有用户空间的地址,其可以通过malloc和mmap等函数来申请内存,malloc和mmap等函数的实现都是基于进程线性区描述struct vm_erea_struct,内核管理进程地址空间的数据结构struct vm_erea_struct,简称VMA。

对于每个进程的内存描述符mm_struct,都有各自的VMA,通过mm->mmap链表将所有的VMA管理起来,同时会记录到mm->mm_rb的红黑树,用于高速查找合并VMA等操作。

2. 虚拟内存区域的表示
先来说说task_struct,task_struct是一个结构体,这个结构体非常的庞大,linux下用它来完整的描述一个进程的所有信息。在每装载一个进程的时候,内核就会帮我们去创建一个新的task_struct结构体。然后我们知道一个每一个独立的进程都有自己独立的虚拟空间,所以,在task_struct结构体里会有一个struct mm_struct *mm成员,这个mm成员就是用来描述和管理进程的虚拟空间的。由上图可知,每个区域通过一个vm_eara_struct实例描述,进程的各区域按照以下两种方式排序

在一个双链表上(开始于mm_struct->mmap)

在一个红黑树上,跟节点位于mm_rb

总结来说,简单的理解这三者的关系就是task_struct结构体包含了一个mm_sturcut结构体成员,mm_struct结构体包含了一个vm_area_struct结构体成员mmap,然后这个mmap成员指向一个VMA链表,管理所有的VMA。

用户虚拟地址空间中的每个区域由开始和结束地址描述,现存的区域按照起始地址以递增次序被归入链表中。扫描链表找到与特定地址关联的区域,在有大量区域时是非常低效的操作。因此vm_eara_struct的各个实例可以通过红黑树管理,可以显著加快扫描速度。

在这里插入图片描述

3. 总结
当一个进程要运行起来需要以下的内存结构:

用户态:

代码段、全局变量、BSS
函数栈

内存映射区
内核态:

内核的代码、全局变量、BSS
内核数据结构例如 task_struct
内核栈
内核中动态分配的内存
对于64位的系统,其进程运行状态如下图所示:

Linux 为虚拟内存不同的段,提供了不同的数据结构来描述:

在 Linux 内核眼中所有的进程、线程都是 task 都适用 task_struck 描述。

task_struck 数据结构中的 mm 字段(mm_struct 类型)描述了进程或者线程用户态的内存信息;

mm_struct 中有映射页的统计信息(总页数, 锁定页数, 数据/代码/栈映射页数等)以及各区域地址

mm_struct 维护着 vm_area_struct 的链表,每个链表节点都描述了用户空间虚拟内存的布局划分:

在这里插入图片描述

4. 参考文档
 


http://chatgpt.dhexx.cn/article/BT1exkmA.shtml

相关文章

【此后无良辰】 实验3 进程的创建

实验目的 了解操作系统中内核程序和应用程序之间的关系;了解操作系统创建进程的过程;掌握在应用程序中创建进程的方法。 实验内容与记录 3.1 复制SDK文件夹的目的是什么?尝试在复制后把本地所生成的EOS内核项目文件夹删除,应用…

Linux进程管理(二)进程调度

Linux进程管理 Linux进程管理(一)进程数据结构 Linux进程管理(二)进程调度 Linux进程管理(三)进程调度之主动调度 Linux进程管理(四)进程调度之抢占式调度 Linux进程管理&#…

c语言静态结构体指针变量,C语言 结构体和指针详解及简单示例

指针也可以指向一个结构体,定义的形式一般为: struct 结构体名 *变量名; 下面是一个定义结构体指针的实例: struct stu{ char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在小组 float score; //成绩 } stu1 { "T…

多进程图像

多进程图像 1 多进程设计2 一个大概的设计思路3 一个实际的进程切换案例3.1 进程的创建 - fork函数3.2 进程的切换 - schedule函数3.3 进程状态转换图3.4 如何执行我们自己的代码 参考资料 所谓多进程图像就是:多道程序,交替执行。本章主要介绍操作系统为…

操作系统之进程创建与进程状态

一、进程的创建 阻塞状态:正在运行的进程由于某些原因调用阻塞原语把自己阻塞(如果不把自己阻塞的话会一直占用处理机),等待相应的事件出现后才被唤醒,事件完成回到就绪状态。 通常这种处于阻塞状态的进程也排成一个队列。有的系统则根据阻塞…

linux进程管理原理

Linux 是一种动态系统,能够适应不断变化的计算需求。 linux 计算需求的表现是以进程的通用抽象为中心的。进程可以是短期的(从命令行执行的一个命令),也可以是长期的(一种网络服务)。因此,对进程…

进程系统调用——fork函数深入理解

转载 进程系统调用——fork函数深入理解 当我们在一个现代系统上运行一个程序的时候,我们会得到一个假象,就好像我们的程序是系统中当前运行的唯一程序。我们的程序好像是独占的使用处理器和存储器。处理器就是无间断的一条一条地执行我们程序中的指令。…

进程调度实验

一、实验目的 通过编写程序实现进程或作业先来先服务、高优先权、按时间片轮转调度算法,进一步掌握进程调度的概念和算法,加深对处理机分配的理解。了解进程(线程)的调度机制。学习使用进程(线程)调度算法…

基于C++实现的进程调度算法

资源下载地址:https://download.csdn.net/download/sheziqiong/85650672 一、问题描述与分析 1.1 设计构想 程序能够完成以下操作:选择调度算法;查看历史记录;创建进程;进程调度:进程创建完成后就选择进程调度算法,每次执行的结果都在屏幕上输出。 1…

短进程优先调度算法c语言spf,短进程优先的调度算法详解

短进程优先的调度算法详解 发布时间:2020-05-17 04:52:01 来源:51CTO 阅读:293 作者:张立达 一、SPF算法简介 SJF算法SJF(shortest job first)是以进程的运行时间长度作为优先级,进程运行时间越短,优先级越高。 SJF算法的缺点必须预知进程的运行时间。即使是程序员也很难…

进程调度算法(c语言)

对一个非抢占式多道批处理系统采用以下算法的任意两种,实现进程调度,并计算进程的开始执行时间,周转时间,带权周转时间,平均周转时间,平均带权周转时间 1.先来先服务算法 2.短进程优先算法 *3.高响应比优先算法 一、设计思想 每个进程有一个进程控制块(…

进程的创建——fork函数

1. 进程的信息 进程的结构 在Linux中,一切皆文件,进程也是保存在内存中的一个实例,下图描述了进程的结构: 堆栈:保存局部变量数据段:一般存放全局变量和静态变量代码段:存储进程的代码文件TSS状态段:进程做切换时,需要保存进程现场…

C语言结构体

本节主要讲解下结构体的一些易错点和重要内容 结构体变量定义 (使用typedef起别名) 一般的结构体定义:定义类型变量 struct student {long stuID;char stuName[10];char stuSex;char birthYear;int mathScore; }stu1;可以用typedef取别…

深入探索 Linux 进程信号的奥秘

Linux 进程信号 0 查看IPC(进程间通信)资源的指令1 学习进程信号的过程2 Linux 进程信号的基本概念2.1 对信号的基本认知 3 Linux 进程信号的产生方式4 Linux 进程信号的保存和处理5 Linux 进程信号递达6 volatile关键字 0 查看IPC(进程间通信)资源的指令 ipcs -m : 查看共享内…

Linux 进程信号深剖

目录 传统艺能😎概念🤔信号发送🤔信号记录🤔信号产生🤔常见信号处理方式🤔终端按键产生信号🤔核心转储😋如何调试🤔 系统函数发送信号🤔raise函数&#x1f91…

Linux进程信号

文章目录 一.信号入门二. 产生信号(1). 通过键盘按键产生信号(2). 硬件异常产生信号(3).通过系统函数发送信号(4). 由软件条件产生信号 三.阻塞信号(1). 阻塞/递达/未决概念 :(2). 信号在内核中的表示(3). sigset_t(4). 信号集操作函数(5). 处理信号 四. 可重入函数/不可重入函…

[培训-DSP快速入门-7]:C54x DSP开发环境与第一个汇编语言程序

作者主页(文火冰糖的硅基工坊):https://blog.csdn.net/HiWangWenBing 本文网址:https://blog.csdn.net/HiWangWenBing/article/details/119011489 目录 引言: 第1章 DSP汇编语言编程的流程概述 第2章 汇编语言程序建立过程 2.1 建立工程…

[培训-DSP快速入门-6]:C54x DSP开发中C语言库函数的使用

作者主页(文火冰糖的硅基工坊):https://blog.csdn.net/HiWangWenBing 本文网址:https://blog.csdn.net/HiWangWenBing/article/details/119010855 目录 第1章 DSP库函数概述 第2章 运行时支持库 2.1 如何加入运行时支持库 2.2 为什么需要运行时的库…

【DSP开发】帮您快速入门 TI 的 Codec Engine

德州仪器半导体技术(上海)有限公司 通用DSP 技术应用工程师 崔晶 德州仪器(TI)的第一颗达芬奇(DaVinci)芯片(处理器)DM6446已经问世快三年了。继DM644x之后,TI又陆续推出…

DSP开发,使用CCS软件建立工程以及烧录

DSP开发,使用CCS软件建立工程以及烧录 1 概述1.1 资源概述1.2 DSP介绍 2 工程建立步骤4 烧录到flash中4.1 通过增减文件实现4.2 增加预编译宏 5 独立下载工具5.1 Uniflash5.2 C2prog 6 程序源码6.1main()函数6.2 leds.c文件6.3 leds.h文件 1 概述 实验的代码已经上…