目录
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的数据结构来实现线性区。进程获得新的线性区或改变线性区的一些典型情况:
- 创建了一个新的用户进程时,会申请一组线性区。如控制台输入命令,shell进程为执行此命令创建新的进程;
相关系统调用为:fork() - 正在运行的进程装入一个不同的程序,会释放旧的线性区,创建新的线性区;
相关系统调用为:execve() - 进程对一个文件执行内存映射,内核为此进程分配线性区映射此文件;
相关系统调用为:mmap()/mmap2() - 进程栈用完,需要扩展映射栈的线性区;
相关系统调用为:mremap() - 进程创建IPC共享线性区用于和其它线程共享数据;
相关系统调用为:shmat()/shmdt() - 用户调用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 Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 00000000004001c8 000001c8
000000000000001b 0000000000000000 A 0 0 1
......
[13] .text PROGBITS 0000000000400430 00000430
0000000000000218 0000000000000000 AX 0 0 8
......
[35] .symtab SYMTAB 0000000000000000 000018e0
0000000000000a98 0000000000000018 36 88 8
[36] .strtab STRTAB 0000000000000000 00002378
0000000000000522 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的标志属性
内存管理关于权限相关的属性标志主要包含如下几种:
- 定义在页表项中标志位,此部分主要由硬件来去判定处理,一般为mmu;
- 用于alloc_pages时所使用的标志,这些标志主要用于指定内存域,分配过程是否允许睡眠,是否有IO操作及VFS操作等,此为纯粹操作系统软件层面的标志,与硬件无关
- 定义在page->flags中的标志,如PG_locked等,此为纯粹操作系统软件层面的标志,与硬件无关;
- 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:指向文件映射的偏移量
参考文档
- How The Kernel Manages Your Memory
- 奔跑吧,Linux内核
- ULA
- ULK