mmap
malloc的两种情况
再用malloc分配内存的时候有两种情况:
一种是小于128k的时候调用brk系统调用
brk是将数据段(.data)的最高地址指针_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系),如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R13hFd11-1611567939104)(https://i.loli.net/2021/01/09/uXOyBVv4WKrb9Pf.jpg)]
看图(2)_edata+30K只是完成虚拟地址的分配,A这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的。 图(3)是调用B=malloc(40k)的情况。
一种是大于128k的时候调用mmap系统调用
此时使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0),如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CHI9YNAd-1611567939105)(https://i.loli.net/2021/01/09/FBzHC8csLt7ruRZ.jpg)]
进程调用C=malloc(200K)以后,内存空间如图(4):
默认情况下,malloc函数分配内存,如果请求内存大于128K是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存。
这样子做主要是因为:
brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,这就是内存碎片产生的原因,当最高地址空间的空闲内存超过128k时,执行内存紧缩操作),而mmap分配的内存可以单独释放。
进程调用D=malloc(100K)以后,内存空间如图(5);进程调用free©以后,C对应的虚拟内存和物理内存一起释放。如图(6)。
mmap基础概念和映射原理
基础概念
mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。
进程的虚拟地址空间,由多个虚拟内存区域构成。虚拟内存区域是进程的虚拟地址空间中的一个具有同样特性的连续地址范围。
linux中vm_area_struct结构来表示一个独立的虚拟内存区域,由于每个不同质的虚拟内存区域功能和内部机制都不同,因此一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。各个vm_area_struct结构使用链表或者树形结构链接,方便进程快速访问。
struct task_struct {
...
struct mm_struct *mm
...
};
struct mm_struct {
...
struct vm_area_struct *mmap; /* list of VMAs */
...
}
struct vm_area_struct {
...
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
unsigned long vm_flags; /* Flags, see mm.h. */
const struct vm_operations_struct *vm_ops;
...
};
vm_area_struct结构中包含区域起始和终止地址以及其他相关信息,同时也包含一个vm_ops指针,其内部可引出所有针对这个区域可以使用的系统调用函数。这样,进程对某一虚拟内存区域的任何操作需要用要的信息,都可以从vm_area_struct中获得。
映射原理
映射关系有4种组合
1、私有文件映射
多个进程使用同样的物理内存页进行初始化,但是各个进程对内存文件的修改不会共享,也不会反应到物理文件中
2、私有匿名映射
mmap会创建一个新的映射,各个进程不共享,这种使用主要用于分配内存(malloc分配大内存会调用mmap)。
例如开辟新进程时,会为每个进程分配虚拟的地址空间,这些虚拟地址映射的物理内存空间各个进程间读的时候共享,写的时候会copy-on-write。
3、共享文件映射
多个进程通过虚拟内存技术共享同样的物理内存空间,对内存文件 的修改会反应到实际物理文件中,他也是进程间通信(IPC)的一种机制。
4、共享匿名映射
这种机制在进行fork的时候不会采用写时复制,父子进程完全共享同样的物理内存页,这也就实现了父子进程通信(IPC).
mmap只是在虚拟内存分配了地址空间,只有在第一次访问虚拟内存的时候才分配物理内存。
在mmap之后,并没有在将文件内容加载到物理页上,只上在虚拟内存中分配了地址空间。当进程在访问这段地址时,通过查找页表,发现虚拟内存对应的页没有在物理内存中缓存,则产生"缺页",由内核的缺页异常处理程序处理,将文件对应内容,以页为单位(4096字节)加载到物理内存,注意是只加载缺页,但也会受操作系统一些调度策略影响,加载的比所需的多。
相关代码
先看调用关系:
SyS_mmap() {
SyS_mmap_pgoff() {
vm_mmap_pgoff() {
do_mmap() {
get_unmapped_area();
mmap_region();
}
}
}
}
下面是内核的实现关键
do_mmap
这个函数是分配一个新的线性地址空间,如果file不为空,则进行内存映射,如果file为空,则进行匿名映射
static inline unsigned long do_mmap(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flag, unsigned long offset)
{
unsigned long ret = -EINVAL;
if ((offset + PAGE_ALIGN(len)) < offset)
//页对齐len,检测传入参数是否有误。
goto out;
if (!(offset & ~PAGE_MASK))
//检测offset是否页对齐。映射时只能映射页对齐的长度。
ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);
out:
return ret;
}
它的核心函数是do_mmap_pgoff,这里主要关注下do_mmap的参数情况:
file: 如果新的线性区将要把一个文件映射到内存,则要用文件描述符file和文件偏移offset,如不需要,则file和offset不考虑都为空;
addr: 指定从哪里开始查找空闲区间,一般都是NULL即由内核指定;
len: 要求的线性地址空间长度;
prot: 指定线性区下的页的访问权限;
flag: 指定线性区的其他标志;
它会调用do_mmap_pgoff
do_mmap_pgoff
首先做入参检测,把len 按照页面大小对齐。 在用户地址空间中寻找个合适的地址 通过 get_unmapped_area完成。判断是文件映射还是匿名映射,最后调用mmap_region完成vma区插入。
unsigned long do_mmap_pgoff(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flags, unsigned long pgoff)
{
/*当前进程的mm*/
struct mm_struct * mm = current->mm;
//为当前进程创建mm_struct内存描述符
struct inode *inode;
//索引节点
unsigned int vm_flags;
int error;
unsigned long reqprot = prot;
/*
* Does the application expect PROT_READ to imply PROT_EXEC?
*
* (the exception is when the underlying filesystem is noexec
* mounted, in which case we dont add PROT_EXEC.)
*/<