内核中的内存管理
内核把物理页作为内存管理的基本单位,尽管处理器最小寻址单位为字,但是MMU(管理内存并且把虚拟地址转换为物理地址的硬件)通常以页为单位进行处理。
每个物理页面都由一个相应的 struct page 结构来表示,4GB的内存大约有20MB的空间是存储每个物理页的struct page。
struct page {
unsigned long flags; // 页面状态标志
atomic_t _count; // 页面引用计数
atomic_t _mapcount; // 页面映射计数
unsigned long private; // 用于私有数据
struct address_space *mapping; // 与页面关联的地址空间
pgoff_t index; // 页面在地址空间中的偏移量
struct list_head lru; // LRU 链表中的节点
void *virtual; // 虚拟地址
};
通过mapping字段可以获取到与页面关联的地址空间对象,拥有者可以是用户空间进程,动态分配的内核数据,静态内核代码和页高速缓存等。
struct address_space {
const struct address_space_operations *a_ops;
struct inode *host; // 关联的对象,通常是 inode 结构,查看 inode 结构以获取文件的详细信息
};
内核把物理页划分为不同的区(zone),只是逻辑上的划分。
将内存划分为区,形成不同的内存池。
在一些体系结构中,有些内存不能DMA(Direct Memory Access).
有三种分区(常见的):
- ZONE_DMA:可以DMA的内存区域。
- ZONE_NORMAL:可以正常映射的页。
- ZONE_HIGHEM:"高位内存“,其中的页不能永久地映射到地址空间。
使用page_struct来管理页
分页子系统(Page Allocator)使用struct page来维护空闲页的列表、管理页面的状态等。在内核的其他部分,比如文件系统、内存映射子系统等,也会使用struct page来管理页面。
伙伴系统(Buddy System)是一种常见的物理内存管理算法。Buddy Allocator用于管理物理页的分配和释放,而Slab Allocator则用于高效管理内核中的对象缓存。
slab系统和buddy系统和分配内存底层函数的关系
alloc_pages等获得内存的函数是基于buddy系统,而slab系统又是基于分配内存的函数,kmalloc()又是基于slab系统的(使用了一组通用高速缓存)。
伙伴系统(Buddy System):用于内核的物理内存管理
管理物理帧,为了减少碎片。保持块的大小确定,alloc_pages就是使用buddy system,_get_free_pages 函数是基于伙伴系统的实现的。
维持11个链表,代表不同大小的块(页数不同)。
- 链表 0(大小为 2^0 = 1个页)
- 链表 1(大小为 2^1 = 2个页)
- 链表 2(大小为 2^2 = 4个页)
- …
分配内存:
当需要分配一块大小为 2^k 的内存时,伙伴系统首先检查对应大小的内存块链表。如果找到了一个空闲的块,就将其分配出去。
如果对应大小的链表中没有可用的块,系统会向更大一级的链表查找,直到找到一个可用的块。这个找到的块会被一分为二,其中一个分配给请求者,而另一个则继续放入对应大小的链表。
释放内存:
当一块内存被释放时,伙伴系统会检查其相邻的块是否也是空闲的。如果是,它们将被合并成一个更大的块,并将该块插入到对应大小的链表中。
这个过程会一直进行,直到找到一个不是空闲的伙伴块或者达到最大的块大小。
这种方式可以有效地减少外部碎片,因为当内存被释放时,相邻的空闲块很有可能会被合并成一个更大的块,使得更大的内存块变得可用。
需要注意的是,伙伴系统中通常会有多个链表,每个链表对应不同大小的块。
内存分配和释放的函数
内存分配的底层函数
struct page *alloc_pages(gfp_t gfp_mask,unsigned int order);
该函数分配2的order次方个连续的物理页,并返回一个指针,指向第一个page结构体。
alloc_pages函数与伙伴系统紧密相关,因为它通常是由伙伴系统实现的,用于在物理内存中分配一定数量的连续页面。
void *page_address(struct page * page)
将物理页转换为逻辑地址
unsigned long _get_free_pages(gfp_t gft_mask,unsigned int order);
返回一个指针,指向物理页所在的逻辑地址,而不是struct_page
unsigned long get_zeroed_page(unsigned int gfp_mask);
这个函数和_get_free_pages()实现方式相同,只是将分配好的页都填充为了0.
内存释放的底层函数
void _free_pages(struct page* page,unsigned int order)
free_pages(unsigned long addr,unsigned int order);
void free page(unsigned long addr);
以字节为单位的分配和释放:kmalloc()和kfree();vmalloc()和vfree();
void* kmalloc(size_t size,gfp_t flags)
这个函数返回一个指向内存块的指针,其内存块至少要有size大小,所分配的内存区是物理上连续的
gfp_mask标志
行为修饰符标识内核该如何分配所需内存。
void vmalloc(unsigned long size)
返回的内存物理地址上不是连续的,使用较少。
Slab分配器
buddy无法分配字节大小的内存块,slab分配器就应运而生了,专为小内存分配而生。slab分配器分配内存以Byte为单位。但是slab分配器并没有脱离伙伴系统,而是基于伙伴系统分配的大内存进一步细分成小内存分配。
分配和释放数据结构的缓存链表。
空闲链表(即图中的高速缓存)包含可供使用的,已经分配好的数据结构块,当使用时就从链表中取出一个。
图中的对象指的是固定大小的内存块(即被缓存的数据结构)。
我们可以创建任何数据结构的高速缓存链表。
slab管理的内容
Slab Allocator通常用于管理内核中的对象缓存,其中缓存的对象的大小是固定的。这些对象缓存可以包括一系列常用的内核数据结构,以提高对这些数据结构的分配和释放的效率。以下是一些常见的内核数据结构,通常由Slab Allocator来管理:
进程控制块(Process Control Block,PCB):
每个运行的进程在内核中都有一个对应的 PCB,包含了进程的各种信息,如进程状态、寄存器值、进程ID等。
文件结构体(File Structure):
内核中管理文件的结构体,包含有关文件的信息,如文件描述符、文件位置指针、文件状态等。
网络套接字结构体:
用于表示网络套接字的结构体,包含有关套接字的信息,如协议、端口号、连接状态等。
页表项(Page Table Entry):
用于虚拟内存管理的页表项,包含有关虚拟地址到物理地址的映射信息。
目录项(Directory Entry):
用于文件系统管理的目录项,包含有关文件或子目录的信息,如文件名、文件类型、文件大小等。
缓存块结构体:
用于文件系统缓存的数据块结构体,包含文件数据、索引节点等信息。
内核栈
每个进程的内核栈小而且固定。总的来说,内核栈为一页或者两页。
中断处理程序栈
中断栈为每个进程提供一个用于中断处理程序的栈
用户的内存空间分配
创建进程的虚拟地址空间分配
操作系统会为每个用户进程提供一个虚拟地址空间,这个虚拟地址空间通常是连续的、从0开始的地址范围。具体的大小和布局取决于操作系统和硬件架构。
操作系统会为用户进程的虚拟地址空间建立页表,用于将虚拟地址映射到物理地址。
这个过程涉及到虚拟地址的划分,例如将虚拟地址空间划分为代码段、数据段、栈等不同的区域。
懒加载: 初始时,并不是所有的虚拟地址空间都会被立即映射到物理内存中。部分页面可能是“懒加载”的,只有在访问到相应的虚拟地址时,才会触发实际的物理内存分配和映射。