LV10-06-内核内存管理-01-基础知识

本文主要是内核内存管理基础知识的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
Linux开发板 华清远见 底板: FS4412_DEV_V5 核心板: FS4412 V2
u-boot 2013.01
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
文件下载链接
------

一、虚拟地址

通常32位Linux内核地址空间划分0~3G为用户空间,3~4G为内核空间,为了充分利用和管理系统内存资源,Linux采用虚拟内存管理技术,利用虚拟内存技术让每个进程都有 4GB 互不干涉的虚拟地址空间。

不管是用户空间还是内核空间,我们使用的地址都是虚拟地址,当需进程要实际访问内存的时候,会由内核的请求分页机制产生缺页异常,然后调入物理内存页。

二、物理地址

linux 内核将物理内存等分成 N 块 4KB ,称之为一页,每页都用一个 struct page 来表示,采用伙伴关系算法维护(暂时没有深究),所有的物理内存会被内核大概分为 3 个管理区域,分别为:

  • ZONE_DMA

DMA内存区域,包含0MB~16MB之间的内存页框,可以由老式基于 ISA 的设备通过 DMA 使用,直接映射到内核的地址空间。

  • ZONE_NORMAL

普通内存区域,包含16MB~896MB之间的内存页框,常规页框,直接映射到内核的地址空间。

  • ZONE_HIGHMEM

高端内存区域,包含896MB以上的内存页框,不进行直接映射,可以通过永久映射和临时映射进行这部分内存页框的访问。

image-20220918183307252

三、映射关系

虚拟地址和物理地址有怎样的映射关系呢?大概如下图所示:

image-20220918183702011

内核空间虚拟地址各部分详情如下:

虚拟内核空间划分图

【说明】图中high_memory处为虚拟地址3GB + 896MB的位置,PAGE_OFFSET处为虚拟地址3GB位置,也就是内核空间开始的位置。

  • 低端内存

内核空间前896MB的空间被称为低端内存,也就是虚拟地址的3GB~3GB+896MB的范围,这一部分的虚拟地址可以直接与896MB大小的物理内存进行映射,加上一个偏移量就可以了,即:虚拟地址 = 3G + 物理地址。可被直接映射的这部分物理内存可以细分为ZONE_DMA和ZONE_NORMAL。

低端内存分配方式:

(1)使用 kmalloc() 进行分配,它是一种小内存分配,采用的是 slab 算法,分配的内存虚拟地址连续,物理地址也连续。

(2)使用 get_free_page() 函数进行分配,它是整页分配,每次分配2的n次方页内存空间,n 最大为10,需要申请大片的内存空间的时候可以采用此种方式,它分配的内存虚拟地址连续,物理地址也连续。

  • 高端内存

大于3G+896M的内核空间被称为高端内存,也就是虚拟地址的 3GB+896MB ~4GB的范围,可以细分为vmalloc区、持久映射区、固定映射区

高端内存分配方式:

使用 vmalloc() 函数进行分配,使用该函数申请的内存空间的虚拟地址连续,但是物理地址不连续。

四、内核中常用动态分配

1. kmalloc

1.1 kmalloc()

我们使用以下命令查询一下函数所在头文件:

1
2
3
grep kmalloc -r -n ~/5linux/linux-3.14/include
# 搜索结果过多的话,可以加强搜索关键字
grep "*kmalloc(" -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 需包含的头文件 */
#include <linux/slab.h>

/* 函数定义 */
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
if (__builtin_constant_p(size)) {
if (size > KMALLOC_MAX_CACHE_SIZE)
return kmalloc_large(size, flags);
#ifndef CONFIG_SLOB
if (!(flags & GFP_DMA)) {
int index = kmalloc_index(size);

if (!index)
return ZERO_SIZE_PTR;

return kmem_cache_alloc_trace(kmalloc_caches[index],
flags, size);
}
#endif
}
return __kmalloc(size, flags);
}

【函数说明】该函数用于动态申请内存,kmalloc() 申请的内存位于直接映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因为存在较简单的转换关系,所以对申请的内存大小有限制,不能超过128KB。

【函数参数】

  • size:size_t类型,表示要申请的内存空间的大小,注意不要超过128KB。
  • flags:gfp_t类型,表示分配内存的方法。
点击查看 flags 常用取值
flags说明
GFP_ATOMIC分配内存的过程是一个原子过程,分配内存的过程不会被(高优先级进程或中断)打断。
GFP_KERNEL正常分配内存。
GFP_DMA给 DMA 控制器分配内存,需要使用该标志(DMA要求分配虚拟地址和物理地址连续)。
  • 参考用法
1
2
3
4
5
6
7
8
flags 的参考用法
 |– 进程上下文,可以睡眠     GFP_KERNEL
 |– 异常上下文,不可以睡眠    GFP_ATOMIC
 |  |– 中断处理程序      GFP_ATOMIC
 |  |– 软中断         GFP_ATOMIC
 |  |– Tasklet        GFP_ATOMIC
 |– 用于DMA的内存,可以睡眠   GFP_DMA | GFP_KERNEL
 |– 用于DMA的内存,不可以睡眠  GFP_DMA | GFP_ATOMIC

【返回值】void * 类型,返回分配好的内存的首地址,一般都需要进行强制类型转换,转换为我们需要的类型,分配失败返回NULL。

【使用格式】none

【注意事项】 none

1.2 kzalloc()

我们使用以下命令查询一下函数所在头文件:

1
2
3
grep kzalloc -r -n ~/5linux/linux-3.14/include
# 搜索结果过多的话,可以加强搜索关键字
grep "*kzalloc(" -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 需包含的头文件 */
#include <linux/slab.h>

/* 函数定义 */
/**
* kzalloc - allocate memory. The memory is set to zero.
* @size: how many bytes of memory are required.
* @flags: the type of memory to allocate (see kmalloc).
*/
static inline void *kzalloc(size_t size, gfp_t flags)
{
return kmalloc(size, flags | __GFP_ZERO);
}

【函数说明】该函数用于动态申请内存,并且将申请的内存区域初始化为0(可以看出其实是通过调用kmalloc完成的分配)。

【函数参数】

  • size:size_t类型,表示要申请的内存空间的大小,注意不要超过128KB。
  • flags:gfp_t类型,表示分配内存的方法,同 kmalloc()。

【返回值】void * 类型,返回分配好的内存的首地址,一般都需要进行强制类型转换,转换为我们需要的类型,分配失败返回NULL。

【使用格式】none

【注意事项】 none

1.3 kfree()

我们使用以下命令查询一下函数所在头文件:

1
2
3
grep kfree -r -n ~/5linux/linux-3.14/include
# 搜索结果过多的话,可以加强搜索关键字
grep "void kfree" -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
/* 需包含的头文件 */
#include <linux/slab.h>

/* 函数声明 */
void kfree(const void *objp);

【函数说明】该函数用于释放通过 kmalloc() 或者 kkzalloc() 函数申请的动态内存。

【函数参数】

  • objp:void * 类型,表示要释放的内存空间的首地址。

【返回值】none

【使用格式】none

【注意事项】 none

2. vmalloc

2.1 vmalloc()

我们使用以下命令查询一下函数所在头文件:

1
2
3
grep vmalloc -r -n ~/5linux/linux-3.14/include
# 搜索结果过多的话,可以加强搜索关键字
grep "*vmalloc(" -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
/* 需包含的头文件 */
#include <linux/vmalloc.h>

/* 函数声明 */
extern void *vmalloc(unsigned long size);

【函数说明】该函数用于动态申请内存,vmalloc() 函数会在虚拟内存空间给出一块连续的内存区,但这片连续的虚拟内存在物理内存中并不一定连续。由于 vmalloc() 没有保证申请到的是连续的物理内存,因此对申请的内存大小没有限制,如果需要申请较大的内存空间就需要用此函数了。。

【函数参数】

  • size:unsigned long类型,表示要申请的内存空间的大小。

【返回值】void * 类型,返回分配好的内存的首地址,一般都需要进行强制类型转换,转换为我们需要的类型,分配失败返回NULL。

【使用格式】none

【注意事项】 vmalloc() 可以睡眠,因此不能从异常上下文调用。

2.2 vfree()

我们使用以下命令查询一下函数所在头文件:

1
2
3
grep vfree -r -n ~/5linux/linux-3.14/include
# 搜索结果过多的话,可以加强搜索关键字
grep "void vfree" -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
/* 需包含的头文件 */
#include <linux/vmalloc.h>

/* 函数声明 */
extern void vfree(const void *addr);

【函数说明】该函数用于释放通过 vmalloc() 函数申请的动态内存。

【函数参数】

  • addr:void * 类型,表示要释放的内存空间的首地址。

【返回值】none

【使用格式】none

【注意事项】 vfree() 可以睡眠,因此不能从异常上下文调用。

3. kmalloc & vmalloc

  • kmalloc()、kzalloc()、vmalloc() 的共同特点是:

(1)用于申请内核空间的内存;

(2)内存以字节为单位进行分配;

(3)所分配的内存虚拟地址上连续;

  • kmalloc()、kzalloc()、vmalloc() 的区别是:

(1)kzalloc 是强制清零的 kmalloc 操作;(以下描述不区分 kmalloc 和 kzalloc)

(2)kmalloc 分配的内存大小有限制(128KB),而 vmalloc 没有限制;

(3)kmalloc 可以保证分配的内存物理地址是连续的,但是 vmalloc 不能保证;

(4)kmalloc 分配内存的过程可以是原子过程(使用 GFP_ATOMIC),而 vmalloc 分配内存时则可能产生阻塞;

(5)kmalloc 分配内存的开销小,因此 kmalloc 比 vmalloc 要快;

4. 选择原则

一般情况下,内存只有在要被 DMA 访问的时候才需要物理上连续,但为了性能上的考虑,内核中一般使用 kmalloc(),而只有在需要获得大块内存时才使用 vmalloc()。

总的来说:

(1)小内存(< 128k)用kmalloc,大内存用vmalloc或get_free_page;

(2)如果需要比较大的内存,并且要求使用效率较高时用 get_free_page,否则用vmalloc。