《Linux内核设计与实现》阅读笔记:(第十二章)内存管理

书在一开始阐明,第十二章主要讨论的是在内核中获取内存的方法。

一些概念

  • 物理页:内存管理的基本单位;
  • 内存管理单元(MMU):一种硬件,用来管理内存以及把虚拟地址转换为物理地址;
  • 页大小:不同的计算机体系结构支持不同的页大小,32位一般支持4KB的页,64位支持8KB的页;
  • struct page {…}:与物理页相关,而非虚拟页,该结构用于描述物理内存本身,内核用此结构管理所有页,系统中每个物理页都需要分配一个这样的结构体;

一些概念

  • 区:内核把页划分成不同的区,使用区对具有相似特性的页进行分组;
  • 内存寻址问题:一些硬件只能直接访问特定的内存地址(其它不能直接访问);物理地址比虚拟地址还要宽泛;
  • ZONE_DMA:该区包含的页能被硬件直接访问;
  • ZONE_DMA32:该区的页只能被32位设备直接访问;
  • ZONE_NORMAL:该区包含能正常映射的页;
  • ZONE_HIGHEM:该区包含“高端内存”,不能映射到内核地址空间;
  • 区的划分意义:区的划分没有物理意义,这是内核管理页的逻辑分组;

获取页

内核提供了请求内存的底层机制,提供了对内存进行访问的一些接口,通过这些接口在内核内分配和释放内存。

以页为单位分配内存最核心的函数:

1
struct page * alloc_pages(gfp_t gfp_mask, unsigned int order)

分配2order个连续物理页,返回指针指向第一个页的page结构体。

把给定页转换它的逻辑地址:

1
void * page_address(struct page *page)

该函数返回给定物理页当前所在的逻辑地址。

书中放出了所有底层页分配方法的列表。

释放页

释放页的函数:

1
2
3
void __free_pages(struct page *page, unsigned int order)
void free_pages(unsighed long addr, unsigned int order)
void free_page(unsigned long addr)

释放页要谨慎,因为内核态不是用户态,内核完全信任自己,所以传递错误值会导致崩溃。

kmalloc()

用来获得以字节为单位的一块内存,如果需要整个页,那么前面讨论的页分配接口可能是更好的选择。

kmalloc()分配的内存区在物理上是连续的。

1
2
struct sth *p;
kmalloc(sizeof(struct sth), GFP_KERNEL);

GFP_KERNEL标志表示内存分配器将采取行为。

gfp_mask标志

标志它分为三类:

  • 行为修饰符:表示内核应该如何分配所需要的内存;
  • 区修饰符:表示从哪分配内存;
  • 类型标志:组合了行为修饰符和区修饰符,将各种可能用到的组合归纳为不同类型,简化修饰符的使用;

前面提到的GFP_KERNEL就是一种类型标志。

K.V.M(kmalloc、vmalloc、malloc)

技术要点:

  • kmalloc、vmalloc分配的是内核内存,malloc分配的是用户内存;
  • kmalloc分配的内存在物理上连续,vmalloc是在虚拟地址空间上连续;
  • 大多数情况下,只有硬件设备需要得到物理地址连续的内存,因为它在内存管理单元之外,不懂虚拟地址;
  • 需要DMA访问的时候需要物理上连续;
  • 很多内核代码用K而不用V是基于性能考虑;
  • V比直接内存DMA映射会有大得多TLB(一种硬件缓冲区,缓存虚拟到地址物理的映射关系)的抖动;

slab层

数据频繁分配和回收会伴随优化的问题。通常做法是采用空闲链表,它相当于一个对象高速缓存,比如需要新数据就从空闲链表抓取,而不需要分配内存再放数据,当不需要该数据时就把它放回空闲链表,而非释放它。

但是空闲链表一般的实现方法在Linux中会存在一些局限,内核编写者通过权衡各种利弊为Linux内核提供了slab内存分配器。

slab负责内存紧缺情况下所有底层的对其、着色、分配、释放以及回收,如果频繁创建很多相同类型的对象,应该考虑用slab高速缓存,而不要自己去实现空闲链表。

在栈上静态分配

用户空间能够奢侈的负担起非常大的栈,而且栈空间可以动态增长,见下图进程内存的逻辑结构:

进程内存结构

内核栈小而固定,大小依赖体系结构,也与编译时选项有关,历史上每个页都有两个页的内核栈。

为了避免终端处理程序放在内核栈中,内核开发者还实现了中断栈,这样中断处理程序不用再和被中断进程共享一个内核栈。

在内核栈上进行大量的静态分配是很危险的,通过内核栈溢出一文可以看到实验中内存溢出时的打印报告。在栈溢出的时候最好的情况是宕机,最坏是悄无声息破坏数据。

每个CPU分配

SMP的全称是“对称多处理”(Symmetrical Multi-Processing)技术,是指在一个计算机上汇集了一组处理器(多CPU),各CPU之间共享内存子系统以及总线结构。在Linux中,各CPU的数据都存在一个数组中,用当前处理器号作为数组的索引访问数组元素。

补充

内核态和用户态

  • 内核态/用户态:两个状态通过执行硬件指令使CPU切换,也就是说这两个状态是针对CPU而言的;
  • 虚拟内存区域:分为用户空间和内核空间;
  • 用户态:CPU只能访问被标记为用户空间的内存,访问内核空间会出发硬件异常;
  • 内核态:CPU可以访问内核空间和用户空间;

进程/task 处于与世隔绝态。一个简单的例子解释:“某进程创建另一个进程”,其实是某进程请求内核创建另一个进程。进程的“创建”只是一种系统级调用。

系统调用的执行步骤可以通过一张图来说明:

系统调用/用户态内核态切换步骤图

CPU控制

Linux正是通过这种方式“控制”CPU的执行,这里的“控制”指的是执行谁的代码,每当进程进行系统级调用,那么Linux就拿到了CPU的控制权。

Linux还可以通过时钟中断的方式拿到CPU的控制权。下面引用操作系统对CPU的控制权一文的描述:

你的主板里,会有一个时钟,滴滴答答的走着,每隔一段时间,它就会给CPU发信号。CPU收到信号,就会执行预先设定好的操作系统的代码,一旦这些代码被执行了,操作系统就有控制权了。

这就是为什么即便线程处于循环中,仍然能够被操作系统调度。

小结

本文通过快速阅读《Linux内核设计与实现》十二章的内容来了解Linux内存管理的基本架构,为后续相关技术的研究做好铺垫。

参考

《Linux内核设计与实现(第三版)》

《Linux Kernel Development, 3rd Edition》

《Linux_UNIX系统编程手册(上)》

内核栈溢出

操作系统对CPU的控制权