对于使用c语言的同学来说,malloc函数是很经典的函数,使用起来也很简单,可是内存实现并不简单。malloc函数其实是为用户空间分配进程地址空间,用内核术语来说就是分配一块VMA,相当于一个空的纸箱子。那什么时候才往纸箱子里装东西呢?有两种方式,一种是到了真正使用箱子的时候才往里面装东西,另一种是分配箱子的时候就装了你想要的东西。进程A里面的testA函数就是第一种情况,当使用这段内存时,CPU去查询页表,发现页表为空,CPU触发缺页中断,然后在缺页中断里一页一页地分配内存,需要一页给一页。进程B里面的testB函数,是第二种情况,直接分配已经装满的纸箱子,你要的虚拟内存都已经分配了物理内存并建立了页表映射。
假设不考虑libc库的因素,malloc分配100Byte,那么内存会分配多少Byte呢?处理器MMU硬件丹玉处理最小单元是页,所以内核分配内存、建立虚拟地址和物理地址映射关系都是以页为单位,PAGE_ALIGN(addr)宏让地址addr按页面大小对齐。
使用printf打印两个进程的malloc分配的虚拟地址一样的,那么内存中这两个虚拟地址空间会打架吗?其实每个用户进程有自己的一份页表,mm_struct数据结构中有一个pgd成员指向整个页表的基地址,在fork新进程的是偶会初始化一份页表。每个进程有一个mm_struct数据结构,包含一个属于进程自己的页表、一个管理VMA的红黑树和使用malloc分配内存返回的相同的虚拟地址,但其实它们是两个不同的VMA,分配被不同的两套页表来管理。
下图是malloc函数的实现流程,malloc的实现还涉及内存管理的几个重要函数。
1. get_user_pages()函数
用于把用户空间的虚拟内存空间传到内核空间,内核空间为其分配物理内存并建立相应的映射关系,实现过程如下图:
long get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
unsigned long start, unsigned long nr_pages, int write,
int force, struct page **pages, struct vm_area_struct **vmas)
2. follow_page()函数
通过虚拟地址addr寻找相应的物理页面,返回normal mapping页面对应的struct page数据结构,该函数会查询页表。
static inline struct page *follow_page(struct vm_area_struct *vma,
unsigned long address, unsigned int foll_flags)
3. vm_normal_page()函数
该函数由pte返回normal mapping的struct page数据结构,主要目的是过滤掉那些令人讨厌的special mapping的页面。
struct page *vm_normal_page(struct vm_area_struct *vma, unsigned long addr,
pte_t pte);