目录

  • 1. 前言
  • 2. 进程地址空间
  • 3. 用户空间与内核空间的隔离
  • 4. 进程地址空间主要数据结构
    • struct mm_struct
    • struct vm_area_struct
  • 5. vma的标志属性
  • 6. vma主要API
    • 查找vma
    • 插入vma
    • 删除vma
    • 合并vma
  • 参考文档

1. 前言

本专题我们开始学习内存管理部分,本文为进程地址空间的学习笔记。本文主要参考了《奔跑吧, Linux内核》、ULA、ULK的相关内容。

通过前几节的内容可以看到,内核中的函数以非常直接的方式动态获取内存,如alloc_pages/__get_free_pages直接从buddy分配,kmem_cache_alloc/kmalloc直接从slab分配,而vmalloc可以获得一块物理地址非连续的内存区,也就是内核函数分配内存是会被立即响应的,不存在延迟分配,原因如下:
1.内核是操作系统优先级最高的成分,默认内核有正当的理由请求内存分配;如分配内存管理相关的结构体vm_struct, vm_area_struct, kmem_cache等都是需要立即分配;
2.内核信任自己,所有内核函数假定没有错误

而对用户空间的进程,则有可能会被延迟分配,原因如下:
1.认为进程请求动态分配内存并不紧迫。如进程调用malloc函数,并不意味着进程将访问所有内存;
2.用户进程是不可信任的,如它可以请求访问没有权限的地址

因此,对于用户空间的内存分配采用了一种新的方法实现对进程动态内存的延迟分配,当用户态进程动态请求内存时,并没有真正分配物理页框,而是仅仅获得一段新的虚拟地址空间(ULK中称为线性区)的使用权,通过VMA(vm_area_struct)来管理这一段新的虚拟地址空间。

kernel版本:5.10
平台:arm64

2. 进程地址空间

进程地址空间由允许进程使用的全部线性地址组成。每个进程看到的线性地址集合是不同的。内核通过增加或删除某些线性地址区间来动态修改进程的地址空间。由于内核需要区分用户空间无效线性地址(编程错误)和有效线性地址(如用户进程访问malloc分配的空间引发的缺页),因此需要确定一个进程当前所拥有的线性区。

在ULK中,把组成进程地址空间的每一段线性区间称为线性区,通过vm_area_struct的数据结构来实现线性区。进程获得新的线性区或改变线性区的一些典型情况:

  1. 创建了一个新的用户进程时,会申请一组线性区。如控制台输入命令,shell进程为执行此命令创建新的进程;
    相关系统调用为:fork()
  2. 正在运行的进程装入一个不同的程序,会释放旧的线性区,创建新的线性区;
    相关系统调用为:execve()
  3. 进程对一个文件执行内存映射,内核为此进程分配线性区映射此文件;
    相关系统调用为:mmap()/mmap2()
  4. 进程栈用完,需要扩展映射栈的线性区;
    相关系统调用为:mremap()
  5. 进程创建IPC共享线性区用于和其它线程共享数据;
    相关系统调用为:shmat()/shmdt()
  6. 用户调用malloc,内核可能会扩展映射堆的线性区;
    相关系统调用为:brk()

对于一个elf可执行文件来讲,exec系统调用会调用load_elf_binary来创建进程的地址空间,如下为一个用户进程的进程地址空间被创建后的实例:

如上可以看到进程地址空间被划分为多个线性区,包括stack, mmap映射区,heap, bss, data段, text段,每个线性区对应一个vm_area_struct的结构进行管理,vm_area_struct设定了线性区的访问权限以及起始地址。

注:在text段线性区之前往往有一段偏移区间,主要用于捕获NULL指针,不同的架构区间大小不同,如对于aarch64则为0x400430 ,如下:

ubuntu@VM-0-9-ubuntu:~/test$ aarch64-linux-gnu-readelf -S a.out
There are 37 section headers, starting at offset 0x28a0:Section Headers:[Nr] Name              Type             Address           OffsetSize              EntSize          Flags  Link  Info  Align[ 0]                   NULL             0000000000000000  000000000000000000000000  0000000000000000           0     0     0[ 1] .interp           PROGBITS         00000000004001c8  000001c8000000000000001b  0000000000000000   A       0     0     1
......[13] .text             PROGBITS         0000000000400430  000004300000000000000218  0000000000000000  AX       0     0     8
......[35] .symtab           SYMTAB           0000000000000000  000018e00000000000000a98  0000000000000018          36    88     8[36] .strtab           STRTAB           0000000000000000  000023780000000000000522  0000000000000000           0     0     1
Key to Flags:W (write), A (alloc), X (execute), M (merge), S (strings)I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)O (extra OS processing required) o (OS specific), p (processor specific)

3. 用户空间与内核空间的隔离

用户进程不能直接访问内核空间,也不能随意访问其它用户进程的用户空间。对于aarch64来讲,这主要通过页表基址寄存器来实现:

Documentation/arm64/memory.rst:
User addresses have bits 63:48 set to 0 while the kernel addresses have the same bits set to 1. TTBRx selection is given by bit 63 of the virtual address. The swapper_pg_dir contains only kernel (global) mappings while the user pgd contains only user (non-global) mappings.The swapper_pg_dir address is written to TTBR1 and never written to TTBR0.

当处于内核态则采用TTBR1中保存的PGD页表基址,当处于用户态则采用TTBR0中保存的PGD页表基址,如何确认内核态还是用户态?主要通过虚拟地址的bit63来判断,当bit63为1则为内核态,否则为用户态。
同时所有进程共享同样的内核空间,因此所有进程看到的内核空间页表是一致的,也就是有唯一的pgd页表地址,它最初来源于init进程的init_mm内存描述符,之后所有的进程将init_mm->pgd复制到自己的内存描述符;而不同进程的用户空间是不一样的,每一个用户进程的PGD页表基址是不同的,因此实现了不同进程用户空间的地址隔离。

4. 进程地址空间主要数据结构

struct mm_struct

描述了与进程地址空间有关的全部信息,所有的进程描述符存放在一个双向链表

  • 主要成员变量
    mmap: 进程所有的vma(线性区)链接成一个链表,此为链表头
    mm_rb: vma红黑树的根节点
    mmap_base: mmap线性区的起始地址
    pgd:指向进程的PGD页表
    mm_users:记录正在使用该进城地址空间的进程数目,如两个线程共享地址空间
    mm_count: mm_struct结构体的主引用计数
    mm_list: mm_struct结构体连入全局链表的链接件
    start_code,end_code: 代码段的起始地址和结束地址;
    start_data,end_data: 数据段的起始地址和结束地址;
    start_stack:栈起始地址
    start_brk: 堆空间的起始地址
    brk: 表示当前堆vma的结束地址
    total_vm: 已使用的进程地址空间总和

struct vm_area_struct

vma用于描述一段用户空间的线性区间线性区不同重叠,通过两种方法存储:
1.通过链表,进程所有线性区以内存地址升序链接在一起,链表头位于mm_struct->mmap
2.通过红黑树存放

  • 主要成员变量
    (1)vm_start:包含区间的第一个线性地址;
    (2)vm_end:包含区间之外的第一个线性地址
    (3)vm_rb:用于加入vm_struct红黑树的节点;
    (4)vm_next:用于链接下一个vm_area_struct
    (5)vm_prev:用于链接上一个vm_area_struct
    (6)vm_mm:向线性区所在的内存描述符
    (7)vm_pgoff:对文件页,它是映射文件偏移量,内存映射文件的第一个映射单元的位置,以页大小为单位;
    对匿名页,它等于0或vm_start/PAGE_SIZE
    (8)vm_file:向所映射文件的文件对象

5. vma的标志属性

内存管理关于权限相关的属性标志主要包含如下几种:

  1. 定义在页表项中标志位,此部分主要由硬件来去判定处理,一般为mmu;
  2. 用于alloc_pages时所使用的标志,这些标志主要用于指定内存域,分配过程是否允许睡眠,是否有IO操作及VFS操作等,此为纯粹操作系统软件层面的标志,与硬件无关
  3. 定义在page->flags中的标志,如PG_locked等,此为纯粹操作系统软件层面的标志,与硬件无关;
  4. vm_area_struct->vm_flags为vma线性区层面的属性,属于软件层面,它将最终转换处理器页表项层面的标志,通过vm_get_page_prot可以完成这个转换,转换之后的页表项层面的标志一般保存到vm_area_struct->vm_page_prot

对于vma的权限属性与页表项的转换如下图:

6. vma主要API

查找vma

/* Look up the first VMA which satisfies  addr < vm_end,  NULL if none. */
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)

查询满足addr小于vm_end的第一个vma,如下图addr1和addr2执行find_vma后都将返回VMA2

/* Look up the first VMA which intersects the interval start_addr..end_addr-1,
NULL if none.  Assume start_addr < end_addr. */
static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)

查询满足与[start_addr,end_addr]区间有重叠的第一个vma, 此区间可能与vma部分重叠,或位于vma内部,或包含vma的整个区间

struct vm_area_struct *find_vma_prev(struct mm_struct *mm, unsigned long addr,struct vm_area_struct **pprev)

同find_vma,与find_vma的区别是此处返回的是find_vma查找到的vma的前驱(vma->vm_prev)

unsigned long
get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,unsigned long pgoff, unsigned long flags)

查进程的线性地址空间以找到一个可以使用的的线性地址空间,如果查找成功,返回这个新区间的起始地址
@len:查找区间的长度
@addr:必须从哪个地址开始查找
@return:如果查找成功,返回这个新区间的起始地址

static int find_vma_links(struct mm_struct *mm, unsigned long addr,unsigned long end, struct vm_area_struct **pprev,struct rb_node ***rb_link, struct rb_node **rb_parent)

查找起始地址为addr,结束地址为end的vma的前驱vma,保存到pprev

插入vma

int insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vma)

向内存描述符mm的VMA链表和红黑树插入一个新的VMA

static void vma_link(struct mm_struct *mm, struct vm_area_struct *vma,struct vm_area_struct *prev, struct rb_node **rb_link,struct rb_node *rb_parent)

将一个vma插入到内存描述符的vma链表和vma rb-tree

删除vma

static __always_inline void __vma_unlink(struct mm_struct *mm,struct vm_area_struct *vma,struct vm_area_struct *ignore)

将vma从内存描述符的mmap链表和mm_rb红黑树删除

合并vma

struct vm_area_struct *vma_merge(struct mm_struct *mm,struct vm_area_struct *prev, unsigned long addr,unsigned long end, unsigned long vm_flags,struct anon_vma *anon_vma, struct file *file,pgoff_t pgoff, struct mempolicy *policy)

查找新插入的vma是否可与现有的vma合并,如果可以则执行合并
@prev: 新的vma的前驱节点
@addr,end:新的vma的起始地址和结束地址
@vm_flags:新vma的标志位
@anon_vma: 匿名映射的anon_vma数据结构
@file: 如果新vma属于一个文件映射,则file指向该文件的file数据结构
@proff:指向文件映射的偏移量

参考文档

  1. How The Kernel Manages Your Memory
  2. 奔跑吧,Linux内核
  3. ULA
  4. ULK

内存管理基础学习笔记 - 3.1 进程地址空间 - VMA线性区相关推荐

  1. 内存管理基础学习笔记 - 3.2 进程地址空间 - brk系统调用

    目录 1. 前言 2. SYSCALL_DEFINE1(brk, unsigned long, brk) |- -__do_munmap |- -do_brk_flags |- - -get_unma ...

  2. 内存管理基础学习笔记 - 5.2 页面回收 - kswapd

    目录 1. 前言 2. kswapd_init 3.kswapd |- -balance_pgdat |- - -pgdat_balanced |- - -kswapd_shrink_node |- ...

  3. 内存管理基础学习笔记 - 2. 内核地址空间 - SLAB

    目录 1. 前言 2. slab总体说明 3. kmem_cache_create |- -__kmem_cache_create |- - -setup_cpu_cache 4. kmem_cach ...

  4. 《深入理解LINUX内存管理》学习笔记(一)

    引子 为什么要写这个笔记: 1,这本书的中文版翻译了太垃圾,没法阅读.阅读英文原版,可以很好的理解作者的思路.作此笔记备忘 2,一直以来学习LINUX kernel的知识缺乏系统化,借对这本书的学习, ...

  5. Arm V8内存管理架构.学习笔记

    目录 第1章 分级存储架构 1.1基础认识 1.1.1 从数据通路描述 1.1.2 从数据交换单位描述 1.1.3 Cache数据一致性拓扑结构 1.2 系统层内存模型 1.2.1 内存属性 1.2. ...

  6. RT-Thread 静态内存管理(学习笔记)

    本文参考自[野火EmbedFire]<RT-Thread内核实现与应用开发实战--基于STM32>,仅作为个人学习笔记.更详细的内容和步骤请查看原文(可到野火资料下载中心下载) 文章目录 ...

  7. RT-Thread 动态内存管理(学习笔记)

    本文参考自[野火EmbedFire]<RT-Thread内核实现与应用开发实战--基于STM32>,仅作为个人学习笔记.更详细的内容和步骤请查看原文(可到野火资料下载中心下载) 文章目录 ...

  8. arm64的ioremap_ARMv8 内存管理架构.学习笔记

    第1章分级存储架构 1.1基础认识 通常为了保证计算机的整体性能,内存和CPU之间的通信需保证很高的传输速率,然而这受限制于内存的大小和昂贵的硬件实现,传输速率和内存容量大小的关系遵循"Sm ...

  9. 8.Python基础学习笔记day8-正则表达式、网络编程、进程与线程

    8.Python基础学习笔记day8-正则表达式.网络编程.进程与线程 一.正则表达式 ''' 1. [1,2,3,4]中任意取3个元素排列: A43 = 4x3x2 = 24itertools.pe ...

最新文章

  1. 隐藏式抽屉SlidingDrawer(无法实现垂直)
  2. codeforces 922E
  3. (九)python3 只需3小时带你轻松入门——函数自定义
  4. jvm(6)-Class字节码文件结构总结
  5. leetcode —— 19. 删除链表的倒数第N个节点
  6. 吴恩达老师经验:80%的数据+20%的模型=更好的机器学习
  7. Convert.ToString和ToString的区别
  8. 在线ASCII流程图编辑器工具
  9. 13.1Question Answering 问答系统意境级讲解
  10. 计算机网络物理层之数字传输系统
  11. 浅谈声纹识别应用:声音被模仿,声音识别身份可靠吗?
  12. 毕业论文参考文献格式设置(以GB/T 7714-2015为例)
  13. 两个月学习一个月备考托福101分攻略
  14. http请求中简单的签名验证
  15. 【C语言】操作符详解
  16. eclipse中访问受限api
  17. 如何查看本机Mac地址
  18. ActiveMQ 在shareplex8中的使用
  19. Stream流的常用方法以及代码练习
  20. 解决stack overflow栈溢出问题!

热门文章

  1. (006)网络编程,反射及其应用,MySQL数据库
  2. javaweb——Response下载文件
  3. Java中,集合与数组之间的相互转换
  4. 如何查询Python包的所有历史版本
  5. Centos 升级docker 至最新版本或指定版本
  6. Android开发 菜单制作
  7. linux修改Ip的shell脚本
  8. 海思Hi3518ev300视频监控摄像开发板防雷防静电推荐图
  9. C语言奇妙之旅_if大家庭
  10. Java8特性总结(一)概述