目录

x86 内存寻址

x86 分页机制

虚拟内存地址管理

虚拟地址与物理地址映射 - do_page_fault()

推荐阅读


Linux的内存管理分为 虚拟内存管理 和 物理内存管理,本文主要介绍 虚拟内存管理 的原理和实现。在介绍 虚拟内存管理 前,首先介绍一下 x86 CPU 内存寻址的具体过程。

x86 内存寻址


Intel x86 CPU 把内存地址分为3种:逻辑地址线性地址 和 物理地址

  • 逻辑地址: 由 段寄存器:偏移量 组成(段寄存器 为16位,偏移量 为32位),偏移量 是应用程序能够直接操作的地址,比如在C语言中使用 & 操作符取得的变量地址就是 逻辑地址
  • 线性地址:也称为 虚拟地址,是通过 CPU 的分段单元把 段寄存器:偏移量 转换成一个32位的无符号整数,范围从 0x00000000 ~ 0xFFFFFFFFF。分段机制的原理是,段寄存器指向一个段描述符,段描述符里面包含了段的基地址(开始地址),然后通过基地址加上偏移量就是线性地址。
  • 物理地址:内存中的每个字节都由一个32位的整数编号表示,而这个整数编号就是内存的 物理地址。比如在安装了4GB内存条的计算机中,能够寻址的物理地址范围为 0x00000000 ~ 0xFFFFFFFFF。在开启了分页机制的情况下,线性地址要经过分页单元转换才能得到 物理地址

下图展示了 逻辑地址线性地址 和 物理地址 三者的关系:

x86 分页机制


前面介绍过,应用程序中的逻辑地址需要通过分段机制和分页机制转换后才能得到真正的物理地址。由于Linux把代码段和数据段的基地址都设置为0,所以逻辑地址中的偏移量就等价于线性地址。所以这里就不介绍分段机制了,有兴趣可以查阅相关的文章或者书籍。

由于Linux主要使用分页机制,所以下面重点介绍一下分页机制的原理。

由于CPU只能对 物理地址 进行寻址,所以 线性地址 需要映射到 物理地址 才能使用,而映射是按 页(Page) 作为单位进行的,一个页的大小为4KB,所以32位的线性地址可以划分为 2^32 / 2^12 = 2^20 (1048576) 个页。

映射是通过 页表 作为媒介的。页表是一个类型为整型的数组,数组的每个元素保存了线性地址页对应的物理地址页的起始地址。由于32位的线性地址可以划分为 2^20 个页,而每个线性地址页需要一个整型来映射到物理地址页,所以页表的大小为 4 * 2^20 (4MB)。由于并不是所有的线性地址都会映射到物理地址,所以为了节省空间,引入了一个二级管理模式的机器来组织分页单元。如下图:

从上图可以看出,线性地址被划分为3部分:面目录索引(10位)页表索引(10位) 和 偏移量(12位)。而 cr3寄存器 保存了 页目录 的物理地址,这样就可以通过 cr3寄存器 来找到 页目录页目录项 指向页表地址,页表项 指向映射的物理内存页地址,而 偏移量 指定了在物理内存页的偏移量。

虚拟内存地址管理


应用程序使用 malloc() 函数向Linux内核申请内存时,Linux内核会返回可用的虚拟内存地址给应用程序。我们可以通过以下程序来验证:

#include <stdio.h>
#include <stdlib.h>int main()
{void *ptr;ptr = malloc(1024);printf("%p\n", ptr);return 0;
}

运行程序后输出:

# 0x7fffeefe6260

内核返回的是虚拟内存地址,但虚拟内存地址映射到物理内存地址不会在申请内存时进行,只有在应用程序读写申请的内存时才会进行映射。

每个进程都可以使用4GB的虚拟内存地址,所以Linux内核需要为每个进程管理这4GB的虚拟内存地址。例如记录哪些虚拟内存地址是空闲的可以分配的,哪些虚拟内存地址已经被占用了。而Linux内核使用 vm_area_struct 结构进行管理,定义如下:

struct vm_area_struct {struct mm_struct * vm_mm;   /* VM area parameters */unsigned long vm_start;unsigned long vm_end;/* linked list of VM areas per task, sorted by address */struct vm_area_struct *vm_next;pgprot_t vm_page_prot;unsigned long vm_flags;/* AVL tree of VM areas per task, sorted by address */short vm_avl_height;struct vm_area_struct * vm_avl_left;struct vm_area_struct * vm_avl_right;struct vm_area_struct *vm_next_share;struct vm_area_struct **vm_pprev_share;struct vm_operations_struct * vm_ops;unsigned long vm_pgoff;struct file * vm_file;...
};

vm_area_struct 结构各个字段作用:

  • vm_mm:指向进程内存空间管理对象。
  • vm_start:内存区的开始地址。
  • vm_end:内存区的结束地址。
  • vm_next:用于连接进程的所有内存区。
  • vm_page_prot:指定内存区的访问权限。
  • vm_flags:内存区的一些标志。
  • vm_file:指向映射的文件对象。
  • vm_ops:内存区的一些操作函数。

vm_area_struct 结构通过以下方式对虚拟内存地址进行管理,如下图:

从上图可以看出,通过 vm_area_struct 结构体可以把虚拟内存地址划分为多个用途不相同的内存区,比如可以划分为数据区、代码区、堆区和栈区等等。每个进程描述符(内核用于管理进程的结构)都有一个类型为 mm_struct 结构的字段,这个结构的 mmap 字段保存了已经被使用的虚拟内存地址。

当应用程序通过 malloc() 函数向内核申请内存时,会触发系统调用 sys_brk(),(当然,根据申请的内存大小,也可能是sys_mmap)sys_brk() 实现如下:

asmlinkage unsigned long sys_brk(unsigned long brk)
{unsigned long rlim, retval;unsigned long newbrk, oldbrk;struct mm_struct *mm = current->mm;down(&mm->mmap_sem);if (brk < mm->end_code)goto out;newbrk = PAGE_ALIGN(brk);oldbrk = PAGE_ALIGN(mm->brk);if (oldbrk == newbrk)goto set_brk;if (brk <= mm->brk) { // 缩小堆空间if (!do_munmap(mm, newbrk, oldbrk-newbrk))goto set_brk;goto out;}// 检测是否超过限制rlim = current->rlim[RLIMIT_DATA].rlim_cur;if (rlim < RLIM_INFINITY && brk - mm->start_data > rlim)goto out;// 如果已经存在, 那么直接返回if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))goto out;// 是否有足够的内存页?if (!vm_enough_memory((newbrk-oldbrk) >> PAGE_SHIFT))goto out;// 所有判断都成功, 现在调用do_brk()进行扩展堆空间if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)goto out;
set_brk:mm->brk = brk;
out:retval = mm->brk;up(&mm->mmap_sem);return retval;
}

sys_brk() 系统调用的 brk 参数指定了堆区的新指针,sys_brk() 首先会进行一些检测,然后调用 do_brk() 函数进行虚拟内存地址的申请,do_brk() 函数实现如下:

unsigned long do_brk(unsigned long addr, unsigned long len)
{struct mm_struct * mm = current->mm;struct vm_area_struct * vma;unsigned long flags, retval;...// 如果新申请的内存空间与之前的内存空间相连并且特性一样, 那么就合并内存空间if (addr) {struct vm_area_struct * vma = find_vma(mm, addr-1);if (vma && vma->vm_end == addr &&!vma->vm_file &&vma->vm_flags == flags){vma->vm_end = addr + len;goto out;}}// 新申请一个 vm_area_struct 结构vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);if (!vma)return -ENOMEM;vma->vm_mm = mm;vma->vm_start = addr;vma->vm_end = addr + len;vma->vm_flags = flags;vma->vm_page_prot = protection_map[flags & 0x0f];vma->vm_ops = NULL;vma->vm_pgoff = 0;vma->vm_file = NULL;vma->vm_private_data = NULL;insert_vm_struct(mm, vma); // 添加到虚拟内存管理器中out:...return addr;
}

do_brk() 函数主要通过调用 kmem_cache_alloc() 申请一个 vm_area_struct 结构,然后对这个结构的各个字段进行初始。最后通过调用 insert_vm_struct() 函数把这个结构添加到进程的虚拟内存地址链表中。

为了加速查找虚拟内存区,Linux内核还为 vm_area_struct 结构构建了一个 AVL树(新版本为红黑树),有兴趣的可以查阅源码或相关资料。

虚拟地址与物理地址映射 - do_page_fault()


前面说过,虚拟地址必须要与物理地址进行映射才能使用,如果访问了没有被映射的虚拟地址,CPU会触发内存访问异常,并且调用异常处理例程对没被映射的虚拟地址进行映射操作。Linux的内存访问异常处理例程是 do_page_fault(),代码如下:

asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{...// 获取发生错误的虚拟地址__asm__("movl %%cr2,%0":"=r" (address));...down(&mm->mmap_sem);// 查找第一个结束地址比address大的vmavma = find_vma(mm, address);if (!vma)goto bad_area;...// 这里是进行物理内存映射的地方switch (handle_mm_fault(mm, vma, address, write)) {case 1:tsk->min_flt++;break;case 2:tsk->maj_flt++;break;case 0:goto do_sigbus;default:goto out_of_memory;}...bad_area:up(&mm->mmap_sem);bad_area_nosemaphore:// 用户空间触发的虚拟内存地址越界访问, 发送SIGSEGV信息(段错误)if (error_code & 4) {tsk->thread.cr2 = address;tsk->thread.error_code = error_code;tsk->thread.trap_no = 14;info.si_signo = SIGSEGV;info.si_errno = 0;info.si_addr = (void *)address;force_sig_info(SIGSEGV, &info, tsk);return;}
}

当异常发生时,CPU会把触发异常的虚拟内存地址保存到 cr2寄存器中,do_page_fault() 函数首先通过读取 cr2寄存器 获取到触发异常的虚拟内存地址,然后调用 find_vma() 函数获取虚拟内存地址对应的 vm_area_struct 结构,如果找不到说明这个虚拟内存地址是不合法的(没有进行申请),所以内核会发送 SIGSEGV 信号(传说中的段错误)给进程。如果虚拟地址是合法的,那么就调用 handle_mm_fault() 函数对虚拟地址进行映射。


推荐阅读


《Linux虚拟内存管理 | 虚拟地址与物理地址映射、段错误SIGSEGV》

《linux内核之slob、slab、slub》

《Linux内核:kmalloc()和SLOB、SLAB、SLUB内存分配器》

《Linux内存管理:内存分配:slab分配器》

《Linux内存管理:内存描述之内存节点node》

《Linux内存管理:内存描述之内存区域zone》

《Linux内存管理:内存描述之内存页面page》

《Linux内存管理:内存描述之高端内存》

《Linux内存管理:分页机制》

《内存管理:Linux Memory Management:MMU、段、分页、PAE、Cache、TLB》

《Linux内存管理:ARM64体系结构与编程之cache(1)》

《Linux内存管理:ARM64体系结构与编程之cache(2)》

《ARM SMMU原理与IOMMU技术(“VT-d” DMA、I/O虚拟化、内存虚拟化)》

《内核引导参数IOMMU与INTEL_IOMMU有何不同?》

《提升KVM异构虚拟机启动效率:透传(pass-through)、DMA映射(VFIO、PCI、IOMMU)、virtio-balloon、异步DMA映射、预处理》

Linux虚拟内存管理 | 虚拟地址与物理地址映射、段错误SIGSEGV相关推荐

  1. 深入理解Linux虚拟内存管理(一)

    系列文章目录 Linux 内核设计与实现 深入理解 Linux 内核(一) 深入理解 Linux 内核(二) Linux 设备驱动程序(一) Linux 设备驱动程序(二) Linux 设备驱动程序( ...

  2. [转]十问 Linux 虚拟内存管理 (glibc)

    最近在做 MySQL 版本升级时( 5.1->5.5 ) , 发现了 mysqld 疑似"内存泄露"现象,但通过 valgrind 等工具检测后,并没发现类似的问题.因此,需 ...

  3. 深入理解Linux虚拟内存管理(二)

    系列文章目录 Linux 内核设计与实现 深入理解 Linux 内核(一) 深入理解 Linux 内核(二) Linux 设备驱动程序(一) Linux 设备驱动程序(二) Linux 设备驱动程序( ...

  4. Linux虚拟内存管理

    Linux的虚拟内存管理有几个关键概念: 每个进程有独立的虚拟地址空间,进程访问的虚拟地址空间并不是真正的物理地址 虚拟地址可通过每个进程上页表与物理地址进行映射,获得真正的物理地址 如果虚拟地址所对 ...

  5. 十问 Linux 虚拟内存管理 (glibc)

      最近在做 MySQL 版本升级时( 5.1->5.5 ) , 发现了 mysqld 疑似"内存泄露"现象,但通过 valgrind 等工具检测后,并没发现类似的问题.因此 ...

  6. 十问 Linux 虚拟内存管理 (glibc) (一二 )

    转自:https://zhuanlan.zhihu.com/p/26855207 最近在做 MySQL 版本升级时( 5.1->5.5 ) , 发现了 mysqld 疑似"内存泄露&q ...

  7. 【Linux】十问 Linux 虚拟内存管理

    Linux 的虚拟内存管理有几个关键概念: 每个进程有独立的虚拟地址空间,进程访问的虚拟地址并不是真正的物理地址 虚拟地址可通过每个进程上页表与物理地址进行映射,获得真正物理地址 如果虚拟地址对应物理 ...

  8. linux虚拟内存 ppt,Linux虚拟内存管理基础v2研究报告.ppt

    Linux虚拟内存管理基础v2研究报告.ppt Linux虚拟内存管理基础篇 4 用户地址空间与内核地址空间 Linux将每个进程的4GB的独立地址空间又划分为用户地址空间(0~ 0xBFFFFFFF ...

  9. 深入理解Linux虚拟内存管理(六)

    系列文章目录 Linux 内核设计与实现 深入理解 Linux 内核 Linux 设备驱动程序 Linux设备驱动开发详解 深入理解Linux虚拟内存管理(一) 深入理解Linux虚拟内存管理(二) ...

最新文章

  1. Java 强、弱、软、虚,你属于哪一种?
  2. 微信小程序设置启动图时出现滚动条
  3. 十天冲刺---Day8
  4. 计算机二级网址打不开,大神为你解决win7系统打不开二级网页链接的操作教程...
  5. z3 C++学习笔记
  6. mysql查询去年本月的数据_MySQL查询本周、上周、本月、上个月份数据的sql代码...
  7. 在 TMG 更新中心中使用 WSUS进行每日的定义更新
  8. 限时福利:入群锁定大会直播+PPT,听百位 AI 技术大咖、20 大热门主题分享!...
  9. 《Effective Python》笔记
  10. 层次狄利克雷过程HDP(Hierarchical Dirichlet Processes)
  11. Base16加密算法
  12. 阿里编码规范认证考试题库(免费版)
  13. 数论_埃氏筛法(求区间内多少素数)
  14. java 短连接转长连接_HTTP的长连接和短连接转换接口(API)
  15. 爬虫初学——爬取京东商品的评论(二)
  16. 微信开放平台PC端扫码登录功能个人总结
  17. 已知两点坐标如何快速增加其他坐标_天文坐标系分类
  18. IT架构——降低成本和复杂性
  19. 服务器查看文件口令,查看服务器序列号命令
  20. 理财就是理生活 —— 小白理财训练营(上)

热门文章

  1. idea中报文件Not Found
  2. ES6中的扩展运算符
  3. 循环往数组中添加对象
  4. [luoguP3068] [USACO13JAN]派对邀请函Party Invitations(stl大乱交)
  5. AC日记——红色的幻想乡 洛谷 P3801
  6. 翻译文章第六章4-7
  7. linux 下vim文件乱码 cat文件正常处理方法
  8. [BZOJ1998][Hnoi2010]Fsk物品调度
  9. android demo 调用,Android中调用C++函数的一个简单Demo
  10. react 父子传值_React父子组件传值