大家好,我是硬核王同学,最近在做免费的嵌入式知识分享,帮助对嵌入式感兴趣的同学学习嵌入式、做项目、找工作!
移步飞书获得更好阅读体验:
Docs
Hello,大家好我是硬核王同学,是一名刚刚工作一年多的Linux工程师,很感谢EEWorld的本次活动,让我有机会参与评测这本和Linux内核相关的的这本书。
在计算机编程中,内存分配是程序设计中的关键问题之一,而kmalloc、vmalloc、malloc和new是常用的内存申请方式。kmalloc和vmalloc主要用于操作系统内核空间中的内存分配,而malloc和new则主要用于应用程序中的内存分配。本文将一起了解下这几种常用的内存申请方式及其特点。
一、kmalloc
(1)定义和特点
kmalloc是Linux内核中提供的用于分配内核空间中连续内存的函数。
其特点如下:
- 连续内存分配:kmalloc分配的内存是连续的,适合需要对连续内存进行操作的场景。
- 小内存块:kmalloc适用于分配小内存块,一般最大限制是128KB。
- 物理内存映射:kmalloc分配的内存与物理内存进行了映射,可以直接访问物理地址,适合需要直接操作物理地址的场景。
(2)底层实现机制
kmalloc()函数的核心实现是slab机制。
类似于伙伴系统机制,在内存块中按照2的order字节来创建多个slab描述符,如16字节、32字节、64字节、128字节等大小,系统会分别创建kmalloc-16、kmalloc-32、kmalloc-64等slab描述符,在系统启动时这在creat_kmalloc_caches()函数中完成。
如要分配30字节的一小内存块,可以用“kmalloc(30,GFP_KERNEL)”实现,之后会从kmalloc-32 slab描述符中分配一个对象。
<include/linux/slab.h>
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
int index = kmalloc_index(size);
return kmem_cache_alloc_trace(kmalloc_caches[indeex], flags, size);
}
kmalloc_index()函数可以用于查找使用的是哪个slab缓冲区,这很形象地展示了kmalloc()的设计思想。
(3)适用场景
kmalloc主要用于在内核空间中分配内存块,适用于以下情况:
- 内核模块开发:在编写内核模块时,可能需要动态分配内存来存储数据结构、缓冲区或其他临时数据。
- 驱动程序开发:在编写设备驱动程序时,可能需要分配内存来管理设备的状态、接收和发送数据等。
- 内核组件开发:在开发或修改内核的组件时,可能需要分配内存来实现特定的功能或数据结构。
- 内核线程和进程:内核线程或进程可能需要分配内存来存储上下文、堆栈或其他数据。
kmalloc的限制包括:
- 仅能在内核空间中使用:kmalloc函数只能在内核空间中使用,无法直接在用户空间中调用。
- 内存大小限制:kmalloc对分配的内存块大小有一定的限制,具体限制取决于系统设置和内核版本。
- 内存泄漏风险:kmalloc分配的内存块需要手动释放,在不再使用时必须调用对应的kfree函数释放内存,否则可能导致内存泄漏。
(4)代码示例
以下是一个使用kmalloc分配内存的示例代码:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
static char *buffer;
static int __init my_module_init(void)
{
// 分配100字节的内存块
buffer = kmalloc(100, GFP_KERNEL);
if (!buffer) {
printk(KERN_INFO "Failed to allocate memory\n");
return -ENOMEM;
}
// 使用分配的内存块
snprintf(buffer, 100, "Hello, world!");
printk(KERN_INFO "Buffer: %s\n", buffer);
return 0;
}
static void __exit my_module_exit(void)
{
// 释放内存块
kfree(buffer);
printk(KERN_INFO "Memory freed\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
二、vmalloc
(1)定义和特点
vmalloc是Linux内核提供的用于在内核空间中动态分配虚拟内存的函数。
vmalloc有以下特点:
- 分配虚拟内存:vmalloc函数用于在内核空间中分配虚拟内存块,可以跨越多个物理页,不要求物理页是连续的。
- 动态内存分配:vmalloc函数可以根据需要动态地分配内存块,而不需要在编译时指定内存大小。
- 适用于大内存分配:vmalloc适用于分配大量内存的场景,例如大型数据结构、缓冲区或需要大量内存的数据。
(2)底层实现机制
在Linux 5.0内核代码中,vmalloc函数的实现代码位于mm/vmalloc.c文件中。以下是该函数的定义和实现:
<mm/vmalloc.c>
void *vmalloc(unsigned long size)
{
return __vmalloc_node_flags(size, NUMA_NO_NODE, GFP_KERNEL);
}
EXPORT_SYMBOL(vmalloc);
vmalloc函数的作用是分配一块虚拟内存区域,并返回该区域的起始地址。
vmalloc函数只有一个参数就是指定需要的内核虚拟地址空间的大小size,但是size的不能是字节只能是页。我已1页4K为例,该函数分配的内存大小是4K的整数倍。
GFP_KERNEL是内核中分配内存最常用的标志,这种分配可能会引起睡眠,阻塞,使用的普通优先级,用在可以重新安全调度的进程上下文中。因此不能在中断上下文中调用vmalloc,也不允许在不允许阻塞的地方调用。
__GFP_HIGHMEM表示尽量从物理内存区的高端内存区分配内存,x86_64没有高端内存区。
在vmalloc函数中,调用了__vmalloc_node_flags函数,该函数实现了具体的虚拟内存分配逻辑。
<mm/vmalloc.c>
static inline void *__vmalloc_node_flags(unsigned long size,
int node, gfp_t flags)
{
return __vmalloc_node(size, 1, flags, PAGE_KERNEL,
node, __builtin_return_address(0));
}
vmalloc()函数的核心实现主要是调用__vmalloc_node_range()函数实现的。
<mm/vmalloc.c>
static void *__vmalloc_node(unsigned long size, unsigned long align,
gfp_t gfp_mask, pgprot_t prot,
int node, const void *caller)
{
return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END,
gfp_mask, prot, node, caller);
}
这里的VMALLOC_START和VMALLOC_END是vmalloc()中很重要的宏,VMALLOC_START是vmalloc区域的开始地址,它以内核模块区域的结束地址(VMALLOC_END)为起始点。
在ARM64系统中,VMALLOC_START宏的值为0xFFFF 0000 1000 0000,VMALLOC_END宏的值为0xFFFF 7DFF BFFF 0000,整个vmalloc区域的大小为129022GB。
<mm/vmalloc.c>
void *__vmalloc_node_range(unsigned long size, int node,
unsigned long start, unsigned long end,
gfp_t gfp_mask, pgprot_t prot,
unsigned long vm_flags,
void *caller)
{
struct vm_struct *area;
struct vm_area_struct *vma;
unsigned long align = PAGE_SIZE;
unsigned long size_aligned;
/* 对 size 进行对齐操作 */
size_aligned = ALIGN(size, align);
/* 在全局 vmalloc 列表中查找合适的内存块 */
area = __find_vma_alloc(size_aligned, align, start, end, gfp_mask, prot, vm_flags);
if (!area)
return NULL;
vma = area->addr;
/* 通过调用 __do_vm_map 函数将虚拟内存映射到物理页 */
if (likely(__do_vm_map(area, vma, vm_flags, caller))) {
if (!(gfp_mask & __GFP_HIGHMEM))
kmemleak_vmalloc(vma->vm_start, size_aligned, vma->vm_flags);
return (void *)vma->vm_start;
}
vfree(area);
return NULL;
}
它首先对要分配的虚拟内存大小进行对齐操作,然后调用 __find_vma_alloc 函数查找合适的内存块。通过调用 __do_vm_map 函数将虚拟内存映射到物理页,并且如果分配成功,返回分配的虚拟内存区域的起始地址;如果分配失败,则释放已分配的内存并返回空指针。
vmalloc()的分配流程如图所示:
(3)适用场景
vmalloc主要用于在内核空间中分配虚拟内存块,适用于以下情况:
- 大内存分配:当需要分配大量连续内存时,但物理页不一定需要连续时,可以使用vmalloc函数。
- 驱动程序开发:在编写设备驱动程序时,可能需要分配大量内存来存储数据缓冲区或其他大型数据结构。
- DMA操作:在进行DMA操作时,可能需要在内核空间分配虚拟内存来缓存或管理DMA数据。
vmalloc的限制包括:
- 仅能在内核空间中使用:vmalloc函数只能在内核空间中使用,无法直接在用户空间中调用。
- 性能开销:vmalloc分配的内存块不一定在物理上是连续的,并且可能跨越多个物理页,这可能导致性能开销,特别是在访问大量虚拟内存时。
- 物理页大小限制:vmalloc分配的虚拟内存块的大小受物理页大小限制,具体限制取决于系统设置和硬件架构。
(4)代码示例
以下是一个示例代码,展示了在内核模块中使用vmalloc函数分配和释放虚拟内存的过程:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/vmalloc.h>
static int *buffer;
static int __init my_module_init(void)
{
// 分配100个整型数据的虚拟内存
buffer = (int *)vmalloc(100 * sizeof(int));
if (!buffer) {
printk(KERN_INFO "Failed to allocate memory\n");
return -ENOMEM;
}
// 使用虚拟内存
for (int i = 0; i < 100; i++) {
buffer[i] = i;
}
// 打印虚拟内存中的数据
printk(KERN_INFO "Buffer: ");
for (int i = 0; i < 100; i++) {
printk(KERN_CONT "%d ", buffer[i]);
}
printk(KERN_CONT "\n");
return 0;
}
static void __exit my_module_exit(void)
{
// 释放虚拟内存
vfree(buffer);
printk(KERN_INFO "Memory freed\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
三、malloc
(1)定义和特点
malloc(Memory Allocation)是C语言标准库中的函数,用于在堆(heap)中分配一块特定大小的内存空间,并返回指向该内存空间的指针。
以下是malloc函数的三个特点:
- 动态内存分配:malloc函数可以在程序运行时动态地分配所需的内存空间。这意味着可以根据实际需求来动态调整内存分配的大小,而不需要提前知道需要多大的内存空间。
- 返回指向分配内存的指针:malloc函数会返回一个指向分配内存空间的指针,可以将该指针赋值给指针变量,并通过该指针访问和操作已分配的内存。
- 不会初始化内存空间:malloc函数不会对分配的内存空间进行初始化。分配的内存单元中可能包含之前使用过的数据。因此,在使用分配的内存空间之前,需要手动进行初始化操作,以确保数据的正确性和一致性。
(2)底层实现机制
malloc底层实现有两种情况:
- 当开辟的空间小于 128K 时,调用 brk()函数,malloc 的底层实现是系统调用函数 brk(),其主要移动指针 _enddata(此时的 _enddata 指的是 Linux 地址空间中堆段的末尾地址,不是数据段的末尾地址)
- 当开辟的空间大于 128K 时,mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟。
类型1:当maalloc 小于 128K 的内存,使用 brk 分配
将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系),如下图:
1,进程启动的时候,其(虚拟)内存空间的初始布局如图1所示
2,进程调用A=malloc(30K)以后,内存空间如图2:
malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配
你可能会问:难道这样就完成内存分配了?
事实是:_edata+30K只是完成虚拟地址的分配,A这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的。
3,进程调用B=malloc(40K)以后,内存空间如图3
类型2:malloc 大于 128K 的内存,使用 mmap 分配(munmap 释放)
4,进程调用C=malloc(200K)以后,内存空间如图4
默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存
这样子做主要是因为:
brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,因为只有一个_edata 指针,这就是内存碎片产生的原因,什么时候紧缩看下面),而mmap分配的内存可以单独释放。
当然,还有其它的好处,也有坏处,再具体下去,有兴趣的同学可以去看glibc里面malloc的代码了。
5,进程调用D=malloc(100K)以后,内存空间如图5
6,进程调用free(C)以后,C对应的虚拟内存和物理内存一起释放
7,进程调用free(B)以后,如图7所示
B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,如果往回推,那么D这块内存怎么办呢?当然,B这块内存,是可以重用的,如果这个时候再来一个40K的请求,那么malloc很可能就把B这块内存返回回去了
8,进程调用free(D)以后,如图8所示
B和D连接起来,变成一块140K的空闲内存
9,默认情况下:
当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩,变成图9所示
最后是malloc()函数的实现流程:
(3)适用场景
malloc函数适用于以下情况:
- 需要动态分配内存空间的情况。
- 需要在函数返回后仍然有效的内存空间。
malloc函数的主要限制有:
- 分配的内存空间必须是连续的,因此可能会造成内存碎片的问题。
- 分配的内存空间不会初始化,可能包含未初始化的数据。
- 必须确保及时释放已分配的内存空间,否则可能导致内存泄漏问题。
(4)代码示例
以下是一个简单的示例代码,展示了如何使用malloc函数分配内存空间:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *numbers;
int size = 5;
// 使用malloc分配内存空间
numbers = (int *)malloc(size * sizeof(int));
if (numbers == NULL) {
printf("内存分配失败!\n");
return 1;
}
// 初始化分配的内存空间
for (int i = 0; i < size; i++) {
numbers[i] = i;
}
// 使用分配的内存空间
for (int i = 0; i < size; i++) {
printf("%d ", numbers[i]);
}
printf("\n");
// 释放已分配的内存空间
free(numbers);
return 0;
}
在上述示例代码中,使用malloc函数分配了一段大小为5个整数的内存空间,并将返回的指针赋值给指针变量numbers。然后,使用for循环初始化了分配的内存空间,将0到4依次赋值给numbers数组中的元素。接着,使用另一个for循环遍历并打印出numbers数组的元素。最后,通过调用free函数释放了已分配的内存空间,以避免内存泄漏问题。
四、结论
- kmalloc函数是在内核中动态分配一块指定大小的连续内存区域。主要用于内核代码中的动态内存分配。由于需要分配连续的内存区域,所以适用于需要访问连续内存区域的数据结构,如需要进行连续存储、读写操作的缓冲区或数据块。
- vmalloc函数是在内核中动态分配一块指定大小的虚拟内存区域。主要用于内核代码中的动态内存分配。虚拟内存区域不要求连续,所以适用于需要大块内存但不要求连续性的数据结构,如大数组、大缓冲区、映射物理设备等。
- malloc函数是在用户空间动态分配一块指定大小的连续内存区域。主要用于用户空间的动态内存分配。适用于需要分配一块连续内存区域用于存储数据的情况,如动态创建数组、链表、字符串等。
在Linux系统中有很多申请内存的方式,不仅仅只有kmalloc、vmalloc、malloc等方式,不过这几个是最常见的。了解这些申请内存的方式和底层原理,有助于我们可以更好地设计出高效的程序。最后,想要了解更深的内存申请底层技术实现,可以阅读相关的一些书籍或参考源码进行理解~
如果觉得有用请点个免费的赞,您的支持就是我最大的动力,这对我很重要!!!