一起读《奔跑吧Linux内核(第2版)卷1:基础架构》- 了解kmalloc、vmalloc、malloc

发布时间:2024年01月22日

大家好,我是硬核王同学,最近在做免费的嵌入式知识分享,帮助对嵌入式感兴趣的同学学习嵌入式、做项目、找工作!

移步飞书获得更好阅读体验:

Docs

Hello,大家好我是硬核王同学,是一名刚刚工作一年多的Linux工程师,很感谢EEWorld的本次活动,让我有机会参与评测这本和Linux内核相关的的这本书。

在计算机编程中,内存分配是程序设计中的关键问题之一,而kmalloc、vmalloc、malloc和new是常用的内存申请方式。kmalloc和vmalloc主要用于操作系统内核空间中的内存分配,而malloc和new则主要用于应用程序中的内存分配。本文将一起了解下这几种常用的内存申请方式及其特点。

一、kmalloc

(1)定义和特点

kmalloc是Linux内核中提供的用于分配内核空间中连续内存的函数。

其特点如下:

  1. 连续内存分配:kmalloc分配的内存是连续的,适合需要对连续内存进行操作的场景。
  2. 小内存块:kmalloc适用于分配小内存块,一般最大限制是128KB。
  3. 物理内存映射: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主要用于在内核空间中分配内存块,适用于以下情况:

  1. 内核模块开发:在编写内核模块时,可能需要动态分配内存来存储数据结构、缓冲区或其他临时数据。
  2. 驱动程序开发:在编写设备驱动程序时,可能需要分配内存来管理设备的状态、接收和发送数据等。
  3. 内核组件开发:在开发或修改内核的组件时,可能需要分配内存来实现特定的功能或数据结构。
  4. 内核线程和进程:内核线程或进程可能需要分配内存来存储上下文、堆栈或其他数据。

kmalloc的限制包括:

  1. 仅能在内核空间中使用:kmalloc函数只能在内核空间中使用,无法直接在用户空间中调用。
  2. 内存大小限制:kmalloc对分配的内存块大小有一定的限制,具体限制取决于系统设置和内核版本。
  3. 内存泄漏风险: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有以下特点:

  1. 分配虚拟内存:vmalloc函数用于在内核空间中分配虚拟内存块,可以跨越多个物理页,不要求物理页是连续的。
  2. 动态内存分配:vmalloc函数可以根据需要动态地分配内存块,而不需要在编译时指定内存大小。
  3. 适用于大内存分配: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主要用于在内核空间中分配虚拟内存块,适用于以下情况:

  1. 大内存分配:当需要分配大量连续内存时,但物理页不一定需要连续时,可以使用vmalloc函数。
  2. 驱动程序开发:在编写设备驱动程序时,可能需要分配大量内存来存储数据缓冲区或其他大型数据结构。
  3. DMA操作:在进行DMA操作时,可能需要在内核空间分配虚拟内存来缓存或管理DMA数据。

vmalloc的限制包括:

  1. 仅能在内核空间中使用:vmalloc函数只能在内核空间中使用,无法直接在用户空间中调用。
  2. 性能开销:vmalloc分配的内存块不一定在物理上是连续的,并且可能跨越多个物理页,这可能导致性能开销,特别是在访问大量虚拟内存时。
  3. 物理页大小限制: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函数的三个特点:

  1. 动态内存分配:malloc函数可以在程序运行时动态地分配所需的内存空间。这意味着可以根据实际需求来动态调整内存分配的大小,而不需要提前知道需要多大的内存空间。
  2. 返回指向分配内存的指针:malloc函数会返回一个指向分配内存空间的指针,可以将该指针赋值给指针变量,并通过该指针访问和操作已分配的内存。
  3. 不会初始化内存空间:malloc函数不会对分配的内存空间进行初始化。分配的内存单元中可能包含之前使用过的数据。因此,在使用分配的内存空间之前,需要手动进行初始化操作,以确保数据的正确性和一致性。

(2)底层实现机制

malloc底层实现有两种情况:

  1. 当开辟的空间小于 128K 时,调用 brk()函数,malloc 的底层实现是系统调用函数 brk(),其主要移动指针 _enddata(此时的 _enddata 指的是 Linux 地址空间中堆段的末尾地址,不是数据段的末尾地址)
  2. 当开辟的空间大于 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函数适用于以下情况:

  1. 需要动态分配内存空间的情况。
  2. 需要在函数返回后仍然有效的内存空间。

malloc函数的主要限制有:

  1. 分配的内存空间必须是连续的,因此可能会造成内存碎片的问题。
  2. 分配的内存空间不会初始化,可能包含未初始化的数据。
  3. 必须确保及时释放已分配的内存空间,否则可能导致内存泄漏问题。

(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函数释放了已分配的内存空间,以避免内存泄漏问题。

四、结论

  1. kmalloc函数是在内核中动态分配一块指定大小的连续内存区域。主要用于内核代码中的动态内存分配。由于需要分配连续的内存区域,所以适用于需要访问连续内存区域的数据结构,如需要进行连续存储、读写操作的缓冲区或数据块。
  2. vmalloc函数是在内核中动态分配一块指定大小的虚拟内存区域。主要用于内核代码中的动态内存分配。虚拟内存区域不要求连续,所以适用于需要大块内存但不要求连续性的数据结构,如大数组、大缓冲区、映射物理设备等。
  3. malloc函数是在用户空间动态分配一块指定大小的连续内存区域。主要用于用户空间的动态内存分配。适用于需要分配一块连续内存区域用于存储数据的情况,如动态创建数组、链表、字符串等。

在Linux系统中有很多申请内存的方式,不仅仅只有kmalloc、vmalloc、malloc等方式,不过这几个是最常见的。了解这些申请内存的方式和底层原理,有助于我们可以更好地设计出高效的程序。最后,想要了解更深的内存申请底层技术实现,可以阅读相关的一些书籍或参考源码进行理解~

如果觉得有用请点个免费的赞,您的支持就是我最大的动力,这对我很重要!!!

文章来源:https://blog.csdn.net/qq_56921854/article/details/135743004
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。