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

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

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

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等方式,不过这几个是最常见的。了解这些申请内存的方式和底层原理,有助于我们可以更好地设计出高效的程序。最后,想要了解更深的内存申请底层技术实现,可以阅读相关的一些书籍或参考源码进行理解~

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/339465.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

开源无代码应用程序生成器Saltcorn

什么是 Saltcorn &#xff1f; Saltcorn 是一个无需编写任何代码即可构建数据库 Web 应用程序的平台。它配备了一个吸睛的仪表板&#xff0c;丰富的生态系统、视图生成器以及支持主题的界面&#xff0c;使用直观的点击、拖放用户界面来构建整个应用程序。 软件的特点&#xff1…

上位机图像处理和嵌入式模块部署(流程)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 前面我们说过&#xff0c;传统图像处理的方法&#xff0c;一般就是pccamera的处理方式。camera本身只是提供基本的raw data数据&#xff0c;所有的…

SpringBoot - SpringBoot手写模拟SpringBoot启动过程

依赖 建一个工程&#xff0c;两个Module: 1. springboot模块&#xff0c;表示springboot框架的源码实现 2. user包&#xff0c;表示用户业务系统&#xff0c;用来写业务代码来测试我们所模拟出来的SpringBoot 首先&#xff0c;SpringBoot是基于的Spring&#xff0c;所以我…

140:leaflet加载here地图(v2软件多种形式)

第140个 点击查看专栏目录 本示例介绍如何在vue+leaflet中添加HERE地图(v2版本的软件),并且含多种的表现形式。包括地图类型,文字标记的设置、语言的选择、PPI的设定。 v3版本和v2版本有很大的区别,关键是引用方法上,请参考文章尾部的API链接。 直接复制下面的 vue+leaf…

spring boot学习第八篇:kafka监听消费

为了实现监听器功能 pom.xml文件内容如下&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLoc…

开发者的瑞士军刀!一款适用于开发者的工具集合!

大家好&#xff0c;我是 Java陈序员。 俗话说“工欲善其事必先利其器”&#xff0c;有一个好的工具可以事半功倍。 编程开发亦是如此。 今天&#xff0c;给大家介绍一款离线的 Windows 应用程序&#xff0c;该应用涵盖常见的开发工具集合&#xff0c;旨在提高工作效率&#…

【Coding】寒假每日一题Day.5.三国游戏

题目来源 题目来自于AcWing平台&#xff1a;https://www.acwing.com/problem/content/description/4968/。 以blog的形式记录程序设计算法学习的过程&#xff0c;仅做学习记录之用。 题目描述 输入输出格式与数据范围 样例 思路 思路参考自题解&#xff1a;https://www.acwi…

Maven 打包时,依赖配置正确,但是类引入出现错误,一般是快照(Snapshot)依赖拉取策略问题

问题描述&#xff1a; 项目打包时&#xff0c;类缺少依赖&#xff0c;操作 pom.xml -> Maven -> Reload project &#xff0c;还是不生效&#xff0c;但是同事&#xff08;别人&#xff09;那里正常。 问题出现的环境&#xff1a; 可能项目是多模块项目&#xff0c;结构…

css中>>>、/deep/、::v-deep的作用和区别,element-ui自定义样式

文章目录 一、前言1.1、/deep/1.2、::v-deep1.3、>>> 二、区别三、总结四、最后 一、前言 1.1、/deep/ 在style经常用scoped属性实现组件的私有化时&#xff0c;要改变element-ui某个深层元素&#xff08;例如.el-input__inner&#xff09;或其他深层样式时&#xf…

深度学习基础之数据操作

深度学习中最常用的数据是张量&#xff0c;对张量进行操作是进行深度学习的基础。以下是对张量进行的一些操作&#xff1a; 首先我们需要先导入相关的张量库torch。 元素构造&#xff08;初始化&#xff09; 使用arange创造一个行向量&#xff0c;也就是0轴&#xff08;0维&a…

中断——外部中断EXIT

终端可以分成外部中断和内部中断吗 文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 野火中断章节有这样一句话 【F103在内核水平上搭载了一个异常响应系统&#xff0c; 支持为数众多的系统异常和外部中断。 其中系统异常有8个&#xff…

学校服务器hpc东南大学,下载国家基因组科技中心数据 gsa-human ascp chatpt建议 Linux系统中写代码

使用ascp批量下载数据 You files.csv 帮我写个批量下载的脚本&#xff0c;批量下载时候&#xff0c;把路径中最后的HRR659816批量替换成 Accession列的内容就行了。下面是示例 ascp -v -QT -l 300m -P33001 -k1 -i ~/.aspera/connect/etc/aspera01.openssh_for_gsa -d asper…

HNU-数据挖掘-实验3-图深度学习

数据挖掘课程实验实验3 图深度学习 计科210X 甘晴void 202108010XXX 文章目录 数据挖掘课程实验<br>实验3 图深度学习实验背景实验要求数据集解析实验内容&#xff08;0&#xff09;基础知识&#xff1a;基于图的深度学习方法浅识&#xff1a;图卷积网络 (GCN)浅识&…

【机组】微程序控制单元实验的解密与实战

​&#x1f308;个人主页&#xff1a;Sarapines Programmer&#x1f525; 系列专栏&#xff1a;《机组 | 模块单元实验》⏰诗赋清音&#xff1a;云生高巅梦远游&#xff0c; 星光点缀碧海愁。 山川深邃情难晤&#xff0c; 剑气凌云志自修。 ​ 目录 &#x1f33a;一、 实验目…

HTML以及CSS相关知识总结(一)

近日就开始回顾html和css相关知识啦&#xff0c;并且会学习html5和css3的新知识&#xff0c;以下是我对记忆不太深刻的地方以及新知识点的总结&#xff1a; Web标准&#xff1a; 结构&#xff1a;用于对网页元素进行整理和分类&#xff0c;即HTML 表现&#xff1a;用于设置网页…

计算机的受信任平台模块出现故障,错误代码 80090016

在一次修改 MicroSoft 365 密码后&#xff0c;本地登录Teams出现错误&#xff1a; 计算机的受信任平台模块出现故障。如果此错误仍然存在&#xff0c;请与系统管理员联系&#xff0c;并提供错误代码80090016。 详细信息&#xff1a;https::/www.microsoft.com/wamerrors Teams…

OpenAI的GPT接口的调用流程

要调用OpenAI的GPT接口&#xff0c;您需要获得API密钥&#xff0c;并使用HTTP请求发送请求。以下是一般的步骤&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1.获取OpenAI API密钥&#xff1a; 在使…

Threejs实现立体3D园区解决方案及代码

一、实现方案 单独贴代码可能容易混乱&#xff0c;所以这里只讲实现思路&#xff0c;代码放在最后汇总了下。 想要实现一个简单的工业园区、主要包含的内容是一个大楼、左右两片停车位、四条道路以及多个可在道路上随机移动的车辆、遇到停车位时随机选择是否要停车&#xff0…

【MATLAB源码-第121期】基于matlab的斑马优化算法(ZOA)机器人栅格路径规划,输出做短路径图和适应度曲线。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 斑马优化算法&#xff08;Zebra Optimization Algorithm&#xff0c;简称ZOA&#xff09;是一种模仿斑马群体行为的优化算法。在自然界中&#xff0c;斑马是一种社会性很强的动物&#xff0c;它们具有独特的群体行为模式&…

精品基于Uniapp+springboot菜谱美食饮食健康管理App

《[含文档PPT源码等]精品基于Uniappspringboot饮食健康管理App》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程、包运行成功&#xff01; 软件开发环境及开发工具&#xff1a; 开发语言&#xff1a;Java 后台框架&#xff1a;springboot、ssm 安卓…
最新文章