操作系统-内存管理 - 3
2024-02-28 09:19:48 0 举报
AI智能生成
进程虚拟内存空间的管理
作者其他创作
大纲/内容
虚拟内存区域组织
vm_area_struct 结构与组织结构相关的一些属性
分支主题
链表查找虚拟内存区域
双向链表中的最后一个 VMA 节点的 vm_next 指针指向 NULL,双向链表的头指针存储在内存描述符 struct mm_struct 结构中的 mmap 中,正是这个 mmap 串联起了整个虚拟内存空间中的虚拟内存区域
分支主题
分支主题
内核通过一个vm_area_struct结构的双向链表将虚拟内存空间中的虚拟内存区域VMA串联起来的
vm_next ,vm_prev 指针分别指向 VMA 节点所在双向链表中的后继节点和前驱节点
内核中的这个 VMA 双向链表是有顺序的,所有 VMA 节点按照低地址到高地址的增长方向排序
虚拟内存区域VMA通过struct vm_area_struct中的vm_mm指针指向了所属的虚拟内存空间mm_struct
红黑树查找特定的虚拟内存区域
分支主题
分支主题
在进程虚拟内存空间中包含的内存区域 VMA 比较多的情况下,使用红黑树查找特定虚拟内存区域的时间复杂度是 O( logN ) ,可以显著减少查找所需的时间
每个 VMA 区域都是红黑树中的一个节点,通过 struct vm_area_struct 结构中的 vm_rb 将自己连接到红黑树中
虚拟内存区域的相关操作
vm_area_struct 结构中还有一个 vm_ops 用来指向针对虚拟内存区域 VMA 的相关操作的函数指针
分支主题
调用场景
指定的虚拟内存区域被加入到进程虚拟空间中时,open函数会被调用
虚拟内存区域vma从进程虚拟内存空间被删除时,close函数会被调用
进程访问虚拟内存时,访问的页面不在物理内存中,可能是未分配物理内存,也可能是被置换到磁盘中,这时会产生缺页中断,fault函数会被调用
当一个只读的页面将要变为可写时,page_mkwrite函数会被调用
关联内存的映射关系
关键属性
anno_vma
vm_file
vm_pgoff
映射关系
匿名映射
虚拟内存区域映射到物理内存上
文件映射
虚拟内存区域映射到文件中
分支主题
映射过程
调用 malloc 申请内存时,如果申请的是小块内存(低于 128K)则会使用 do_brk() 系统调用通过调整堆中的 brk 指针大小来增加或者回收堆内存
如果申请的是比较大块的内存(超过 128K)时,则会调用 mmap 在上图虚拟内存空间中的文件映射与匿名映射区创建出一块 VMA 内存区域(这里是匿名映射)
这块匿名映射区域就用 struct anon_vma 结构表示
当调用 mmap 进行文件映射时,vm_file 属性就用来关联被映射的文件。这样一来虚拟内存区域就与映射文件关联了起来。vm_pgoff 则表示映射进虚拟内存中的文件内容,在文件中的偏移
定义虚拟内存区域的访问权限和行为规范
vm_page_prot 和 vm_flags 都是用来标记 vm_area_struct 结构表示的这块虚拟内存区域的访问权限和行为规范
vm_page_prot 偏向于定义底层内存管理架构中页这一级别的访问控制权限,它可以直接应用在底层页表中,它是一个具体的概念
vm_flags 则偏向于定于整个虚拟内存区域的访问权限以及行为规范。描述的是虚拟内存区域中的整体信息,而不是虚拟内存区域中具体的某个独立页面。它是一个抽象的概念
可以通过 vma->vm_page_prot = vm_get_page_prot(vma->vm_flags) 实现到具体页面访问权限 vm_page_prot 的转换
vm_flags
分支主题
读写执行权限示例
代码段这块内存区域的权限是可读,可执行,但是不可写
数据段具有可读可写的权限但是不可执行
堆则具有可读可写,可执行的权限(Java 中的字节码存储在堆中,所以需要可执行权限)
栈一般是可读可写的权限,一般很少有可执行权限
而文件映射与匿名映射区存放了共享链接库,所以也需要可执行的权限
VM_SHARD 用于指定这块虚拟内存区域映射的物理内存是否可以在多进程之间共享,以便完成进程间通讯
VM_IO 的设置表示这块虚拟内存区域可以映射至设备 IO 空间中。通常在设备驱动程序执行 mmap 进行 IO 空间映射时才会被设置
VM_RESERVED 的设置表示在内存紧张的时候,这块虚拟内存区域非常重要,不能被换出到磁盘中。
VM_SEQ_READ 的设置用来暗示内核,应用程序对这块虚拟内存区域的读取是会采用顺序读的方式进行,内核会根据实际情况决定预读后续的内存页数,以便加快下次顺序访问速度
VM_RAND_READ 的设置会暗示内核,应用程序会对这块虚拟内存区域进行随机读取,内核则会根据实际情况减少预读的内存页数甚至停止预读
分支主题
核心描述符
进程描述符
task_struct
分支主题
内存描述符
mm_struct
调用 fork() 函数创建进程的时候,表示进程地址空间的 mm_struct 结构会随着进程描述符 task_struct 的创建而创建
分支主题
在 copy_process 函数中创建 task_struct 结构,并拷贝父进程的相关资源到新进程的 task_struct 结构里
分支主题
copy_mm 函数完成了子进程虚拟内存空间 mm_struct 结构的的创建以及初始化
分支主题
copy_mm 函数首先会将父进程的虚拟内存空间 current->mm 赋值给指针 oldmm
然后通过 dup_mm 函数将父进程的虚拟内存空间以及相关页表拷贝到子进程的 mm_struct 结构中
最后将拷贝出来的 mm_struct 赋值给子进程的 task_struct 结构
通过 fork() 函数创建出的子进程,它的虚拟内存空间以及相关页表相当于父进程虚拟内存空间的一份拷贝,直接从父进程中拷贝到子进程中
vfork或者clone调用创建子进程
首先会设置 CLONE_VM 标识,这样来到 copy_mm 函数中就会进入 if (clone_flags & CLONE_VM) 条件中
在这个分支中会将父进程的虚拟内存空间以及相关页表直接赋值给子进程
父进程和子进程的虚拟内存空间就变成共享的了。也就是说父子进程之间使用的虚拟内存空间是一样的,并不是一份拷贝
子进程共享了父进程的虚拟内存空间,这样子进程就变成了我们熟悉的线程,是否共享地址空间几乎是进程和线程之间的本质区别。
Linux 内核并不区别对待它们,线程对于内核来说仅仅是一个共享特定资源的进程而已
内核线程和用户态线程的区别就是内核线程没有相关的内存描述符 mm_struct ,内核线程对应的 task_struct 结构中的 mm 域指向 Null,所以内核线程之间调度是不涉及地址空间切换的
内核线程调度
会发现自己的虚拟地址空间为 Null,虽然它不会访问用户态的内存,但是它会访问内核内存
内核会将调度之前的上一个用户态进程的虚拟内存空间 mm_struct 直接赋值给内核线程
内核线程不会访问用户空间的内存,它仅仅只会访问内核空间的内存
直接复用上一个用户态进程的虚拟地址空间就可以避免为内核线程分配 mm_struct 和相关页表的开销,以及避免内核线程之间调度时地址空间的切换开销
父进程与子进程的区别,进程与线程的区别,以及内核线程与用户态线程的区别其实都是围绕着这个 mm_struct 展开的
内核划分用户态、内核态
mm_struct结构体的task_size变量定义了用户地址空间和内核态地址空间的分解线
分支主题
/arch/x86/include/asm/page_32_types.h 文件中关于 TASK_SIZE 的定义
分支主题
分支主题
/arch/x86/include/asm/page_64_types.h 文件中关于 TASK_SIZE 的定义
分支主题
在 task_size_max() 的计算逻辑中 1 左移 47 位得到的地址是 0x0000800000000000,然后减去一个 PAGE_SIZE (默认为 4K),就是 0x00007FFFFFFFF000,共 128T。
所以在 64 位系统中的 TASK_SIZE 为 0x00007FFFFFFFF000
64 位虚拟内存空间的布局是和物理内存页 page 的大小有关的,物理内存页 page 默认大小 PAGE_SIZE 为 4K
PAGE_SIZE 定义在 /arch/x86/include/asm/page_types.h文件中
分支主题
内核布局进程虚拟内存空间
mm_struct结构体核心变量
分支主题
定义
start_code、end_code
定义代码段的起始和结束位置,程序编译后的二进制文件中的机器码被加载进内存之后就存放在这里
start_data、end_data
定义数据段的起始和结束位置,二进制文件中存放的全局变量和静态变量被加载进内存中放到这里
BSS 段
用于存放未被初始化的全局变量和静态变量,这些变量在加载进内存时会生成一段 0 填充的内存区域 (BSS 段), BSS 段的大小是固定的
OS 堆
在堆中内存地址的增长方向是由低地址向高地址增长, start_brk 定义堆的起始位置,brk 定义堆当前的结束位置
内存映射区
内存映射区内存地址的增长方向是由高地址向低地址增长
mmap_base 定义内存映射区的起始地址
进程运行时所依赖的动态链接库中的代码段,数据段,BSS 段以及我们调用 mmap 映射出来的一段虚拟内存空间就保存在这个区域
栈
start_stack 是栈的起始位置在 RBP 寄存器中存储,栈的结束位置也就是栈顶指针 stack pointer 在 RSP 寄存器中存储
在栈中内存地址的增长方向也是由高地址向低地址增长
arg_start 和 arg_end 是参数列表的位置, env_start 和 env_end 是环境变量的位置。它们都位于栈中的最高地址处
total_vm
表示在进程虚拟内存空间中总共与物理内存映射的页的总数
locked_vm
当内存吃紧的时候,有些页可以换出到硬盘上,而有些页因为比较重要,不能换出
被锁定不能换出的内存页总数
pinned_vm
表示既不能换出,也不能移动的内存页总数
data_vm
表示数据段中映射的内存页数目
exec_vm
代码段中存放可执行文件的内存页数目
stack_vm
栈中所映射的内存页数目
布局
分支主题
内核管理虚拟内存空间
虚拟内存区域在内核中又是如何表示
核心结构体
分支主题
每个 vm_area_struct 结构对应于虚拟内存空间中的唯一虚拟内存区域 VMA
vm_start 指向了这块虚拟内存区域的起始地址(最低地址),vm_start 本身包含在这块虚拟内存区域内
vm_end 指向了这块虚拟内存区域的结束地址(最高地址),而 vm_end 本身包含在这块虚拟内存区域之外
vm_area_struct 结构描述的是 [vm_start,vm_end) 这样一段左闭右开的虚拟内存区域
分支主题
0 条评论
下一页