Linux内核空间的布局
目录
一、内核空间布局图
(1)线性映射区域(绿色)
(2).text、.data、.init(粉色系列)
(3)modules(深绿)
(4)PCI I/O(橙色)
(5)vmemmap(浅紫)
(6)vmalloc(浅紫)
(7)不规范地址(蓝色)
二、内核空间分配虚拟地址的函数
(1)kmalloc:物理地址连续的小内存
(2)vmalloc:物理地址不一定连续的大内存
(3)alloc_pages:按页分配连续的物理内存
三、用户空间分配虚拟内存的函数
1. brk:调整数据段(堆)的边界
2. sbrk:间接调整堆边界(库函数,基于brk)
3. mmap:映射内存区域(匿名映射 / 文件映射)
4.关于PCB中的结构体mm_struct
我们知道所有的操作系统都会分为用户空间和内核空间,因为这样可以方便操作系统管理权限、用户进程等。但是关于内核空间真正的布局是什么样子呢?这篇文章我们就来仔细看看。
一、内核空间布局图
在64位系统中,有着2^64次方个地址,即170多亿TB空间。但是真实的物理内存远远不可能这么大,所以操作系统中谈论地址一般都指的是虚拟地址,即进程误以为自己掌控了那么大的空间,实际上都是页表营造出来的假象罢了。
即使在虚拟地址空间中,也要有一个规范的使用。于是普遍把低地址的空间给用户地址空间,高地址给内核使用,至于中间的部分则空缺出来不允许使用,也许随着技术的发展,内存空间极度扩大等原因,中间部分也能被利用起来。
而内核布局如下:
我们一一看看其作用:
(1)线性映射区域(绿色)
- 作用:把 物理内存 直接映射到内核虚拟地址。简单说,内核想快速访问物理内存里的数据(比如硬件驱动读写内存)比如kmalloc、就依赖于此,用这里最方便。
- 关键点:地址是 “线性” 对应的(虚拟地址和物理地址差个固定值),访问速度快,但会受物理内存大小限制。
(2).text、.data、.init(粉色系列)
- .text(代码段):存内核的可执行代码(比如调度进程、处理系统调用的逻辑),只读!防止内核自己把代码改乱。
- .data(数据段):存内核的全局变量、静态变量(比如记录系统总内存的变量)。
- .init(初始化段):存内核启动时的初始化代码,启动完成后会被释放(节省内存)。
(3)modules(深绿)
- 作用:加载内核模块(比如 USB 驱动、网卡驱动)的区域。驱动不需要编译进内核,用
insmod
动态加载到这里。这里就是我们之前所说的Linux借鉴了微内核插件式的特点。(4)PCI I/O(橙色)
- 作用:给 PCI 设备(比如显卡、网卡)的 I/O 地址用的。内核通过这里读写硬件寄存器,和外接设备通信。
(5)vmemmap(浅紫)
- 作用:管理物理页框(Page Frame)的元数据。内核用它记录 “哪些物理内存被占用、哪些空闲”,类似一个 “内存台账”。
- 物理内存中的每个页框(Page Frame)对应 vmemmap 中的一个
struct page
;(6)vmalloc(浅紫)
- 作用:分配 虚拟地址连续、物理地址可能不连续 的内存。比如内核需要一大块内存,但物理内存零散,用
vmalloc
拼接虚拟地址。- 关联函数:内核里用
vmalloc()
分配,对应用户空间的mmap
(但机制不同)。(7)不规范地址(蓝色)
- 作用:“禁区”!内核和用户空间都不能用,防止程序乱访问地址导致崩溃。
二、内核空间分配虚拟地址的函数
(1)kmalloc
:物理地址连续的小内存
作用:分配 物理地址连续 的内存,适合驱动里存小数据(比如设备结构体)。基于
slab
分配器,高效且少碎片。
内核中管理的一些数据结构,各种设备的结构体节点,pcb等因为查询频繁,最好使用连续的物理内存,能提高查找效率和命中率
#include <linux/slab.h>
void driver_init()
{struct device_data *data = kmalloc(sizeof(struct device_data), GFP_KERNEL);if (data) { /* 用内存 */ }kfree(data); // 释放
}
(2)vmalloc
:物理地址不一定连续的大内存
作用:分配 虚拟地址连续、物理地址可能分散 的内存,适合需要大块内存但物理内存零散的场景(比如加载大模块)。
缺点:因为物理地址不连续,访问速度比
kmalloc
慢一点。
一般在操作一些驱动设备模块的时候,需要较大的空间,并且对物理内存不要求连续,则往往会使用vmalloc。
(3)alloc_pages
:按页分配连续的物理内存
作用:直接分配物理页框(比如 4KB 一页),返回页的虚拟地址。适合底层内存管理(比如文件缓存)。
有些外设会使用DMA操作,提高读取效率,这时候按页分配连续的物理内存较为合适。
#include <linux/gfp.h>
struct page *pages = alloc_pages(GFP_KERNEL, 2); // 分配 2 页(8KB)
// ... 使用内存 ...
__free_pages(pages, 2); // 释放
三、用户空间分配虚拟内存的函数
1. brk
:调整数据段(堆)的边界
- 功能:通过修改进程数据段(堆)的结束地址(
brk
值),扩展或收缩堆空间。
堆是从数据段末尾(end_data
)向高地址增长的区域,brk
系统调用直接控制堆的 “边界”。- 参数:
addr
为新的堆结束地址(必须是页对齐的)。- 返回值:
- 成功:返回 0(表示堆已调整到
addr
);- 失败:返回 - 1(如内存不足)。
- 适用场景:分配小内存块(堆内存),
malloc
对小内存的分配通常基于brk
实现。
#include <unistd.h>
#include <stdio.h>
int main()
{void *current_brk = sbrk(0); // 获取当前brk值(sbrk是库函数,基于brk)void *new_brk = (char*)current_brk + 4096; // 新地址(+4KB)if (brk(new_brk) == -1) {perror("brk failed");return 1;}printf("堆已扩展到:%p\n", new_brk);return 0;
}
2. sbrk
:间接调整堆边界(库函数,基于brk
)
- 说明:
sbrk
不是系统调用,而是 glibc 提供的库函数,其底层通过brk
系统调用实现,更方便地调整堆大小。- 参数:
increment
为堆的增量(正数扩展,负数收缩,0 则返回当前brk
值)。void *sbrk(intptr_t increment);
- 返回值:
- 成功:返回调整前的
brk
值;- 失败:返回
(void*)-1
。
3. mmap
:映射内存区域(匿名映射 / 文件映射)
- 功能:在进程地址空间中创建一个新的内存映射区域,可用于分配 “匿名内存”(不关联文件)或映射文件内容到内存。
匿名映射是用户空间分配大内存块的主要方式(malloc
对大内存通常用mmap
)。- 适用场景:分配大内存块(通常
malloc
对大于 128KB 的内存用mmap
,避免堆碎片)、文件映射。
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
4.关于PCB中的结构体mm_struct
进程的所有内存操作(堆、栈、映射区域)都通过mm_struct
被内核跟踪和管理,确保地址空间的合法性和安全性。也就是说我们上述所有给用户空间分配内存的操作,本质都是在修改mm_struct的成员。
用户空间的api,如brk、mmap、malloc本质都是在填充内核中mm_struct的某些字段,并不会当时就分配好内存,仅规划虚拟地址空间。而是真正要使用的时候,会触发缺页中断,再去查看mm_struct,最终调用到alloc_pages才算分配物理内存。