3.7 用户空间与内核空间
3.7.1 说明
Linux 简化了分段机制,使得虚拟地址与线性地址总是一致,因此,Linux 的虚拟地址空间也为0~4G 字节(32bit)。Linux 内核将这4G 字节的空间分为两部分。将最高的1G 字节(从虚拟地址0xC0000000 到0xFFFFFFFF),供内核使用,称为“内核空间”。而将较低的3G 字节(从虚拟地址0x00000000 到0xBFFFFFFF),供各个进程使用,称为“用户空间”。因为每个进程可以通过系统调用进入内核,因此,Linux 内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G 字节的虚拟空间。
3.7.2 区别
关于内存管理一定要区分两个概念,内核空间内存管理及用户空间内存管理,Linux在这两个空间的内存管理方式非常不同,但也有一些相同点:1. 都有页表;2. 都是通过虚拟地址访问内存。主要的不同点:1. 内核中虚拟地址到物理地址基本是直接映射,分配连续的物理内存(也可以分配非连续的内存,比如vmalloc等,具体后续单独描述),而用户空间会基于进程各自的页表,分配非连续的物理内存;2. 物理内存中有一部分内存只能被内核访问的(具体见内核域划分)。所以内核空间会直接依赖物理内存的管理,而用户空间则需要多一层的虚拟地址空间到物理地址的转换。
linux内核一般将虚拟地址空间划分为会两部分,将底部比较大的部分用于用户进程,顶部专用于内核进程,在IA-32系统上比例为3:1,因此内核访问的地址一般是从0xC0000000开始。内核空间的内存管理相对要简单一些,因为内核空间信任自身,而没法信任用户程序。内核对于物理内存管理主要包括以下几个概念:分配大块内存的伙伴系统;分配非连续内存块的vmalloc机制及内存映射机制;分配小块内存的slab,slub和slob;
内核分配内存使用简单的方法,内核中的函数使用直截了当的方式获得动态内存,原因:
(1)内核是操作系统中优先级最高的成分。如果某个内核函数请求动态内存,那么必须有正当的理由发出那个请求,因此没有道理试图推迟这个请求。
(2)内核信任自己。所有的内核函数都被假定是没有错误的,因此内核函数不必插入针对编程错误的任何保护措施。
给用户态进程分配内存时,情况完全不同:
(1)进程对动态内存的请求被认为是不紧迫的。例如,当进程的可执行文件被装入时,进程并不一定立即对所有的代码进行访问。类似的,当进程调用malloc以获得请求的动态内存时,也并不意味着进程很快就会访问所有获得的内存。因此一般来说,内核总是尽量推迟给用户态进程分配动态内存。
(2)由于用户进程是不可信任的,因此,内核必须能够随时准备捕获用户态进程引起的所有寻址错误。
3.7.3 UMA VS NUMA
有两种类型的计算机,分别以不同的方式管理物理内存:
1)UMA(uniform memory access)计算机,将内存以连续的方式组织起来。SMP系统中每个处理器都是访问同一块内存。
2)NUMA(non-uniform memory access)计算机,总是多处理器计算机,系统各个CPU有各自本地的内存访问,各个处理器之间的总线是连着的,可以访问其他CPU,但是要慢一些。
简而言之,就是 UMA总线逻辑更简单,但是访问速度要慢,带宽低;NUMA总线逻辑更复杂,但是访问更高效,带宽高,也更容易伸缩。UMA更适合分时系统,NUMA更适合实时系统,UMA并行能力特别差。下面我们讲解的主要是NUMA系统,因为弄懂了NUMA系统的内存管理,UMA就特别容易理解了。
以上是硬件层面上的NUMA,而作为软件层面的Linux,则对NUMA的概念进行了抽象。即便硬件上是一整块连续内存的UMA,Linux也可将其划分为若干的node。同样,即便硬件上是物理内存不连续的NUMA,Linux也可将其视作UMA。
所以,在Linux系统中,你可以基于一个UMA的平台测试NUMA上的应用特性。从另一个角度,UMA就是只有一个node的特殊NUMA,所以两者可以统一用NUMA模型表示。
【图片:https://zhuanlan.zhihu.com/p/68465952】
3.7.4 分配和映射
- 映射跟分配是两回事,他们是不相关的。
- 但是如果能做到 分配后才映射,释放后就解映射,从运行安全的角度来说是最好的,可惜这个很难做到。
先看第一个问题:映射跟分配是两回事,他们是不相关的
整个内核,由buddy管理算法来管理所有物理页,只有经过它分配出去的物理页,OS和应用程序才能使用。
映射是指建好页表,即某块虚拟地址空间到物理地址空间的映射,有了这个映射,处理器就可以访问这块虚拟地址空间。
所以,分配是管哪些物理地址可使用,而映射是管哪些虚拟地址可访问
那么问题来了:如果某个物理页没有被分配出来,但已有某个虚拟地址映射到这个物理页上,怎么办?会不会出问题呢?这就是内核lowmem给大家造成的疑惑。
再看第二个问题:但是如果能做到 分配后才映射,释放后就解映射,从运行安全的角度来说是最好的,可惜这个很难做到
我们先看高端内存(即highmem),请大家要注意概念,高端内存是指物理地址空间超过896M的内存地址,跟虚拟空间上的(3G+896M ~ 4G]没有任何关系。
高端内存是满足分配后才映射,释放后就解映射 这个原则的,这个空间主要给用户态进程(比如用户态代码段,数据段,栈,堆,匿名映射)或者用户态业务相关功能使用(访问文件产生的pagecache)。
但是lowmem却不是这样的。总结起来有两个原因:
1) 先有鸡还有先有蛋
虚拟地址到物理地址的映射关系,要写到页表里面,如果这个页表所在的址物理没有映射到虚拟地址,处理器是通过虚拟地址来修改页表的,但在MMU模式下,处理器只能使用虚拟地址来访问任何物理内存。为了解决这个矛盾,需要在进入保护模式(X86架构)/MMU模式(ARM架构)之前,先人为规划出一个固定映射,当然这个映射越简单越好,就是我们常说的线性映射。
2)固定映射应该是多大
从道理上讲,kernel必须的东西:比如代码段,数据段,page结构数组,缺页函数所涉及的访问地址空间,都要在这个预先规划出来的固定映射空间内,才能保证kernel功能的正常运行作。后续内核根据代码逻辑要求,动态申请内存时,建好页表就可以了。但是内核作为整个系统的管理者,它的性能必须高,不能成为系统的瓶颈;用户在运行过程中,内核态必须创建很多管理对象,才能将用户态业务管理好。比如task_struct, mm, vma,file, inode等关键数据结构。内核如果在创建这些对象时,从buddy里面分配内存(当然中间是通过slub来分配的),然后才建立映关系,那么性能必然会比较差。所以内核在运行过程中,动态创建的管理对象所在的内存空间,必须提前映射好。这个区域就是lowmem zone。
kernel运行过程中需要动态分配的管理对象,都是从lowmem zone里面分配的,这个zone的性能比highmem zone优,因为页表已提前建好的。
内核1G的虚拟空间里,使用896M空间做为lowmem空间,即固定映射空间(线性映射区)。内核使用1G的7/8空间大小作为线性区,剩下的1/8作为临时映射区,用于访问高端内存。
最后,回到题主的问题:如果整系统只有128M内存,情况会怎么样?
如果系统只有128M内存,那么系统没有highmem zone,128M都属于lowmem,kernel初始化时,会将这128M内存映射到[3G, 3G + 128M)虚拟空间上。
如果恶意写个KO,可以直接访问这128M内存空间的,因为页表已经建立了,从处理器层面来讲是可访问的。
用户态调用malloc做内存申请时,最终会通过系统调用mmap向kernel申请内存,kernel最终(缺页过程完成后)会为进程分配虚拟内存和物理内存。而这个物理内存就是这128M内存中,还没有分配的物理内存,但这块物理内存早已被映射到[3G, 3G + 128M)空间上了。但如果内核工作正常,由于这块物理内存没有被分配给内核态使用,那内核态就没有指针 指向 访物理内存对应的内核态虚拟地址空间,即内核态是不会访这块物理内存的。
在128M物理内存场景下,用户态进程之间页表是独立的,不可能相互踩,同时用户态是无法踩内核态内存的。由于只有lowmem区,理论上内核态能踩用户态内存,但经上一段分析,概率较低,只有编程错误时才会发生。
对于64位 kernel,没有highmem了,只有lowmem,所有物理内存都映射到lowmem了。与32位系统物理内存小于896M场景是一模一样的。