鸿蒙内核源码分析 (内存管理篇) | 虚拟内存全景图是怎样的

初始化整个内存

OsSysMemInit
    OsMain
        main

从 main() 跟踪可看内存部分初始化是在 OsSysMemInit() 中完成的。

UINT32 OsSysMemInit(VOID)
{
    STATUS_T ret;

    OsKSpaceInit();//内核空间初始化

    ret = OsKHeapInit(OS_KHEAP_BLOCK_SIZE);// 内核动态内存初始化 512K   
    if (ret != LOS_OK) {
        VM_ERR("OsKHeapInit fail");
        return LOS_NOK;
    }

    OsVmPageStartup();// page初始化
    OsInitMappingStartUp();// 映射初始化

    ret = ShmInit();// 共享内存初始化
    if (ret < 0) {
        VM_ERR("ShmInit fail");  
        return LOS_NOK;
    }

    return LOS_OK;
}

鸿蒙虚拟内存整体布局图

[图片上传失败…(image-6e54a-1712499300486)]

// HarmonyOS 内核空间包含以下各段:
extern CHAR __int_stack_start; // 运行系统函数栈的开始地址
extern CHAR __rodata_start;  // ROM开始地址 只读
extern CHAR __rodata_end;  // ROM结束地址
extern CHAR __bss_start;  // bss开始地址
extern CHAR __bss_end;   // bss结束地址
extern CHAR __text_start;  // 代码区开始地址
extern CHAR __text_end;   // 代码区结束地址
extern CHAR __ram_data_start; // RAM开始地址 可读可写
extern CHAR __ram_data_end;  // RAM结束地址
extern UINT32 __heap_start;  // 堆区开始地址
extern UINT32 __heap_end;  // 堆区结束地址

内存一开始一张白纸,这些 extern 就是给它画大界线的,从哪到哪是属于什么段。这些值大小取决实际项目内存条的大小,不同的内存条,地址肯定会不一样,所以必须由外部提供,鸿蒙内核采用了 Linux 的段管理方式。结合上图对比以下的解释自行理解下位置。

BSS 段 (bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS 是英文 Block Started by Symbol 的简称。BSS 段属于静态内存分配。该段用于存储未初始化的全局变量或者是默认初始化为 0 的全局变量,它不占用程序文件的大小,但是占用程序运行时的内存空间。

data 段 该段用于存储初始化的全局变量,初始化为 0 的全局变量出于编译优化的策略还是被保存在 BSS 段。

细心的读者可能发现了,鸿蒙内核几乎所有的全局变量都没有赋初始化值或 NULL,这些变量经过编译后是放在了 BSS 段的,运行时占用内存空间,如此编译出来的 ELF 包就变小了。

.rodata 段,该段也叫常量区,用于存放常量数据,ro 就是 Read Only 之意。

text 段 是用于存放程序代码的,编译时确定,只读。更进一步讲是存放处理器的机器指令,当各个源文件单独编译之后生成目标文件,经连接器链接各个目标文件并解决各个源文件之间函数的引用,与此同时,还得将所有目标文件中的.text 段合在一起。

stack 栈段,是由系统负责申请释放,用于存储参数变量及局部变量以及函数的执行。

heap 段 它由用户申请和释放,申请时至少分配虚存,当真正存储数据时才分配相应的实存,释放时也并非立即释放实存,而是可能被重复利用。

内核空间是怎么初始化的?

LosMux g_vmSpaceListMux;//虚拟空间互斥锁,一般和g_vmSpaceList配套使用
LOS_DL_LIST_HEAD(g_vmSpaceList);//g_vmSpaceList把所有虚拟空间挂在一起,
LosVmSpace g_kVmSpace;    //内核空间地址
LosVmSpace g_vMallocSpace;//虚拟分配空间地址

//鸿蒙内核空间有两个(内核进程空间和内核动态分配空间),共用一张L1页表
VOID OsKSpaceInit(VOID)
{
    OsVmMapInit();// 初始化互斥量
    OsKernVmSpaceInit(&g_kVmSpace, OsGFirstTableGet());// 初始化内核虚拟空间,OsGFirstTableGet 为L1表基地址
    OsVMallocSpaceInit(&g_vMallocSpace, OsGFirstTableGet());// 初始化动态分配区虚拟空间,OsGFirstTableGet 为L1表基地址
}//g_kVmSpace g_vMallocSpace 共用一个L1页表
//初始化内核堆空间
STATUS_T OsKHeapInit(size_t size)
{
    STATUS_T ret;
    VOID *ptr = NULL;
    /*
     * roundup to MB aligned in order to set kernel attributes. kernel text/code/data attributes
     * should page mapping, remaining region should section mapping. so the boundary should be
     * MB aligned.
     */
     //向上舍入到MB对齐是为了设置内核属性。内核文本/代码/数据属性应该是页映射,其余区域应该是段映射,所以边界应该对齐。
    UINTPTR end = ROUNDUP(g_vmBootMemBase + size, MB);//用M是因为采用section mapping 鸿蒙内核源码分析(内存映射篇)有阐述
    size = end - g_vmBootMemBase;
    //ROUNDUP(0x00000200+512,1024) = 1024  ROUNDUP(0x00000201+512,1024) = 2048 此处需细品! 
    ptr = OsVmBootMemAlloc(size);//因刚开机,使用引导分配器分配
    if (!ptr) {
        PRINT_ERR("vmm_kheap_init boot_alloc_mem failed! %d\n", size);
        return -1;
    }

    m_aucSysMem0 = m_aucSysMem1 = ptr;//内存池基地址,取名auc还用0和1来标识有何深意,一直没整明白, 哪位大神能告诉下?
    ret = LOS_MemInit(m_aucSysMem0, size);//初始化内存池
    if (ret != LOS_OK) {
        PRINT_ERR("vmm_kheap_init LOS_MemInit failed!\n");
        g_vmBootMemBase -= size;//分配失败时需归还size, g_vmBootMemBase是很野蛮粗暴的
        return ret;
    }
    LOS_MemExpandEnable(OS_SYS_MEM_ADDR);//地址可扩展
    return LOS_OK;
}

内核空间用了三个全局变量,其中一个是互斥 LosMux,IPC 部分会详细讲,这里先不展开。 比较有意思的是 LOS_DL_LIST_HEAD,看内核源码过程中经常会为这样的代码点头称赞,会心一笑。点赞!

#define LOS_DL_LIST_HEAD(list) LOS_DL_LIST list = { &(list), &(list) }

Page 是如何初始化的?

page 是映射的最小单位,是物理地址 <—> 虚拟地址映射的数据结构的基础

// page初始化
VOID OsVmPageStartup(VOID)
{
    struct VmPhysSeg *seg = NULL;
    LosVmPage *page = NULL;
    paddr_t pa;
    UINT32 nPage;
    INT32 segID;

    OsVmPhysAreaSizeAdjust(ROUNDUP((g_vmBootMemBase - KERNEL_ASPACE_BASE), PAGE_SIZE));//校正 g_physArea size

    nPage = OsVmPhysPageNumGet();//得到 g_physArea 总页数
    g_vmPageArraySize = nPage * sizeof(LosVmPage);//页表总大小
    g_vmPageArray = (LosVmPage *)OsVmBootMemAlloc(g_vmPageArraySize);//申请页表存放区域

    OsVmPhysAreaSizeAdjust(ROUNDUP(g_vmPageArraySize, PAGE_SIZE));// g_physArea 变小

    OsVmPhysSegAdd();// 段页绑定
    OsVmPhysInit();// 加入空闲链表和设置置换算法,LRU(最近最久未使用)算法

    for (segID = 0; segID < g_vmPhysSegNum; segID++) {
        seg = &g_vmPhysSeg[segID];
        nPage = seg->size >> PAGE_SHIFT;
        for (page = seg->pageBase, pa = seg->start; page <= seg->pageBase + nPage;
             page++, pa += PAGE_SIZE) {
            OsVmPageInit(page, pa, segID);//page初始化
        }
        OsVmPageOrderListInit(seg->pageBase, nPage);// 页面分配的排序
    }
}

进程是如何申请内存的?

进程的主体是来自进程池,进程池是统一分配的,怎么创建进程池的去翻系列篇里的文章,所以创建一个进程的时候只需要分配虚拟内存 LosVmSpace,这里要分内核模式和用户模式下的申请。

//初始化进程的 用户空间 或 内核空间
//初始化PCB块
STATIC UINT32 OsInitPCB(LosProcessCB *processCB, UINT32 mode, UINT16 priority, UINT16 policy, const CHAR *name)
{
    UINT32 count;
    LosVmSpace *space = NULL;
    LosVmPage *vmPage = NULL;
    status_t status;
    BOOL retVal = FALSE;

    processCB->processMode = mode;//用户态进程还是内核态进程
    processCB->processStatus = OS_PROCESS_STATUS_INIT;//进程初始状态
    processCB->parentProcessID = OS_INVALID_VALUE;//爸爸进程,外面指定
    processCB->threadGroupID = OS_INVALID_VALUE;//所属线程组
    processCB->priority = priority;//优先级
    processCB->policy = policy;//调度算法 LOS_SCHED_RR
    processCB->umask = OS_PROCESS_DEFAULT_UMASK;//掩码
    processCB->timerID = (timer_t)(UINTPTR)MAX_INVALID_TIMER_VID;

    LOS_ListInit(&processCB->threadSiblingList);//初始化任务/线程链表
    LOS_ListInit(&processCB->childrenList);        //初始化孩子链表
    LOS_ListInit(&processCB->exitChildList);    //初始化记录哪些孩子退出了的链表    
    LOS_ListInit(&(processCB->waitList));        //初始化等待链表

    for (count = 0; count < OS_PRIORITY_QUEUE_NUM; ++count) { //根据 priority数 创建对应个数的队列
        LOS_ListInit(&processCB->threadPriQueueList[count]);  
    }

    if (OsProcessIsUserMode(processCB)) {// 是否为用户态进程
        space = LOS_MemAlloc(m_aucSysMem0, sizeof(LosVmSpace));
        if (space == NULL) {
            PRINT_ERR("%s %d, alloc space failed\n", __FUNCTION__, __LINE__);
            return LOS_ENOMEM;
        }
        VADDR_T *ttb = LOS_PhysPagesAllocContiguous(1);//分配一个物理页用于存储L1页表 4G虚拟内存分成 (4096*1M)
        if (ttb == NULL) {//这里直接获取物理页ttb
            PRINT_ERR("%s %d, alloc ttb or space failed\n", __FUNCTION__, __LINE__);
            (VOID)LOS_MemFree(m_aucSysMem0, space);
            return LOS_ENOMEM;
        }
        (VOID)memset_s(ttb, PAGE_SIZE, 0, PAGE_SIZE);
        retVal = OsUserVmSpaceInit(space, ttb);//初始化虚拟空间和本进程 mmu
        vmPage = OsVmVaddrToPage(ttb);//通过虚拟地址拿到page
        if ((retVal == FALSE) || (vmPage == NULL)) {//异常处理
            PRINT_ERR("create space failed! ret: %d, vmPage: %#x\n", retVal, vmPage);
            processCB->processStatus = OS_PROCESS_FLAG_UNUSED;//进程未使用,干净
            (VOID)LOS_MemFree(m_aucSysMem0, space);//释放虚拟空间
            LOS_PhysPagesFreeContiguous(ttb, 1);//释放物理页,4K
            return LOS_EAGAIN;
        }
        processCB->vmSpace = space;//设为进程虚拟空间
        LOS_ListAdd(&processCB->vmSpace->archMmu.ptList, &(vmPage->node));//将空间映射页表挂在 空间的mmu L1页表, L1为表头
    } else {
        processCB->vmSpace = LOS_GetKVmSpace();//内核共用一个虚拟空间,内核进程 常驻内存
    }

#ifdef LOSCFG_SECURITY_VID
    status = VidMapListInit(processCB);
    if (status != LOS_OK) {
        PRINT_ERR("VidMapListInit failed!\n");
        return LOS_ENOMEM;
    }
#endif
#ifdef LOSCFG_SECURITY_CAPABILITY
    OsInitCapability(processCB);
#endif

    if (OsSetProcessName(processCB, name) != LOS_OK) {
        return LOS_ENOMEM;
    }

    return LOS_OK;
}
LosVmSpace *LOS_GetKVmSpace(VOID)
{
    return &g_kVmSpace;
}

从代码可以看出,内核空间固定只有一个 g_kVmSpace,而每个用户进程的虚拟内存空间都是独立的。请细品!

task 是如何申请内存的?

task 的主体是来自进程池,task 池是统一分配的,怎么创建 task 池的去翻系列篇里的文章。这里 task 只需要申请 stack 空间,还是直接上看源码吧,用 OsUserInitProcess 函数看应用程序的 main () 是如何被内核创建任务和运行的。

//所有的用户进程都是使用同一个用户代码段描述符和用户数据段描述符,它们是__USER_CS和__USER_DS,也就是每个进程处于用户态时,它们的CS寄存器和DS寄存器中的值是相同的。当任何进程或者中断异常进入内核后,都是使用相同的内核代码段描述符和内核数据段描述符,它们是__KERNEL_CS和__KERNEL_DS。这里要明确记得,内核数据段实际上就是内核态堆栈段。
LITE_OS_SEC_TEXT_INIT UINT32 OsUserInitProcess(VOID)
{
    INT32 ret;
    UINT32 size;
    TSK_INIT_PARAM_S param = { 0 };
    VOID *stack = NULL;
    VOID *userText = NULL;
    CHAR *userInitTextStart = (CHAR *)&__user_init_entry;//代码区开始位置 ,所有进程
    CHAR *userInitBssStart = (CHAR *)&__user_init_bss;// 未初始化数据区(BSS)。在运行时改变其值
    CHAR *userInitEnd = (CHAR *)&__user_init_end;// 结束地址
    UINT32 initBssSize = userInitEnd - userInitBssStart;
    UINT32 initSize = userInitEnd - userInitTextStart;

    LosProcessCB *processCB = OS_PCB_FROM_PID(g_userInitProcess);
    ret = OsProcessCreateInit(processCB, OS_USER_MODE, "Init", OS_PROCESS_USERINIT_PRIORITY);// 初始化用户进程,它将是所有应用程序的父进程
    if (ret != LOS_OK) {
        return ret;
    }

    userText = LOS_PhysPagesAllocContiguous(initSize >> PAGE_SHIFT);// 分配连续的物理页
    if (userText == NULL) {
        ret = LOS_NOK;
        goto ERROR;
    }

    (VOID)memcpy_s(userText, initSize, (VOID *)&__user_init_load_addr, initSize);// 安全copy 经加载器load的结果 __user_init_load_addr -> userText
    ret = LOS_VaddrToPaddrMmap(processCB->vmSpace, (VADDR_T)(UINTPTR)userInitTextStart, LOS_PaddrQuery(userText),
                               initSize, VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE |
                               VM_MAP_REGION_FLAG_PERM_EXECUTE | VM_MAP_REGION_FLAG_PERM_USER);// 虚拟地址与物理地址的映射
    if (ret < 0) {
        goto ERROR;
    }

    (VOID)memset_s((VOID *)((UINTPTR)userText + userInitBssStart - userInitTextStart), initBssSize, 0, initBssSize);// 除了代码段,其余都清0

    stack = OsUserInitStackAlloc(g_userInitProcess, &size);// 初始化堆栈区
    if (stack == NULL) {
        PRINTK("user init process malloc user stack failed!\n");
        ret = LOS_NOK;
        goto ERROR;
    }

    param.pfnTaskEntry = (TSK_ENTRY_FUNC)userInitTextStart;// 从代码区开始执行,也就是应用程序main 函数的位置
    param.userParam.userSP = (UINTPTR)stack + size;// 指向栈底
    param.userParam.userMapBase = (UINTPTR)stack;// 栈顶
    param.userParam.userMapSize = size;// 栈大小
    param.uwResved = OS_TASK_FLAG_PTHREAD_JOIN;// 可结合的(joinable)能够被其他线程收回其资源和杀死
    ret = OsUserInitProcessStart(g_userInitProcess, &param);// 创建一个任务,来运行main函数
    if (ret != LOS_OK) {
        (VOID)OsUnMMap(processCB->vmSpace, param.userParam.userMapBase, param.userParam.userMapSize);
        goto ERROR;
    }

    return LOS_OK;

ERROR:
    (VOID)LOS_PhysPagesFreeContiguous(userText, initSize >> PAGE_SHIFT);//释放物理内存块
    OsDeInitPCB(processCB);//删除PCB块
    return ret;
}

所有的用户进程都是通过 init 进程 fork 来的, 可以看到创建进程的同时创建了一个 task, 入口函数就是代码区的第一条指令,也就是应用程序 main 函数。这里再说下 stack 的大小,不同空间下的 task 栈空间是不一样的,鸿蒙内核中有三种栈空间 size,如下

#define LOSCFG_BASE_CORE_TSK_IDLE_STACK_SIZE SIZE(0x800)//内核进程,运行在内核空间2K
#define OS_USER_TASK_SYSCALL_SATCK_SIZE 0x3000 //用户进程,通过系统调用创建的task运行在内核空间的 12K
#define OS_USER_TASK_STACK_SIZE         0x100000//用户进程运行在用户空间的1M

为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙开发学习手册》:

如何快速入门:https://qr21.cn/FV7h05

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. ……

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ……

基于ArkTS 开发:https://qr21.cn/FV7h05

  1. Ability开发
  2. UI开发
  3. 公共事件与通知
  4. 窗口管理
  5. 媒体
  6. 安全
  7. 网络与链接
  8. 电话服务
  9. 数据管理
  10. 后台任务(Background Task)管理
  11. 设备管理
  12. 设备使用信息统计
  13. DFX
  14. 国际化开发
  15. 折叠屏系列
  16. ……

鸿蒙开发面试真题(含参考答案):https://qr18.cn/F781PH

鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH

1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向

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

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

相关文章

Centos7下docker删除容器与镜像

个人记录 查看容器 docker ps -a停止容器运行 docker stop jenkins卸载容器 docker rm jenkins查看镜像 docker images卸载镜像 docker rmi IMAGE ID查看容器与镜像是否卸载完毕 docker images docker ps -a

Vue基础知识:Vue路由——重定向,以及?(可选符)的使用

当网页打开时&#xff0c;url默认是/路径&#xff08;根路径&#xff09;&#xff0c;未匹配到组件时&#xff0c;会出现空白&#xff0c;为了解决这个问题 就要利用重定向 重定向&#xff1a;匹配path后&#xff0c;强制跳转path路径 重定向的语法&#xff1a; {path:匹配…

k8s安全控制、授权管理介绍,全网最新

3.ABAC 4.Webhook 5.Node 6.RBAC 三.Role解释 1.Role和ClusterRole 2.Rolebinding和ClusterBinding 3.Rolebinding和ClusterRole 四.准入控制 1.命令格式 2.可配置控制器 五.例子 1.生成签署证书 2.设置用户和上下文信息 3.为sulibao用户授权 一.Kubernetes安全控…

真实对比kimi、通义千问、文心一言的写代码能力,到底谁强?

&#x1f916;AI改变生活&#xff1a;最近都在说月之暗面的kimi的各项能力吊打国内其他大模型&#xff0c;今天我们真实感受下 kimi、通义千问、文心一言的根据需求写代码的能力。 测评结果让人震惊&#xff01; kimi kimi编程过程 我们先看一下热捧的月之暗面的kimi模型。 …

Spring之BeanFactoryPostProcessor详解

目录 功能与作用 使用案例 spring提供的常见BeanFactoryPostProcessor 1.EventListenerMethodProcessor 2.BeanDefinitionRegistryPostProcessor 功能与作用 使用案例 spring提供的唯一BeanDefinitionRegistryPostProcessor 总结 功能与作用 参考BeanFactoryPostProce…

[StartingPoint][Tier1]Pennyworth

Important Jenkins是一个用于自动化构建、测试和部署软件项目的开源持续集成和持续部署&#xff08;CI/CD&#xff09;工具。它允许开发团队自动执行和监控在软件开发过程中的重复性任务&#xff0c;例如构建代码、运行测试、部署应用程序等。Jenkins提供了一个易于使用的Web界…

力扣---分隔链表

给你一个链表的头节点 head 和一个特定值 x &#xff0c;请你对链表进行分隔&#xff0c;使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。 你应当 保留 两个分区中每个节点的初始相对位置。 示例 1&#xff1a; 输入&#xff1a;head [1,4,3,2,5,2], x 3 输出&a…

【Java基础】面试题汇总

Java基础面试题1. JVM vs JDK vs JRE 2. 什么是字节码?采用字节码的好处是什么?3. 为什么说 Java 语言“编译与解释并存”&#xff1f;4. AOT 有什么优点&#xff1f;为什么不全部使用 AOT 呢&#xff1f;5. Java 和 C 的区别&#xff1f;6. Java 中的基本数据类型&#xff1…

算法第三十九天-验证二叉树的前序序列化

验证二叉树的前序序列化 题目要求 解题思路 方法一&#xff1a;栈 栈的思路是「自底向上」的想法。下面要结合本题是「前序遍历」这个重要特点。 我们知道「前序遍历」是按照「根节点-左子树-右子树」的顺序遍历的&#xff0c;只有当根节点的所有左子树遍历完成之后&#xf…

代码随想录第32天|455.分发饼干 376. 摆动序列

理论基础 贪心算法核心&#xff1a;选择每一阶段的局部最优&#xff0c;从而达到全局最优。 455.分发饼干 455. 分发饼干 - 力扣&#xff08;LeetCode&#xff09;代码随想录 (programmercarl.com)455. 分发饼干 - 力扣&#xff08;LeetCode&#xff09; 贪心算法理论基础&am…

LeetCode:1483. 树节点的第 K 个祖先(倍增 Java)

目录 1483. 树节点的第 K 个祖先 题目描述&#xff1a; 实现代码与解析&#xff1a; 倍增 原理思路&#xff1a; 1483. 树节点的第 K 个祖先 题目描述&#xff1a; 给你一棵树&#xff0c;树上有 n 个节点&#xff0c;按从 0 到 n-1 编号。树以父节点数组的形式给出&#…

【Vue】Vue3中的OptionsAPI与CompositionAPI

文章目录 OptionsAPICompositionAPI对比总结 OptionsAPI 中文名:选项式API通过定义methods,computed,watch,data等属性方法&#xff0c;处理页面逻辑。以下是OptionsAPI代码结构 实例代码: <script lang"ts">// js或者tsimport { defineComponent } from vu…

HubSpot如何通过自动化和优化客户服务流程,提升客户满意度和忠诚度?

HubSpot通过自动化和优化客户服务流程&#xff0c;可以有效地提升客户满意度和忠诚度。以下是HubSpot实现这一目标的几个关键步骤&#xff1a; 建立清晰的服务流程&#xff1a;首先&#xff0c;HubSpot帮助企业建立清晰、标准化的客户服务流程。这包括明确的服务阶段定义&…

git分支 - 分支简介

分支 几乎每个版本控制系统都有某种形式的分支支持。分支意味着偏离了主开发线&#xff0c;并继续进行工作&#xff0c;而不会影响到主开发线。在许多版本控制系统工具中&#xff0c;这是一个比较昂贵的过程&#xff0c;通常需要创建源代码目录的一个新副本&#xff0c;对于大…

第7章 数据安全

思维导图 7.1 引言 数据安全包括安全策略和过程的规划、建立与执行&#xff0c;为数据和信息资产提供正确的身份验证、授权、访问和审计。虽然数据安全的详细情况(如哪些数据需要保护)因行业和国家有所不同&#xff0c;但是数据安全实践的目标是相同的&#xff0c;即根据隐私和…

链表之双向链表的实现

铁汁们大家好&#xff0c;我们上一篇博客学习了单链表&#xff0c;这节课让我们继续往深学习&#xff0c;学习一下双线链表&#xff0c;话不多说&#xff0c;我们开始吧&#xff01; 目录 1.双向链表 2.顺序表和链表的优缺点 3.双向链表的实现 1.双向链表 1.我们要实现的双线…

Elasticsearch快速上手

基本概念 索引&#xff08;Index&#xff09; 索引是文档的容器&#xff0c;就像关系数据库中&#xff0c;要存储行记录必须先创建数据库和表一样。 类型&#xff08;Type&#xff09; ES6 及之前的版本还存在”类型“的概念&#xff0c;一个索引下可以存储多个类型的文档&am…

探索数据结构:特殊的双向队列

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;数据结构与算法 贝蒂的主页&#xff1a;Betty’s blog 1. 双向队列的定义 **双向队列(double‑ended queue)**是一种特殊的队列…

Ant Design Pro | 前端项目初始化

初始化项目 环境确认 这里使用的版本如下&#xff1a; 新建文件夹&#xff08;fapi&#xff09; 执行项目初始化命令 cmd进入命令行执行项目初始化命令&#xff0c;参考官网https://pro.ant.design/zh-CN/ # 使用 npm npm i ant-design/pro-cli -g # fapi-frontend是项目…

树莓派固件烧录教程(2024)

一、烧录工具准备 硬件准备&#xff1a; 16G及以上TF卡和读卡器&#xff0c;TF卡建议高速卡&#xff08;卡的读写速度直接影响树莓派的运行速度&#xff09;。 软件准备&#xff1a;&#xff08;下面二方法选其一即可&#xff09; 方法1&#xff1a;raspberry官方烧录工具R…