大家好,我是硬核王同学,最近在做免费的嵌入式知识分享,帮助对嵌入式感兴趣的同学学习嵌入式、做项目、找工作!
移步飞书获得更好阅读体验:
Hello,大家好我是硬核王同学,是一名刚刚工作一年多的Linux工程师,很感谢EEWorld的本次活动,让我有机会参与评测这本和Linux内核相关的的这本书。
在计算机编程中,内存分配是程序设计中的关键问题之一,而kmalloc、vmalloc、malloc和new是常用的内存申请方式。kmalloc和vmalloc主要用于操作系统内核空间中的内存分配,而malloc和new则主要用于应用程序中的内存分配。本文将一起了解下这几种常用的内存申请方式及其特点。
kmalloc是Linux内核中提供的用于分配内核空间中连续内存的函数。
其特点如下:
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()的设计思想。
kmalloc主要用于在内核空间中分配内存块,适用于以下情况:
kmalloc的限制包括:
以下是一个使用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是Linux内核提供的用于在内核空间中动态分配虚拟内存的函数。
vmalloc有以下特点:
在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()的分配流程如图所示:
vmalloc主要用于在内核空间中分配虚拟内存块,适用于以下情况:
vmalloc的限制包括:
以下是一个示例代码,展示了在内核模块中使用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(Memory Allocation)是C语言标准库中的函数,用于在堆(heap)中分配一块特定大小的内存空间,并返回指向该内存空间的指针。
以下是malloc函数的三个特点:
malloc底层实现有两种情况:
将_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
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()函数的实现流程:
malloc函数适用于以下情况:
malloc函数的主要限制有:
以下是一个简单的示例代码,展示了如何使用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函数释放了已分配的内存空间,以避免内存泄漏问题。
在Linux系统中有很多申请内存的方式,不仅仅只有kmalloc、vmalloc、malloc等方式,不过这几个是最常见的。了解这些申请内存的方式和底层原理,有助于我们可以更好地设计出高效的程序。最后,想要了解更深的内存申请底层技术实现,可以阅读相关的一些书籍或参考源码进行理解~
如果觉得有用请点个免费的赞,您的支持就是我最大的动力,这对我很重要!!!