手写简易操作系统(十四)--内存管理系统

前情提要

前面我们实现了一个简单的C库,现在我们将实现一个内存池。

之前我们的内存都是自己规划的,我们需要 0xc0001500 这个地址,就将程序放在哪儿。但是程序多了怎么办?还需要我们自己去一个一个安排位置吗,有一块不要了怎么办,这一块空出来的怎么回收,这些需求就导致了内存池的诞生。

一、内存池概述

在分页机制下程序中的地址都是虚拟地址,虚拟地址的范围取决于地址总线的宽度,咱们是在32位环境下,所以虚拟地址空间为4GB。除了地址空间比较大以外,分页机制的另一个好处是每个任务都有自己的 4GB 虚拟地址空间,也就是各程序中的虚拟地址不会与其他程序冲突,都可以为相同的虚拟地址,不仅用户进程是这样,内核也是。程序中的地址是由链接器在链接过程中分配的,分配之后就不会再变了,运行时按部就班地送上处理器的CS和EIP即可。

但程序(进程、内核线程)在运行过程中也有申请内存的需求,这种动态申请内存一般是指在堆中申请内存,操作系统接受申请后,为进程或内核自己在堆中选择一空闲的虚拟地址,并且找个空闲的物理地址作为此虚拟地址的映射,之后把这个虚拟地址返回给程序。那么问题又来了,哪些虚拟地址是空闲的?如何跟踪它们的?

对于所有任务(包括用户进程、内核)来说,它们都有各自4GB的虚拟地址空间,因此需要为所有任务都维护它们自己的虚拟地址池,即一个任务一个。

内核为完成某项工作,也需要申请内存,当然它绝对有能力不通过内存管理系统申请内存,先斩后奏,或者奏都不奏一声,直接拿来就用。当然,这种“王者之风”显然不是那么和谐,我们让内核也通过内存管理系统申请内存,为此,它也要有个虚拟地址池,当它申请内存时,从内核自己的虚拟地址池中分配虚拟地址,再从内核物理内存池(内核专用)中分配物理内存,然后在内核自己的页表将这两种地址建立好映射关系。

对用户进程来说,它向内存管理系统,即操作系统,申请内存时,操作系统先从用户进程自己的虚拟地址池中分配空闲虚拟地址,然后再从用户物理内存池(所有用户进程共享)中分配空闲的物理内存,然后在该用户进程自己的页表将这两种地址建立好映射关系。

image-20240319222130789

二、接口与实现

2.1、先看接口

// 一个页框的大小,4KB
#define PG_SIZE 4096
// 位图地址
#define MEM_BITMAP_BASE 0xc009a000

// 0x100000意指跨过低端1M内存,也就是低端的1MB随我们折腾了
#define K_HEAP_START 0xc0100000


#define	 PG_P_1	  1	// 页表项或页目录项存在属性位
#define	 PG_P_0	  0	// 页表项或页目录项存在属性位
#define	 PG_RW_R  0	// R/W 属性位值, 读/执行
#define	 PG_RW_W  2	// R/W 属性位值, 读/写/执行
#define	 PG_US_S  0	// U/S 属性位值, 系统级
#define	 PG_US_U  4	// U/S 属性位值, 用户级


/* 内存池标记,用于判断用哪个内存池 */
void malloc_init(void);
enum pool_flags {
    PF_KERNEL = 1,    // 内核内存池
    PF_USER = 2	     // 用户内存池
};

/* 内存池结构 */
struct pool {
   struct bitmap pool_bitmap; // 本内存池用到的位图结构,用于管理物理内存
   uint32_t phy_addr_start;	  // 本内存池所管理物理内存的起始地址
   uint32_t pool_size;		  // 本内存池字节容量
};

/* 用于虚拟地址管理,虚拟地址池 */
struct virtual_addr {
    /* 虚拟地址用到的位图结构,用于记录哪些虚拟地址被占用了。以页为单位。*/
    struct bitmap vaddr_bitmap;
    /* 管理的虚拟地址 */
    uint32_t vaddr_start;
};

extern struct pool kernel_pool, user_pool;

/* 得到虚拟地址vaddr对应的pte指针*/
uint32_t* pte_ptr(uint32_t vaddr);
/* 得到虚拟地址vaddr对应的pde指针*/
uint32_t* pde_ptr(uint32_t vaddr);
/* 内存管理部分初始化 */
void mem_init(void);
/* 从内核物理内存池中申请pg_cnt页内存,成功则返回其虚拟地址,失败则返回NULL */
void* get_kernel_pages(uint32_t pg_cnt);
/* 分配pg_cnt个页空间,成功则返回起始虚拟地址,失败时返回NULL */
void* malloc_page(enum pool_flags pf, uint32_t pg_cnt);

有一个值 0xc009a000 ,这个地址是位图的地址。为什么放到这里呢?这就需要我们梳理一下现在内存的分布了

首先是低端 1MB

0xc0000400 ~ 0xc0000500: BIOS的数据区,包含系统启动时的一些信息

0xc0001500~0xc0099FFF 内核空间的起始,这里大概有610KB的空间,我们的内核也就这么大

0xc009a000~0xc009dfff 物理内存的位图,这里是16KB = 131072bit 可以管理512MB物理内存

0xc009e000~0xc009efff main线程的pcb,这个还没讲

0xc009f000 main线程的栈,也是main线程pcb的最高地址

0xc009FC00~0xc00A0000: BIOS数据扩展区

0xc000A0000~0xc00100000:用于 VGA 显存、BIOS 映射

看一看出来,这一块在低端的1MB内存中,属于比较靠上的部分了

2.2、首先先来个难的

看两个函数

// 拿到addr的前10bit,也就是页目录的索引
#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22)
// 拿到addr的中10bit,也就是页表的索引
#define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)

/* 得到虚拟地址vaddr对应的pte指针*/
uint32_t* pte_ptr(uint32_t vaddr) {
    uint32_t* pte = (uint32_t*)(0xffc00000 + ((vaddr & 0xffc00000) >> 10) + PTE_IDX(vaddr) * 4);
    return pte;
}

/* 得到虚拟地址vaddr对应的pde的指针 */
uint32_t* pde_ptr(uint32_t vaddr) {
    uint32_t* pde = (uint32_t*)((0xfffff000) + PDE_IDX(vaddr) * 4);
    return pde;
}

上面两个宏比较好说,下面这两个函数就难了。

先看第二个,找到vaddr对应的pde指针,也就是找到指向这个虚拟地址的pde的地址,这个是为了修改vaddr对应的页目录而创建的函数。

还记得分页机制嘛,我们是怎么做的,

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

页目录最后一项是指向自己的物理地址。所以为了能修改自己,第一步是将访问地址前十bit全部置为1

现在页目录被当做了页表,为了能够修改自己,把自己当做内存对待,需要将访问地址中间10bit全部置为1

现在页目录被当做了找到的一个普通页框,修改自己只需要索引就好,索引就是vaddr的前10bit

所以访问vaddr对应的pde就是访问 (0xfffff000) + PDE_IDX(vaddr) * 4 这个地址

再看第一个,得到虚拟地址vaddr对应的pte指针

第一步、访问页目录表,将页目录表当做页表处理,这一步我们已经熟悉了,就是将前10位置1

第二步、找到页表,由于现在的页目录被当做了页表,想要找到真正的页表需要将中十位置为vaddr的前十位

第三步、在页表中找到pte,拿到vaddr的中间十位,乘以4作为pte指针的最后十二位

2.3、获得物理内存容量

如何获得物理内存容量我们在前面的章节已经讲过了,这里不在赘述,不过如何拿到这个地址呢?我们看一下loader的代码

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其中total_mem_bytes被保存在了一条指令和一些数据后面。我们计算一下这些有多少

首先是gdt,一共64个描述符,每个描述符是8位

然后是lgdtr,一共是6位

但是还有一个跳转指令,我们不知道是多少字节,但是bochs可以看到gdt的起始地址如下图

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里给出的地址是 0xc0000603,所以保存内存的地址就是0xc0000603 + 64*8 + 6 = 0xc0000809,也可以看一下这里的数据

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其单位为Byte,小端字节序,所以C语言读入后是 0x2000000

2.4、整体代码

struct pool kernel_pool, user_pool;      // 生成内核内存池和用户内存池
struct virtual_addr kernel_vaddr;	     // 此结构是用来给内核分配虚拟地址

/* 在pf表示的虚拟地址池中同时取出pg_cnt个连续的页,成功返回虚拟地址,失败返回NULL */
static void* vaddr_get(enum pool_flags pf, uint32_t pg_cnt) {
    int vaddr_start = 0, bit_idx_start = -1;
    uint32_t cnt = 0;
    if (pf == PF_KERNEL) {
        bit_idx_start  = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt);
        if (bit_idx_start == -1) {
            return NULL;
        }
        while(cnt < pg_cnt) {
            bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
        }
        vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
    } else {	
    // 用户内存池,将来实现用户进程再补充
    }
    return (void*)vaddr_start;
}

/* 在m_pool指向的物理内存池中分配1个物理页,成功则返回页框的物理地址,失败则返回NULL */
static void* palloc(struct pool* m_pool) {
    int bit_idx = bitmap_scan(&m_pool->pool_bitmap, 1);    // 找一个物理页面
    if (bit_idx == -1 ) {
        return NULL;
    }
    bitmap_set(&m_pool->pool_bitmap, bit_idx, 1);	      // 将此位bit_idx置1
    uint32_t page_phyaddr = ((bit_idx * PG_SIZE) + m_pool->phy_addr_start);
    return (void*)page_phyaddr;
}

/* 页表中添加虚拟地址_vaddr与物理地址_page_phyaddr的映射 */
static void page_table_add(void* _vaddr, void* _page_phyaddr) {
    uint32_t vaddr = (uint32_t)_vaddr;
    uint32_t page_phyaddr = (uint32_t)_page_phyaddr;
    uint32_t* pde = pde_ptr(vaddr);
    uint32_t* pte = pte_ptr(vaddr);

    /* 先在页目录内判断目录项的P位,若为1,则表示该表已存在 */
    if (*pde & 0x00000001) {	      // 页目录项和页表项的第0位为P,此处判断目录项是否存在
        ASSERT(!(*pte & 0x00000001)); // 确保pte的最后一位为0,也就是这一位并未使用
        *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
    } else {  // 页目录项不存在,所以要先创建页目录再创建页表项.
        uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool); // 先申请一页物理内存当做页表
        *pde = (pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1);     // 将相应的pde绑定到申请的物理内存上
        memset((void*)((int)pte & 0xfffff000), 0, PG_SIZE);    // 将申请到的物理内存全部清0,避免旧数据的影响
        ASSERT(!(*pte & 0x00000001));                          // 确保pte的最后一位为0,也就是这一位并未使用
        *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);    // 写入pte完成绑定
    }
}

/* 分配pg_cnt个页空间,成功则返回起始虚拟地址,失败时返回NULL */
void* malloc_page(enum pool_flags pf, uint32_t pg_cnt) {
    if (pg_cnt < 0 && pg_cnt > 3840) {
        put_str("malloc_page error: pg_cnt error!\n");
        return NULL;
    }
    
    void* vaddr_start = vaddr_get(pf, pg_cnt);
    if (vaddr_start == NULL) {
        put_str("malloc_page error: vaddr_get error!\n");
        return NULL;
    }

    uint32_t vaddr = (uint32_t)vaddr_start, cnt = pg_cnt;
    struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;

    /* 因为虚拟地址是连续的,但物理地址可以是不连续的,所以逐个做映射*/
    while (cnt-- > 0) {
        void* page_phyaddr = palloc(mem_pool);
        // 失败时要将曾经已申请的虚拟地址和物理页全部回滚 #TODO
        if (page_phyaddr == NULL) {  
            put_str("malloc_page error: palloc error!\n");
            return NULL;
        }
        page_table_add((void*)vaddr, page_phyaddr); // 在页表中做映射 
        vaddr += PG_SIZE;		                    // 下一个虚拟页
    }
    return vaddr_start;
}

/* 从内核物理内存池中申请pg_cnt页内存,成功则返回其虚拟地址,失败则返回NULL */
void* get_kernel_pages(uint32_t pg_cnt) {
    void* vaddr =  malloc_page(PF_KERNEL, pg_cnt);
    if (vaddr != NULL) {
        memset(vaddr, 0, pg_cnt * PG_SIZE);
    } else {
        put_str("get_kernel_pages error: vaddr error!\n");
    }
    return vaddr;
}

/* 初始化内存池 */
static void mem_pool_init(uint32_t all_mem) {
    put_str("----mem_pool_init start\n");
    
    uint32_t page_table_size = PG_SIZE * 256;	  // 1页的页目录表+第0和第768个页目录项指向同一个页表
                                                  // 第769~1022个页目录项共指向254个页表,共256个页框
    uint32_t used_mem = page_table_size + 0x100000;	  // 使用的内存,也就是低端1MB+1MB
    uint32_t free_mem = all_mem - used_mem;           // 空闲的内存
    uint16_t all_free_pages = free_mem / PG_SIZE;		  // 空闲了多少页 
    uint16_t kernel_free_pages = all_free_pages / 2;                  // 内核空间拿一半空闲的页
    uint16_t user_free_pages = all_free_pages - kernel_free_pages;    // 用户空间拿一半空闲的页
    uint32_t kbm_length = kernel_free_pages / 8;		  // 内核空间bitmap的长度
    uint32_t ubm_length = user_free_pages / 8;			  // 用户空间bitmap的长度
    uint32_t kp_start = used_mem;			        	  // 内核空间的起始地址
    uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE;	  // 用户空间的起始地址

    kernel_pool.phy_addr_start = kp_start;     // 内核内存池物理地址的起始
    user_pool.phy_addr_start   = up_start;     // 用户内存池物理地址的起始

    kernel_pool.pool_size = kernel_free_pages * PG_SIZE;  // 内核内存池的大小
    user_pool.pool_size	 = user_free_pages * PG_SIZE;     // 用户内存池的大小

    kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length;  // 内核内存池位图的长度
    user_pool.pool_bitmap.btmp_bytes_len = ubm_length;    // 用户内存池位图的长度

    kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE;               // 内核内存池位图的起始地址
    user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length);  // 用户内存池位图的起始地址

    bitmap_init(&kernel_pool.pool_bitmap);  // 内核内存池位图初始化
    bitmap_init(&user_pool.pool_bitmap);    // 用户内存池位图初始化

    /******************** 输出内核内存池和用户内存池信息(物理地址) **********************/
    put_str("--------kernel_pool_bitmap_start:");put_hex((int)kernel_pool.pool_bitmap.bits);
    put_str("kernel_pool_phy_addr_start:");put_hex(kernel_pool.phy_addr_start);put_str("\n");
    put_str("--------user_pool_bitmap_start:");put_hex((int)user_pool.pool_bitmap.bits);
    put_str("user_pool_phy_addr_start:");put_hex(user_pool.phy_addr_start);put_str("\n");

    /* 下面初始化内核虚拟地址的位图,按实际物理内存大小生成数组。*/
    kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_length;
    /* 位图的数组指向一块未使用的内存,目前定位在内核内存池和用户内存池之外*/
    kernel_vaddr.vaddr_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length + ubm_length);
    /* 虚拟地址跨过低端的1MB以及页表的1MB */
    kernel_vaddr.vaddr_start = K_HEAP_START;
    bitmap_init(&kernel_vaddr.vaddr_bitmap);

    put_str("----mem_pool_init done\n");
}


void mem_init() {
    put_str("mem_init start\n");
    uint32_t mem_bytes_total = (*(uint32_t*)(0xc0000809));
    mem_pool_init(mem_bytes_total);	  // 初始化内存池
    put_str("mem_init done\n");
}

整体代码就比较简单了,就是两个物理内存池(内核物理内存池,用户物理内存池)的初始化,以及一些申请内存的函数,这里最难得还是修改页目录与页表实现虚拟内存与物理内存的映射。

三、仿真

image-20240320012417848

结束语

本章我们实现了用户和内核的地址分配,下一章,我们将实现内核级线程

老规矩,代码地址 https://github.com/lyajpunov/os.git

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

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

相关文章

四、分布式锁之自定义分布式锁

1、基本原理和实现方式对比 分布式锁&#xff1a;满足分布式系统或集群模式下多个进程可见并且互斥的锁。分布式锁的核心思想就是多线程都使用同一把锁&#xff0c;实现程序串行执行。 分布式锁需要具备的条件&#xff1a; 特性含义可见性多个线程都能感知到变化互斥性分布…

Orange3数据预处理(行选择组件)

选择行 根据数据特征的条件选择数据实例。 输入 数据&#xff1a;输入数据集 输出 匹配数据&#xff1a;满足条件的实例 不匹配数据&#xff1a;不满足条件的实例 数据&#xff1a;带有额外列的数据&#xff0c;显示实例是否被选中 这个小部件根据用户…

数据库系统概论-第16章 数据仓库与联机分析处理技术

概念性的介绍&#xff0c;一略而过&#xff0c;不重要。 16.1 数据仓库技术 16.2 联机分析处理技术 16.3 数据挖掘技术 16.4 大数据时代的新型数据仓库 16.5 小结

jetson nano torch1.6 torchvision0.7.0 yolov5

pytorch版本对应关系查看网址&#xff1a; pytorch torchvision pytorch安装方式 点击pytorch链接&#xff1a;pytorch torchvision安装方式 sudo apt-get install libjpeg-dev zlib1g-dev libavcodec-dev libavformat-dev libswscale-dev git clone --branch v0.7.0 https…

第113讲:Mycat实践指南:按照单位为天的日期实现水平分表

文章目录 1.按天分片的概念1.按天分片的概念 2.按照天数对某张表进行水平拆分2.1.在所有的分片节点中创建表结构2.2.配置Mycat实现字符串按天分片的水平分表2.2.1.配置Schema配置文件2.2.2.配置Rule分片规则配置文件2.2.3.配置Server配置文件2.2.4.重启Mycat 2.3.写入数据观察分…

[每周一练][NewStarCTF 2023 公开赛道]EasyLogin

一打开是个登录界面&#xff0c;注册账号进去看了一下似乎没有什么提示。按照经验这种登录系统的一般就是sql或者爆破。先试试简单的爆破。 猜测管理员账号&#xff1a;admin,密码&#xff1a;123456。抓包看到传入的密码是被加密了的。应该是MD5加密。 爆破的话就必须用MD5的密…

短视频矩阵系统/短视频矩阵系统/自研独立框架

短视频矩阵系统/短视频矩阵系统/自研独立框架&#xff0c; 短视频综合矩阵营销管理系统,一键分发多个平台,帮助企业管理海量视频账号&#xff0c;包含抖音视频、AI混剪、矩阵导流&#xff0c;客户获取等功能。通过将视频分发、到各账号&#xff0c;提高品牌曝光率、且可以同时管…

系列直播预告:Apache Doris 2.1 新版本特性解读来袭,惊喜周边等你拿!

不久之前&#xff0c;Apache Doris 2.1.0 版本迎来正式发布&#xff0c;在盲测性能提升 100% 的同时&#xff0c;更在数据湖分析、半结构化数据分析、数据写入与更新、数据存储与负载隔离等方面推出众多核心特性&#xff0c;实时性和易用性的到全面提升。 为了让更多关注和喜爱…

transformer的学习:Attention is all you need

目录 整体概述&#xff1a;​编辑​编辑 encoder&#xff1a; embedding&#xff1a; ​编辑 self-attention&#xff1a; 向量的相似度计算&#xff1a; qkv怎么来的​编辑 softmax&#xff1a; code multi-head-attention 位置编码&#xff1a; 残差&&FFN&…

leetcode 2617. 网格图中最少访问的格子数【单调栈优化dp+二分】

原题链接&#xff1a;2617. 网格图中最少访问的格子数 题目描述&#xff1a; 给你一个下标从 0 开始的 m x n 整数矩阵 grid 。你一开始的位置在 左上角 格子 (0, 0) 。 当你在格子 (i, j) 的时候&#xff0c;你可以移动到以下格子之一&#xff1a; 满足 j < k < gri…

嵌入式开发--STM32G431RBTx-定时器中断流水灯

嵌入式开发–STM32G431RBTx-定时器中断流水灯 定时器工作原理 如图有反映stm32g431的定时器资源。 共10个定时器 定时器定时器类型个数TIM6&#xff0c;7基本定时器2TIM2&#xff0c;3&#xff0c;4全功能通用定时器3TIM15&#xff0c;16&#xff0c;17通用定时器(只有1或2个…

Linux_开发工具_yum_vim_gcc/g++_gdb_make/makefile_进度条_git_2

文章目录 一、Linux软件包管理器yum1. centos7 中安装软件方式2.安装&#xff0c;卸载&#xff0c;查看3.yum源4.安装lrzsz5.安装扩展源 二、Linux编辑器-vim1.安装vim2.vim的三种模式3.命令模式-文本批量化操作4.vim配置 三、Linux编译器-gcc/g使用1.安装2.gcc如何完成1、 预处…

SpringBoot3使用响应Result类返回的响应状态码为406

Resolved [org.springframework.web.HttpMediaTypeNotAcceptableException: No acceptable representation] 解决方法&#xff1a;Result类上加上Data注解

【算法刷题】Day33

文章目录 1. 最长湍流子数组题干&#xff1a;算法原理&#xff1a;1. 状态表示&#xff1a;2. 状态转移方程3. 初始化4. 填表顺序5. 返回值 代码&#xff1a; 2. 最长递增子序列题干&#xff1a;算法原理&#xff1a;1. 状态表示&#xff1a;2. 状态转移方程3. 初始化4. 填表顺…

链表递归-leetcode两两交换相邻链表中的结点

两两交换相邻链表中的结点 题目&#xff1a; 给定一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后的链表。 你不能只是单纯的改变节点内部的值&#xff0c;而是需要实际的进行节点交换。 示例1 输入&#xff1a;head [1,2,3,4] 输出&#xff1a;[2,1…

文件怎么做扫码预览?创建文件活码的步骤有哪些?

现在文件可以通过扫描二维码的方式来获取&#xff0c;与传统的通过聊天软件来传输相比&#xff0c;二维码方式的应用更加的方便&#xff0c;其他人只需要通过扫描一张二维码就可以在手机上浏览或者下载文件&#xff0c;通过手机就可以预览、存储。 文件二维码的制作方法也很简…

C语言牛客网刷题

1.最大公约数和最小公倍数的组合问题 &#xff08;1&#xff09;在调试的过程中涉及到很大的数据&#xff0c;我们我们在定义变量的时候定义为long long类型 &#xff08;2&#xff09;这个里面我们自定义了max2用来求最大公约数&#xff0c;min2用来求最小公倍数 &#xff0…

稀碎从零算法笔记Day23-LeetCode:翻转二叉树

题型&#xff1a;链表、二叉树 链接&#xff1a;226. 翻转二叉树 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 题目描述 给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 这道题适合就着样例来做 题目样例 …

sqlite3 交叉编译

#1.下载源码并解压 源码路径如下&#xff0c;下载autoconf版本 SQLite Download Page 解压 tar -zxvf sqlite-autoconf-3450200.tar.gz cd sqlite-autoconf-3450200 mkdir build # 2. 配置源代码 # 假设你已经安装了交叉编译工具链&#xff0c;如gcc-arm-linux-gnueabih…

2024年React初学者入门路线指南

在这篇文章中&#xff0c;我们一步一步探索了如何从零基础开始学习React&#xff0c;并逐渐成长为一名初级开发者。通过理解基础概念、实践构建静态和动态项目&#xff0c;最终发展到创建复杂的应用程序并加入到个人作品集中&#xff0c;您现在已经准备好迈向React开发者的职业…