linux内核4-内存管理


导读

注:本章内容均从陈莉君老师的视频中整理
lscpu命令查看cpu结构, 就看到linux内核缓存,

[root@localhost ~]# lscpu
L1d cache:             32K  //1级数据缓存
L1i cache:             32K  //1级指令缓存
L2 cache:              256K
L3 cache:              6144K

这里是linux内存管理机制之一,linux的内存管理有哪些机制,整理如下,他们配合关系如下
比如一个进程创建后读取数据过程,首先内核是通过地址映射机制把进程从磁盘映射到虚拟地址空间,当进程在执行时发现访问的页没有在内存中,就会发出缺页请求(1),内核检测如果有空闲内存可以分配,就会请求分配内存(2);如果缓存有足够的内存就将数据放入缓存中;如果缓存不足,就会触发交换机制,腾出一部分内存(4,5);交换机制在交换的过程中也会用到缓存(6),并将交换后的页表更新到地址映射中(7),最后地址映射就会通过TLB访问缓存(8)。
在这里插入图片描述

一、 进程空间

1.1 用户空间布局

内存管理一个主要目的就是让进程在运行的时候互补干扰,让进行以为自己在独占使用内存,从而减少软件开发工作量。实际上,对于进程而言,各个进程的用户空间是互不可见的,但是内核空间是所有进程以及内核共享的。如下图所示:在32位系统中,各个进程都以为自己独占3G内存(互不可见),但都共享了1G内核空间。
在这里插入图片描述
那么对于进程的地址空间进一步细分,又是怎样的呢,如下图所示:这里主要是对用户空间进行细分,代码段,数据段,堆以及栈等。
在这里插入图片描述

1.2 内核空间

内核空间与物理内存的映射关系,其中小于high_memory部分(896MB)通过线性映射的,高于high_method的部分,通过固定映射,持久映射以及vmalloc进行映射。如下图
在这里插入图片描述
进一步查看分配关系如下:
在这里插入图片描述
内核的虚拟地址与物理地址转换是线性的,其代码如下:

#define __pa(x)			((unsigned long) (x) - PAGE_OFFSET)  //虚拟转物理
#define __va(x)			((void *)((unsigned long) (x) + PAGE_OFFSET)) // 物理转虚拟

1.3 进程空间数据结构

上述的进程空间如何通过数据结构描述,如下图所示。一个进程的内存空间通过两个数据结构进行描述:

  1. mm_struct:描述进程整个用户空间,源代码参见:mm_types.h
  2. vm_area_struct:描述用户空间的各个内存区
    在这里插入图片描述

1.3.1 mm_struct

这里主要说明mm_struct关键数据结构的含义, 细细读一下,会对mm_struct主要行为有大致了解:

  1. mmap, mm_rb: 虚拟区的组织有两种,当虚存区少时,用链表,mmap指向; 多时用红黑树,mm_rb指向;
  2. pgd: 指向进程的页目录,当调度程序调度一个程序运行的时候,就将这个虚地址转换成物理地址并写入控制寄存器CR3中;
    在这里插入图片描述
    除上述成员变量外,mm_struct还通过以下成员的去描述进程地址空间,如下图,从图中不难理解各个结构体成员指向的是虚拟地址
    在这里插入图片描述

1.3.2 vm_area_struct

该结构体描述用户进程空间的一个虚拟内存区间(virtural meory area), 其重要成员如下:
在这里插入图片描述
为甚么linux要把进程的用户空间划分成一个一个的区间呢? 原因是:每个虚拟区的来源可能不同,有的来源于可执行文件的映像,有的来自共享库,来自堆空间,每个来源的读写属性不同,也可能有不同的操作,因此需要分隔管理。 这些区别操作主要由上表中的vm_ops进行实现,主要实现open,clse,nopage(缺页处理函数)等操作。

二、用户空间管理

2.1创建

上节的进程的地址空间(mm_struct,vm_area_struct)是什么时候被映射的呢?
当fork系统调用在进程创建的时候,整体的堆栈结构体被创建,基本有两种方式拷贝或共享父进程用户地址空间(共享指内核线程创建,参考linux内核3-进程调度),在源代码中通过copy_mm()函数来实现,其中利用写时复制技术进行快速创建。

2.2 虚存映射

当我们调用exec系统调用开始执行一个进程的时候,进程的代码段,数据段,堆,栈,共享库也必须装入到进程的用户地址空间中。注意:,linux并不是将映像装入到物理内存,相反,可执行文件是被映射到了进程的用户空间中,这种映射方法叫做虚存映射
在这里插入图片描述

2.2.1 VMA新建方法

想要理解虚存映射,就需要了解VMA的新建方法,用户空间我们经常通过mmap系统调用内核的do_mmap进行创建。其他相关的系统调用函数有哪些:
fork: 用户空间的所有页标记为写时复制,父子进程共享
mmap:在进程的空间内存创建一个新的虚拟内存
munmap: 销毁一个完整的虚存区或其中的一部分,如果取消某个虚存区中间部分,就会被划分两个虚存区
exec: 使用装入的可执行文件代替当前用户空间
exit: 销毁用户空间及其所有的虚存区;

那么虚存区的分类有哪些:

  1. 共享的:有几个进程共享的内存映射读写操作,如文件的共享
  2. 私有的: 进程创建内存映射只是为了写,此时通过私有的效率会更高。
  3. 匿名的: 当映射与内存无关,就叫做匿名映射。映射关系参见上图。

2.2.2 虚拟区实例

如何查看一个进程的虚存映射呢?答案在proc目录下存放,文件目录为进程pid,文件为maps
下面我们通过编写内核模块来打印虚存区的内容:

  1. 编写一个应用程序,申请一段堆空间
char *buf = (char*)malloc(sizeof(char) * 1024);
printf("my pid = %d\n, buf=%p\n", getpid(), buf);
for (int i = 0; i < 60; i++)
    sleep(60);
  1. 编写内核模块,打印应用程序的虚拟内存
static int pid;
module_param(pid, int , 0644);

static int __init vma_init(void)
{
    
    
    struct task_struct *p = NULL;
    struct vm_area_struct *temp = NULL;

    printk("the vma are:\n");
    p = pid_task(find_vpid(pid), PIDTYPE_PID);

    temp = p->mm->mmap;
    while (temp != NULL) {
    
    
        printk("start:%p, end:%p\n", (unsigned long*)(temp->vm_start),
            (unsigned long*)(temp->vm_end));
        temp = temp->vm_next;
    }
    return 0;
}
  1. 运行运用程序,并插入内核模块
    ./a.out &
    insmod vma.ko pid=xxxx
# a.out打印
my pid = 5212, buf=0x250a010
[ 2224.913367] the vma are:
[ 2224.913371] start:0000000000400000, end:0000000000401000
[ 2224.913373] start:0000000000600000, end:0000000000601000
[ 2224.913374] start:0000000000601000, end:0000000000602000
[ 2224.913375] start:000000000250a000, end:000000000252b000 #内存区
[ 2224.913375] start:00007f8c39067000, end:00007f8c3922a000
[ 2224.913376] start:00007f8c3922a000, end:00007f8c3942a000
[ 2224.913377] start:00007f8c3942a000, end:00007f8c3942e000
[ 2224.913378] start:00007f8c3942e000, end:00007f8c39430000
[ 2224.913379] start:00007f8c39430000, end:00007f8c39435000
[ 2224.913380] start:00007f8c39435000, end:00007f8c39457000
[ 2224.913381] start:00007f8c3963c000, end:00007f8c3963f000
[ 2224.913382] start:00007f8c39654000, end:00007f8c39656000
[ 2224.913383] start:00007f8c39656000, end:00007f8c39657000
[ 2224.913384] start:00007f8c39657000, end:00007f8c39658000
[ 2224.913384] start:00007f8c39658000, end:00007f8c39659000
[ 2224.913385] start:00007ffd4d5f5000, end:00007ffd4d616000
  1. 通过 vim /proc/进程号/maps 查看到打印的内核消息均是正确的:
    在这里插入图片描述

2.3 请页机制

请页机制是实现虚拟内存管理的重要手段, 如下图所示, 当一个进程运行的时候CPU访问的是用户控件的虚地址,linux仅把少量的放入内存,需要的时候才会通过请页机制将特定的页面调用到内存中。
在这里插入图片描述
代码查看do_page_fault, 请页及其处理逻辑需要主要一下三个判断,结合图看出具体的处理方式:

  1. 判断page fault是编程错误引起的异常还是缺页引起的异常
  2. 判断发生在内核态还是用户态
  3. 判断访问类型与线性区的访问权限
    在这里插入图片描述

2.4 内存分配与回收

根据请页机制,需要将数据进行加载的情形,整体流程如下:
如下图的情形,进程通过mmap,read 加载磁盘中的file文件,要通过缺页请求将数据加载到物理内存后,然后更新page table映射关系后,进程通过page table访问内存数据,为了提高访问效率,部分数据还会存入到page cache中。
在这里插入图片描述
根据应用程序区域,一般以下两种情形:
一. 加载运行程序,其过程如下
* 内核根据创建进程建立的映射关系,找到所需的内容在执行文件中的位置;
* 分配物理内存页面,将可执行文件内容加载到该内存页中
* 建立该物理页面和虚拟地址空间的映射关系(填充页表),然后把控制权交还给进程;
二. malloc申请动态内存,其过程如下
* 内核根据申请物理内存的大小分别调用不同的系统调用建立映射关系,此时,并不会立即分配物理内存;
* 只要在读写时,才通过请页机制分配出内存来。
在这里插入图片描述
讲解完虚拟地址与物理内存的映射,那个物理内存如果被管理的,linux采用了如下四种机制:伙伴算法,slab缓存,vmalloc,per-CPU页框高速缓存;物理内存在逻辑上呗划分为三级结构,分别使用pg_data_t,zone,page这三种结构体描述。对于pg_data_t与cpu个数相关,而zone一般根据用户内核空间布局分为如下三种:

  1. zone_dma: 标记适合MDA的内存区,在0-16M
  2. zone_normal:可以直接映射到内核空间的物理内存,小于high memory区域
  3. zone_highmem:高端物理内存,对于64位系统,不再有此概念。
    在这里插入图片描述

2.4.1 伙伴算法

负责分配大块连续物理内存的分配和释放,以页框为基本单元。该机制避免了外部碎片。

  1. 什么叫伙伴:大小相同, 连续的物理页面
  2. 伙伴算法:把所有的空闲页面分为多个链表(默认11个),每个链表中一个快含有2的幂次页面;
  3. 相关的数据结构:
  • 物理页框 struct page
  • 内存区 struct zone
  • 空闲页框管理 free_area
    在这里插入图片描述
  1. 分配原理:如果分配阶为n的页框快,那么先从第n条页框块链表中查找是否存在这么大的空闲块。如果不存在则从n+1条链表中继续查找后,该分开对半等分后,一半给申请元素使用,另一半作为新元素对半等分,一般插入n-1的链表,另一半再对分插入n-2链表。相关函数:__rmqueue_smallest() 查看合适页框, expand()分裂大页块
    在这里插入图片描述
  2. 分区页框分配函数调用关系
    在这里插入图片描述

2.4.2 per-CPU页框高速缓存

内核经常请求和释放单个页框,该缓存包含预先分配的页框,用于满足本地CPU发出的单一页框请求。

2.4.3 slab缓存

负责小块物理内存的分配,并且它也作为高速缓存,主要针对内核中经常分配并释放的对象。
将内核常用的数据结构看作对象,slab分配器为每种对象建立高速缓存,内核对这些对象的分配与释放均在这块高速缓存中操作,大致分为两类:

  1. slab通用缓存: 分配任意大小的物理内存,接口kmalloc(),其物理页是连续的
  2. slab专用缓存: 特定对象如task_struct

2.4.4 vmalloc机制

使内核通过连续地址来访问非连续的物理页框,这样可以最大限度的使用高端物理内存。非连续的物理内存分配处理3G到4G之间内核空间的高端内存区。常见的使用场景是内核模块的加载,其数据较多,连续内存不一定分配成功,因此需要不连续的内存。
在这里插入图片描述

2.4.5 总结

在这里插入图片描述
__get_free_pages()alloc_pages(): 从分区页框分配器中获取页框;
kmem_cache_alloc()kmalloc(): 使用slab分配器从专用或通用对象分配块;
vmalloc()vmalloc32(): 获取一块非连续的内存区;

3 总结

3.1 相关的函数调用接口

在这里插入图片描述

4 参考资料

  1. 《深入理解linux内核》第三版8,9章
  2. 内存管理那些事

猜你喜欢

转载自blog.csdn.net/CPriLuke/article/details/111026195