作者:214812031_88fe08 | 来源:互联网 | 2023-07-23 18:32
Linux Kernel Development Edition 3——Memory Management
1. 基本概念 x86 32位CPU架构每个进程都具有4GB的虚拟空间,其中0~3GB属于用户空间,不同进程拥有不同的用户空间,进程私有,进程切换时用户空间也跟着切换。3GB ~ 4GB属于内核空间,由内核负责映射,保持固定不变,所有进程空间共享。因此,每个进程享受4GB的虚拟空间。用户进程最多访问0~3GB物理内存,内核进程可以访问所有的物理内存。
逻辑地址/虚拟地址—(分段)—线性地址—(分页)—物理地址
不开启分页的话,线性地址就是物理地址,只存在一个逻辑地址向物理地址的转换
Linux采用分页模式,也可以说采用分段模式,不过Linux的段都是0~4GB,所以逻辑地址和线性地址一样。
在虚拟空间中内核占据高地址内存,但是在真实的内存空间中,内存从0x0开始存储 。对于内核空间来说,逻辑地址到线性地址(物理地址)只是简单的线性映射,即逻辑地址是x,线性地址就是x-PAGE_OFFSET(0XC000000 3GB)就是宏_pa(x)。用户空间的映射就复杂很多了。
内核中的代码和数据称为内存映像(kernel image)。系统初始化时,保存在0x000100000(1MB)处。在程序运行时,链接程序会将内核地址换成逻辑地址,也即是使用_val(x)加上PAGE_OFFSET。
2. 高端内存
为什么需要高端内存呢?内核空间只有1GB,全部映射到物理内存的话只有1GB(0x0~0x40000000),无法访问全部物理内存,这不行。因此内核空间设置了一个高端内存。不是全部映射,如果内核想要访问大于896MB时,就从HIGH_MEM即0xF8000000 ~ 0xFFFFFFFF地址空间范围内找一段相应大小空闲的逻辑地址空间,借用一会。借用这段逻辑地址空间,建立映射到想访问的那段物理内存(即填充内核PTE页面表),临时用一会,用完释放。
核心:借一段地址,映射到物理内存,用完释放。
x86内核可以访问的物理空间如下图所示,896MB以下由虚拟空间的逻辑地址直接映射,896MB到4GB结束的空间由HIGHMEM的128MB高端内存映射访问。
看到虚拟空间的代码一般从0x08048000开始,bomb实验深有体会。
3. 页、区、 内存管理单元MMU的基本单位是页。page结构体描述的是物理页而非逻辑页,描述的是内存页的信息而不是页中数据。它的描述是短暂的,仅仅记录当前的使用状况,当然也不会描述其中的数据 。struct page包含页的状态,比如是不是脏页、引用次数、映射的虚拟地址是什么void* virtual。
区:有些页是有特定用途的,把这些合在一起就构成了区。
DMA:直接存储器访问,不通过CPU,直接硬件和内存进行。
ZONE_DMA该区域的物理页面专门供I/O设备的DMA使用。之所以需要单独管理DMA的物理页面,是因为DMA使用物理地址访问内存 ,不经过MMU,并且需要连续的缓冲区,所以为了能够提供物理上连续的缓冲区,必须从物理地址空间专门划分一段区域用于DMA。ZONE_DMA区中的页用来进行DMA(直接内存访问)时使用。
ZONE_HIGHMEM是高端内存,其中的页不能永久的映射到内核地址空间 ,也就是说,没有虚拟地址。
剩余的内存就属于ZONE_NORMAL区,叫低端内存。
不是所有体系都定义全部区,有些体系结构,比如x86-64可以映射和处理64位的内存空间 ,所以它没有ZONE_HIGHMEM区 ,所有的物理内存都都处于ZONE_DMA和ZONE_NORMAL区。
部分结构体如下:
4. 获取页 4.1 alloc_pages() 分配2 o r d e r 2^{order} 2 o r d e r 个连续的页,返回第一个物理页的指针,gfp_mask是掩码,一些位决定如何分配页的。 将物理页转换成起始的页地址。 和alloc_pages()一样,不过直接返回虚拟地址,相当于alloc_pages+page_address
只需要一个页的分配函数,都是在上述基础上调用alloc_page() 得到一个填充为0的页 一些释放页的函数。释放页的时候一定要小心谨慎,内核中操作不同于在用户态,若是将地址写错,或是order写错,那么都可能会导致系统的崩溃。若是在用户态进行非法操作,内核作为管理者还会阻止并发出警告,而内核是完全信赖自己的,若是在内核态中有非法操作,那么内核可能会挂掉的。
如何使用上述几个函数
4.2 kmalloc()与vmalloc() 上述4.1以页为单位进行分配,kmalloc和vmalloc以字节为单位进行分配。
不同之处在于,kmalloc分配的是虚拟地址连续,物理地址也连续的一片区域,vmalloc分配的是虚拟地址连续,物理地址不一定连续的一片区域。物理地址不连续,就比较麻烦,需要将逻辑地址一个个的添加页表与物理地址相对应。
这里依然需要特别注意的就是使用释放内存的函数kfree与vfree时一定要注意准确释放,否则会发生不可预测的严重后果。
5. slab层 上述内存分配与管理是以页为单位的,但很多情况使用的更多的数据结构都不足一页,如进程描述符、文件描述符,一页中可以聚集很多个数据结构。
分配和释放数据结构是所有内核中最普遍的操作之一。为了便于数据的频繁分配和回收,常常会用到一个空间链表。它就相当于对象高速缓存以便快速存储频繁使用的对象类型。在内核中,空闲链表面临的主要问题之一是不能全局控制 。当可用内存变得紧张的时候,内核无法通知每个空闲链表,让其收缩缓存的大小以便释放一些内存来。实际上,内核根本不知道有这样的空闲链表。为了弥补这一缺陷,也为了是代码更加稳固,linux内核提供了slab层(也就是所谓的slab分类器),slab分类器扮演了通用数据结构缓存层 的角色。
核心思想就是“存储池”的运用。内存片段(小块内存)被看作对象,当被使用完后,并不直接释放而是被缓存到“存储池”里,留做下次使用,这无疑避免了频繁创建与销毁对象所带来的额外负载。
cache由kmem_cache结构体描述,包括三个链表slabs_full、slabs_partial、slabs_empty。一个slab包含一个或者多个page,通常是一个,用slab结构体描述。
从cache中分配一个object使用kmem_cache_alloc(),释放通过kmem_cache_free() slab层将空闲页分解成众多相同长度的小块内存,以供同类型的数据结构使用 。