1. mmap/malloc之后,首次使用会有较高的latency
使用过mmap映射文件的同学会发现一个问题,search程序访问对应的内存映射时,处理query的时间会有latecny会陡升,究其原因是因为mmap只是建立了一个逻辑地址,linux的内存分配测试都是采用延迟分配的形式,也就是只有你真正去访问时采用分配物理内存页,并与逻辑地址建立映射,这也就是我们常说的缺页中断。
缺页中断分为两类,一种是内存缺页中断,这种的代表是malloc,利用malloc分配的内存只有在程序访问到得时候,内存才会分配;另外就是硬盘缺页中断,这种中断的代表就是mmap,利用mmap映射后的只是逻辑地址,当我们的程序访问时,内核会将硬盘中的文件内容读进物理内存页中,这里我们就会明白为什么mmap之后,访问内存中的数据延时会陡增。
出现问题解决问题,上述情况出现的原因本质上是mmap映射文件之后,实际并没有加载到内存中,要解决这个文件,需要我们进行索引的预加载,这里就会引出本文讲到的另一函数madvise,这个函数会传入一个地址指针,已经一个区间长度,madvise会向内核提供一个针对于于地址区间的I/O的建议,内核可能会采纳这个建议,会做一些预读的操作。例如MADV_SEQUENTIAL这个就表明顺序预读。
如果感觉这样还不给力,可以采用read操作,从mmap文件的首地址开始到最终位置,顺序的读取一遍,这样可以完全保证mmap后的数据全部load到内存中。
2.
brk - 小块内存
mmap - 大块内存(128k)
do_brk()
SYSCALL_DEFINE1(brk, unsigned long, brk)
{
origbrk = mm->brk;//记录原始的brk位置
//这里应该是处理使用brk释放的时候,如果brk位置小于了min值,就什么也不做
if (brk < min_brk)
goto out;
//做rlimit限制检查
if (check_data_rlimit(rlimit(RLIMIT_DATA), brk, mm->start_brk,
mm->end_data, mm->start_data))
goto out;
//记录新brk的值和旧值,做页边界对齐
newbrk = PAGE_ALIGN(brk);
oldbrk = PAGE_ALIGN(mm->brk);
//在页边界对齐之后,如果相同,就没有什么要做的(因为当前已经是这样了)
if (oldbrk == newbrk) {
mm->brk = brk;
goto success;
}
//申请brk位置小于当前mm的brk位置,这是要做缩减操作
if (brk <= mm->brk) {
int ret;
//使用新的brk位置更新mm的记录值
mm->brk = brk;
//将缩减的部分(由newbrk开始,长度为oldbrk-newbrk的空间释放unmap掉)
ret = __do_munmap(mm, newbrk, oldbrk-newbrk, &uf, true);
if (ret < 0) {
//如果没能正确umap,要把mm的记录值恢复,这次缩减失败
mm->brk = origbrk;
goto out;
} else if (ret == 1) {
downgraded = true;
}
goto success;
}
//这里是继续做brk扩充的步骤
next = find_vma(mm, oldbrk);
if (next && newbrk + PAGE_SIZE > vm_start_gap(next))
goto out;
//真正做brk扩充,把新的空间建立vma加入mm
//如果能做vm_merge(将旧的vma扩展包括了新的地址范围),vma是(起始地址,长度)的对表示
//如果不能merge, 就新键vma,将新vma加入mm的vma-link
if (do_brk_flags(oldbrk, newbrk-oldbrk, 0, &uf) < 0)
goto out;
//更新mm的记录值
mm->brk = brk;
success:
//如果是brk扩展 而且使用 mlockall(flags)设置了VM_LOCKED
populate = newbrk > oldbrk && (mm->def_flags & VM_LOCKED) != 0;
if (downgraded)
//如果是在做缩减操作,释放mm的read lock
mmap_read_unlock(mm);
else
//如果是在做扩展或者缩减失败,释放mm的write lock
mmap_write_unlock(mm);
userfaultfd_unmap_complete(mm, &uf);
if (populate)
//如果设定了VM_LOCKED的扩张,就做内存填充,提前处理缺页,分配物理内存
mm_populate(oldbrk, newbrk - oldbrk);
return brk;
out:
retval = origbrk;
mmap_write_unlock(mm);
return retval;
}
SYSCALL_DEFINE1(mlockall, int, flags)
\__ apply_mlockall_flags(flags)
SYSCALL_DEFINE0(munlockall)
\__ apply_mlockall_flags(0)
\__ apply_mlockall_flags()
\__ current->mm->def_flags |= VM_LOCKED
//如果没定义了MMU,mm_populate()是空函数
//如果设定了VM_LOCKED,为新的空间填充物理页面
mm_populate(unsigned long addr, unsigned long len)
\__ __mm_populate(addr, len, 1)
\__ __mm_populate(unsigned long start, unsigned long len, int ignore_errors)
\__ populate_vma_page_range(vma, nstart, nend, &locked);
\__ __get_user_pages()
\__ faultin_page() //如果哪没有分配page,进入缺页处理流程
\__ handle_mm_fault() //这里就和do_page_faul合流了
3. sys_mmap
通常来说mmap()只会分配一段vma空间,并且记录vma和file的关系,并不会马上进行实质性的映射。分配物理内存page、拷贝文件内容、创建mmu映射的这些后续动作在缺页异常中进行
SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,
unsigned long, prot, unsigned long, flags,
unsigned long, fd, unsigned long, pgoff)
{
return ksys_mmap_pgoff(addr, len, prot, flags, fd, pgoff);
}
ksys_mmap_pgoff()
\__ vm_mmap_pgoff(file, addr, len, prot, flags, pgoff)
\\__ do_mmap_pgoff()
| \__do_mmap()
| \__ mmap_region()
| |_ vma=vm_area_alloc()
| |_ vma_link()
\__ mm_populate()
unsigned long vm_mmap_pgoff(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flag, unsigned long pgoff)
{
unsigned long ret;
struct mm_struct *mm = current->mm;
unsigned long populate;
LIST_HEAD(uf);
ret = security_mmap_file(file, prot, flag);
if (!ret) {
if (mmap_write_lock_killable(mm))
return -EINTR;
ret = do_mmap_pgoff(file, addr, len, prot, flag, pgoff,
&populate, &uf);
mmap_write_unlock(mm);
userfaultfd_unmap_complete(mm, &uf);
if (populate)
mm_populate(ret, populate);
}
return ret;
}