手写简易操作系统(二十)--实现堆内存管理

前情提要

前面我们实现了 0x80 中断,并实现了两个中断调用,getpidwrite,其中 write 还由于没有实现文件系统,是个残血版,这一节我们实现堆内存管理。

一、arena

在计算机科学中,“arena” 内存管理通常指的是一种内存分配和管理技术,它通常用于动态内存分配和释放。在这种管理方式下,程序会从系统中获取一大块内存,然后按需分割和管理这块内存,以满足程序中对内存的动态需求。

“arena” 内存管理的优点在于减少了频繁向操作系统请求内存的开销。相比之下,每次调用 malloc 或 free 都需要涉及到系统调用,而 “arena” 内存管理可以减少这种开销,因为它在一开始就获取了一大块内存,然后由程序自己来管理这块内存的分配和释放。

一般来说,“arena” 内存管理会维护一个或多个内存池(memory pool),从中分配内存给程序使用。内存池可以分割成多个固定大小的块,或者按需分配不同大小的内存块。内存分配器会跟踪这些内存块的使用情况,并确保在程序需要时能够高效地分配和释放内存。

许多现代的内存分配器都采用了 “arena” 内存管理的思想,以提高内存分配和释放的效率。这种技术在实际应用中有助于减少内存碎片化、提高性能,并且减少了向操作系统请求内存的次数。

换个说法就是,我准备好很多小块的内存,你需要哪种规格,我给你哪种规格。大于1KB,直接给你通过页框给你分配,小于16B,直接给16B。加入你需要53B,但是53不是一种规格,那么给你64B,64是一种规格,也刚刚好够你使用

1.1、arena数据结构

/* 内存块 */
struct mem_block {
    struct list_elem free_elem;
};

/* 内存块描述符 */
struct mem_block_desc {
    uint32_t block_size;		 // 内存块大小
    uint32_t blocks_per_arena;	 // 本arena中可容纳此mem_block的数量.
    struct list free_list;    	 // 目前可用的mem_block链表
};

/* 内存仓库arena元信息 */
struct arena {
    struct mem_block_desc* desc; // 此arena关联的mem_block_desc
    uint32_t cnt;                // large为ture时,cnt表示的是页框数。否则cnt表示空闲mem_block数量
    bool large;
};

struct mem_block_desc k_block_descs[DESC_CNT];	// 内核内存块描述符数组,其中规格,最小16Byte

首先是内存块描述符,内存块描述符描述的是我们有多少种规格的内存大小分类,这里我们设计的是七种。所以最后我们也生成了一个全局的内存块描述符数组。

内存块描述符中包含三个属性,一个是内存块的大小(16B,32B,64B…),另一个是arena中可以容纳的内存块数量,这个值是一个定值,比如我们的内存块为16B,arena结构体占一定的空间,剩下的空间全部被分为16B的内存块。最后是空闲的内存块的链表,这个链表链接的就是arena仓库中的内存块

arena也包含三个信息,arena关联的内存块描述符的指针,这个是为了标识这个arena的存储的类型,还有就是cnt数量,cnt数量在large为true时表示的页框数,这个很好理解,如果大于1024B的话,那么直接分配页框了,就将large标识为true。

image-20240329195055740

1.2、arena初始化

/* 内核内存块描述符数组初始化 */
void block_desc_init(struct mem_block_desc* desc_array) {
    uint32_t block_size = 16;
    // 初始化每个mem_block_desc描述符
    for (uint32_t desc_idx = 0; desc_idx < DESC_CNT; desc_idx++) {
        desc_array[desc_idx].block_size = block_size;
        // 初始化arena中的内存块数量
        desc_array[desc_idx].blocks_per_arena = (PG_SIZE - sizeof(struct arena)) / block_size;
        // 初始化每个描述符的空闲块链表
        list_init(&(desc_array[desc_idx].free_list));
        // 更新为下一个规格内存块
        block_size *= 2;
    }
}

/* 返回arena中第idx个内存块的地址 */
static struct mem_block* arena2block(struct arena* a, uint32_t idx) {
    return (struct mem_block*)((uint32_t)a + sizeof(struct arena) + idx * a->desc->block_size);
}

/* 返回内存块b所在的arena地址 */
static struct arena* block2arena(struct mem_block* b) {
    return (struct arena*)((uint32_t)b & 0xfffff000);
}

内核内存块描述符数组是在内核中是准备好了的,这里我们只是初始化。

二、malloc

/* 在堆中申请size字节内存 */
void* sys_malloc(uint32_t size) {
    enum pool_flags PF;        // 线程标识
    struct pool* mem_pool;     // 内核内存池或者用户内存池
    uint32_t pool_size;        // 内存池大小
    struct mem_block_desc* descs; // 内存块描述符
    struct task_struct* cur_thread = running_thread();

    if (cur_thread->pgdir == NULL) {     // 若为内核线程
        PF = PF_KERNEL;
        pool_size = kernel_pool.pool_size;
        mem_pool = &kernel_pool;
        descs = k_block_descs;
    }
    else {				                 // 若为用户线程
        PF = PF_USER;
        pool_size = user_pool.pool_size;
        mem_pool = &user_pool;
        descs = cur_thread->u_block_desc;
    }

    if (!(size > 0 && size < pool_size)) { // 若申请的内存不在内存池容量范围内则直接返回NULL
        return NULL;
    }

    struct arena* a;         // 内存仓库元信息
    struct mem_block* b;     // 内存块
    lock_acquire(&mem_pool->lock);

    if (size > 1024) {
        // 超过最大内存块1024, 就分配页框,需要的页框数为申请内存大小+内存块元信息
        uint32_t page_cnt = DIV_ROUND_UP(size + sizeof(struct arena), PG_SIZE);

        a = malloc_page(PF, page_cnt);

        if (a != NULL) {
            memset(a, 0, page_cnt * PG_SIZE);	 // 将分配的内存清0  

            /* 对于分配的大块页框,将desc置为NULL, cnt置为页框数,large置为true */
            a->desc = NULL;
            a->cnt = page_cnt;
            a->large = true;
            lock_release(&mem_pool->lock);
            return (void*)(a + 1);		 // 跨过arena大小,把剩下的内存返回
        }
        else {
            lock_release(&mem_pool->lock);
            return NULL;
        }
    }
    else {
        // 若申请的内存小于等于1024,可在各种规格的mem_block_desc中去适配
        uint8_t desc_idx;

        // 从内存块描述符中匹配合适的内存块规格
        for (desc_idx = 0; desc_idx < DESC_CNT; desc_idx++) {
            if (size <= descs[desc_idx].block_size) {  // 从小往大后,找到后退出
                break;
            }
        }

        // 若mem_block_desc的free_list中已经没有可用的mem_block, 就创建新的arena提供mem_block
        if (list_empty(&descs[desc_idx].free_list)) {
            // 分配1页框做为arena
            a = malloc_page(PF, 1);
            if (a == NULL) {
                lock_release(&mem_pool->lock);
                return NULL;
            }
            memset(a, 0, PG_SIZE);

            // 对于分配的小块内存,将desc置为相应内存块描述符,cnt置为此arena可用的内存块数,large置为false
            a->desc = &descs[desc_idx];
            a->large = false;
            a->cnt = descs[desc_idx].blocks_per_arena;
            uint32_t block_idx;

            enum intr_status old_status = intr_disable();

            // 开始将arena拆分成内存块,并添加到内存块描述符的free_list中
            for (block_idx = 0; block_idx < descs[desc_idx].blocks_per_arena; block_idx++) {
                b = arena2block(a, block_idx);
                ASSERT(!elem_find(&a->desc->free_list, &b->free_elem));
                list_append(&a->desc->free_list, &b->free_elem);
            }
            intr_set_status(old_status);
        }

        /* 开始分配内存块 */
        b = elem2entry(struct mem_block, free_elem, list_pop(&(descs[desc_idx].free_list)));
        memset(b, 0, descs[desc_idx].block_size);

        a = block2arena(b);  // 获取内存块b所在的arena
        a->cnt--;		     // 将此arena中的空闲内存块数减1
        lock_release(&mem_pool->lock);
        return (void*)b;
    }
}

malloc函数较为复杂,首先是判断当前是内核线程还是用户进程,这是不一样的,因为内核线程有内核线程的物理内存池,用户进程有用户进程的物理内存池。每次分配内存要在不同的内存池内分配。

申请的内存以byte为单位,如果大于1024,那么直接分配连续的连续的页。计算出需要多少页,然后通过 malloc_page 函数申请,返回其虚拟地址。此时其实已经为销毁这部分占用内存埋下了伏笔。让我们看一下现在我们分配的内存张啥样

image-20240329205115097

可以看到这里分配的页数是2,找到虚拟地址连续的两页,然后cnt=2,large=true,表示我们直接分配的页框,如果药销毁的话,传入的就是现在的地址,根据现在的地址,我们就可以修改相应的线程或者进程的虚拟地址池和物理地址池,达到销毁的目的。

如果申请的是小于1024的内存呢?比如64Byte。

image-20240329210024260

那就申请一个页,然后创建成这样的arena仓库,此时的cnt为当前可用的内存块数量,desc指向了内存描述符,内存描述符中有一个链表,链表把未分配的地址链接了起来,这是由于未分配的地址最前面放置了一个链表节点结构体,并在初始化这个arena仓库时将所有的空闲块都链接了起来,在释放时,也需要将释放地址前端再初始化为链表节点,加入空闲队列。

三、sys_free

理解了mallloc,再看free就简单很多了,他俩就是两个相互对应的过程,内存怎么分配的就怎么释放,

/* 将物理地址pg_phy_addr回收到物理内存池,这里的回收以页为单位 */
void pfree(uint32_t pg_phy_addr) {
    struct pool* mem_pool;
    uint32_t bit_idx = 0;
    if (pg_phy_addr >= user_pool.phy_addr_start) {         // 用户物理内存池
        mem_pool = &user_pool;
        bit_idx = (pg_phy_addr - user_pool.phy_addr_start) / PG_SIZE;
    }
    else {	                                               // 内核物理内存池
        mem_pool = &kernel_pool;
        bit_idx = (pg_phy_addr - kernel_pool.phy_addr_start) / PG_SIZE;
    }
    bitmap_set(&mem_pool->pool_bitmap, bit_idx, 0);	 // 将位图中该位清0
}

/* 去掉页表中虚拟地址vaddr的映射,只去掉vaddr对应的pte */
static void page_table_pte_remove(uint32_t vaddr) {
    uint32_t* pte = pte_ptr(vaddr);
    *pte &= ~PG_P_1;	                                   // 将页表项pte的P位置0,不需要删除pde
    asm volatile ("invlpg %0"::"m" (vaddr) : "memory");    // 更新tlb
}

/* 在虚拟地址池中释放以_vaddr起始的连续pg_cnt个虚拟页地址 */
static void vaddr_remove(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt) {
    uint32_t bit_idx_start = 0;
    uint32_t vaddr = (uint32_t)_vaddr;
    uint32_t cnt = 0;

    if (pf == PF_KERNEL) {
        // 内核虚拟内存池
        bit_idx_start = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;
        while (cnt < pg_cnt) {
            bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0);
        }
    }
    else if (pf == PF_USER) {
        // 用户虚拟内存池
        struct task_struct* cur_thread = running_thread();
        bit_idx_start = (vaddr - cur_thread->userprog_vaddr.vaddr_start) / PG_SIZE;
        while (cnt < pg_cnt) {
            bitmap_set(&cur_thread->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0);
        }
    }
    else {
        PANIC("vaddr_remove error!\n");
    }
}

/* 释放以虚拟地址vaddr为起始的cnt个页框,vaddr必须是页框起始地址 */
void mfree_page(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt) {
    uint32_t vaddr = (int32_t)_vaddr;
    uint32_t page_cnt = 0;
    // 确保虚拟地址是页框的起始
    ASSERT(pg_cnt >= 1 && vaddr % PG_SIZE == 0);
    // 获取虚拟地址vaddr对应的物理地址
    uint32_t pg_phy_addr = addr_v2p(vaddr);
    // 确保物理地址也是页框的起始
    ASSERT((pg_phy_addr % PG_SIZE) == 0);
    // 确保待释放的物理内存在低端1M+1k大小的页目录+1k大小的页表地址范围外
    ASSERT(pg_phy_addr >= 0x102000);

    if (pg_phy_addr >= user_pool.phy_addr_start) {
        // 位于user_pool内存池,要释放的是用户内存
        for (page_cnt = 0; page_cnt < pg_cnt; page_cnt++) {
            vaddr = (int)_vaddr + PG_SIZE * page_cnt;
            pg_phy_addr = addr_v2p(vaddr);
            // 确保物理地址属于用户物理内存池 
            ASSERT((pg_phy_addr % PG_SIZE) == 0);
            ASSERT(pg_phy_addr >= user_pool.phy_addr_start);
            // 先将对应的物理页框归还到内存池
            pfree(pg_phy_addr);
            // 再从页表中清除此虚拟地址所在的页表项pte
            page_table_pte_remove(vaddr);
        }
    }
    else {
        // 位于kernel_pool内存池,要释放的是内核内存
        for (page_cnt = 0; page_cnt < pg_cnt; page_cnt++) {
            vaddr = (int)_vaddr + PG_SIZE * page_cnt;
            // 获得物理地址
            pg_phy_addr = addr_v2p(vaddr);
            // 确保待释放的物理内存只属于内核物理内存池
            ASSERT((pg_phy_addr % PG_SIZE) == 0);
            ASSERT(pg_phy_addr >= kernel_pool.phy_addr_start);
            ASSERT(pg_phy_addr < user_pool.phy_addr_start);
            // 先将对应的物理页框归还到内存池
            pfree(pg_phy_addr);
            // 再从页表中清除此虚拟地址所在的页表项pte
            page_table_pte_remove(vaddr);
        }
    }
    // 清空虚拟地址的位图中的相应位
    vaddr_remove(pf, _vaddr, pg_cnt);
}

/* 回收堆内存 */
void sys_free(void* ptr) {
    ASSERT(ptr != NULL);
    if (ptr == NULL) return;

    enum pool_flags PF;        // 回收的是内核还是用户的内存
    struct pool* mem_pool;     // 内核用户池或者用户内存池

    /* 判断是线程还是进程 */
    if (running_thread()->pgdir == NULL) {
        ASSERT((uint32_t)ptr >= K_HEAP_START);
        PF = PF_KERNEL;
        mem_pool = &kernel_pool;
    }
    else {
        PF = PF_USER;
        mem_pool = &user_pool;
    }

    lock_acquire(&mem_pool->lock);
    struct mem_block* b = ptr;
    struct arena* a = block2arena(b);	       // 把mem_block转换成arena,获取元信息,元信息在每个块的头部
    if (a->desc == NULL && a->large == true) { // 大于1024的内存
        mfree_page(PF, a, a->cnt);
    }
    else {
        // 小于等于1024的内存块,先将内存块回收到描述符的空闲列表
        list_append(&a->desc->free_list, &b->free_elem);
        // 将内存块元信息中的块数量加1
        a->cnt++;
        // 再判断此arena中的内存块是否都是空闲,如果是就释放这个arena块
        if (a->cnt == a->desc->blocks_per_arena) {
            // 先从空闲列表中逐个删除块
            for (uint32_t block_idx = 0; block_idx < a->desc->blocks_per_arena; block_idx++) {
                struct mem_block* b = arena2block(a, block_idx);
                ASSERT(elem_find(&a->desc->free_list, &b->free_elem));
                list_remove(&b->free_elem);
            }
            // 在删除整个页
            mfree_page(PF, a, 1);
        }
    }
    lock_release(&mem_pool->lock);
}

这里需要注意的一点是,如果一个arena仓库全为空,那么就释放这个仓库所占页面。就是要一点,给一点,内存资源宝贵,只能这样抠抠搜搜。

四、用户调用

/* 初始化系统调用,也就是将syscall_table数组中绑定好确定的函数 */
void syscall_init(void) {
    put_str("syscall_init begin!\n");
    syscall_table[SYS_GETPID] = sys_getpid;
    syscall_table[SYS_WRITE] = sys_write;
    syscall_table[SYS_MALLOC] = sys_malloc;
    syscall_table[SYS_FREE] = sys_free;
    put_str("syscall_init done!\n");
}
/* 申请size字节大小的内存,并返回结果 */
void* malloc(uint32_t size) {
   return (void*)_syscall1(SYS_MALLOC, size);
}

/* 释放ptr指向的内存 */
void free(void* ptr) {
   _syscall1(SYS_FREE, ptr);
}

用户调用的代码和之前的保持一致。

4.1、仿真

仿真的main如下

// os/src/kernel/main.c
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall_init.h"
#include "syscall.h"
#include "stdio.h"
#include "memory.h"

void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);

int main(void) {
    put_str("I am kernel\n");
    init_all();
    intr_enable();
    process_execute(u_prog_a, "u_prog_a");
    process_execute(u_prog_b, "u_prog_b");
    thread_start("k_thread_a", k_thread_a, "I am thread_a");
    thread_start("k_thread_b", k_thread_b, "I am thread_b");
    while (1);
    return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
    void* addr1 = sys_malloc(256);
    void* addr2 = sys_malloc(255);
    void* addr3 = sys_malloc(254);
    printk(" k_thread_a malloc addr:0x%x, 0x%x, 0x%x\n", (int)addr1, (int)addr2, (int)addr3);

    int cpu_delay = 100000;
    while (cpu_delay-- > 0);
    sys_free(addr1);
    sys_free(addr2);
    sys_free(addr3);
    while (1);
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
    void* addr1 = sys_malloc(256);
    void* addr2 = sys_malloc(255);
    void* addr3 = sys_malloc(254);
    printk(" k_thread_b malloc addr:0x%x, 0x%x, 0x%x\n", (int)addr1, (int)addr2, (int)addr3);

    int cpu_delay = 100000;
    while (cpu_delay-- > 0);
    sys_free(addr1);
    sys_free(addr2);
    sys_free(addr3);
    while (1);
}

/* 测试用户进程 */
void u_prog_a(void) {
    void* addr1 = malloc(256);
    void* addr2 = malloc(255);
    void* addr3 = malloc(254);
    printf(" prog_a malloc addr:0x%x, 0x%x, 0x%x\n", (int)addr1, (int)addr2, (int)addr3);

    int cpu_delay = 100000;
    while (cpu_delay-- > 0);
    free(addr1);
    free(addr2);
    free(addr3);
    while (1);
}

/* 测试用户进程 */
void u_prog_b(void) {
    void* addr1 = malloc(256);
    void* addr2 = malloc(255);
    void* addr3 = malloc(254);
    printf(" prog_b malloc addr:0x%x, 0x%x, 0x%x\n", (int)addr1, (int)addr2, (int)addr3);

    int cpu_delay = 100000;
    while (cpu_delay-- > 0);
    free(addr1);
    free(addr2);
    free(addr3);
    while (1);
}

image-20240329211057932

结束语

今天实现了系统调用 mallocfree,将堆内存管理实现,下一节将实现硬盘驱动,在硬盘驱动的基础上实现文件系统。

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

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

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

相关文章

鸿蒙人才供需两旺、抓住职业“薪”机遇

鸿蒙生态崛起&#xff0c;人才需求旺盛&#xff01; 鸿蒙生态迅猛发展&#xff0c;人才供需均呈增长态势&#xff0c;鸿蒙开发工程师平均月薪显著领跑&#xff0c;显示出该领域的巨大潜力和吸引力&#xff0c;对于有志于鸿蒙开发的人才&#xff0c;这无疑是职业发展的黄金时期…

OpenHarmony实战:代码上库

前言 到达这一步好比临门一脚&#xff0c;意义很大&#xff01;您的代码被合入 OpenHarmony 平台&#xff0c;这是最后的一道关口&#xff0c;保证合入的是正确的&#xff0c;并且不会对系统造成意外。 避坑指南 1. 填写 ISSUE 和 PR 按照规范进行 ISSUE 和 PR 填写不规范会…

CentOs7.9中修改Mysql8.0.28默认的3306端口防止被端口扫描入侵

若你的服务器被入侵&#xff0c;可以从这些地方找到证据&#xff1a; 若有上述信息&#xff0c;300%是被入侵了&#xff0c;重装服务器系统以后再重装Mysql数据库&#xff0c;除了设置一个复杂的密码以外&#xff0c;还需要修改默认的Mysql访问端口&#xff0c;逃避常规端口扫描…

马斯克旗下xAI发布Grok-1.5,相比较开源的Grok-1,各项性能大幅提升,接近GPT-4!

本文原文来自DataLearnerAI官方网站&#xff1a;马斯克旗下xAI发布Grok-1.5&#xff0c;相比较开源的Grok-1&#xff0c;各项性能大幅提升&#xff0c;接近GPT-4&#xff01; | 数据学习者官方网站(Datalearner) 继Grok-1开源之后&#xff0c;xAI宣布了Grok-1.5的内测消息&…

Homebrew 镜像源配置

前言 当我们使用默认官方源时&#xff0c;经常会遇到以下问题 查看镜像配置 brew config 切换国内源 /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)" 4.0 镜像配置 温馨提示&#xff1a;不要使用阿里云的 Homebrew 源&am…

使用CRXjs、Vite、Vue 开发 Chrome 多页面插件,手动配置 vite.config.ts 和 manifest.json 文件

一、使用CRXjs、Vite、Vue 开发 Chrome 多页面插件&#xff0c;手动配置 vite.config.ts 和 manifest.json 文件 一、创建 Vue 项目 1. 使用 Vite 创建 Vue 项目 npm create vitelatest # npm yarn create vite # yarn pnpm create vite # pnpm选择 Vue 和 TS 进入项目…

Spring IOC 容器循环依赖解决(三级缓存)

对于循环依赖的解决&#xff0c;首先得了解Spring IOC 容器的创建过程&#xff0c;在加载过程中&#xff0c;Bean 的实例化和初始化是分开的&#xff0c;所以在解决循环依赖的问题时&#xff0c;也是基于Bean 的实例化和初始化分开执行这一特点。 我们将实例化后的Bean 叫 半成…

【Web自动化】Selenium的使用(一)

目录 关于自动化测试selenium工作机制 selenium的使用selenium中常用API定位元素按id定位按名称定位按类名定位按标签名定位按CSS选择器定位按XPath定位示例 操作测试对象等待sleep休眠隐式等待显示等待 打印信息浏览器操作键盘事件鼠标事件切换窗口截图关闭浏览器 欢迎阅读本文…

Windows 11 专业版 23H2 Docker Desktop 下载 安装 配置 使用

博文目录 文章目录 Docker Desktop准备系统要求 (WSL 2 backend)在 Windows 上打开 WSL 2 功能先决条件开启 WSL 2 WSL下载安装启动配置使用镜像 Image卷积 Volumes容器 Containers 命令RedisMySQLPostGreSQL Docker Desktop Overview of Docker Desktop Docker Desktop 疑难解…

揭秘五五复制模式,助力平台用户快速裂变至百万级!

你是否时常为平台的用户增长缓慢而倍感压力&#xff1f;是否渴望找到一种方法&#xff0c;让平台用户迅速扩张&#xff0c;实现百万级用户量的突破&#xff1f;今天&#xff0c;我将为大家揭晓一种创新的商业模式——五五复制模式&#xff0c;它或许能成为你实现梦想的关键。 五…

位运算

本文用于记录个人算法竞赛学习&#xff0c;仅供参考 目录 一.n的二进制表示中第k位x 二.通过lowbit操作返回x的最后一位1 1.lowbit实现&#xff1a;x & (-x) 2. lowbit具体作用 一.n的二进制表示中第k位x n 15 &#xff08;1111&#xff09;2 操作&#xff1a;1.x …

Redis主从同步机制

一、步骤如下&#xff1a;&#xff08;全量&#xff09; 1.从服务器向主服务器发送同步命令 sync&#xff1b; 2.主数据库接收到同步命令后&#xff0c;会执行 bgsave 命令&#xff0c;在后台生成一个 rdb 文件&#xff0c;并使用一个缓冲区记录从现在开始执行的所有写命令&a…

苏州金龙新V系客车创新打造,剑指新标杆

诞生于2004年的苏州金龙V系客车在20年时间里销售了6万多辆&#xff0c;用户超过5000家&#xff0c;用户的反复选择体现了它超强的产品力。3月下旬&#xff0c;全新打造的苏州金龙新V系客车震撼登场&#xff0c;拥趸们发现&#xff0c;该系列客车在智能化、网联化及设计语言方面…

如何使用剪映专业版剪辑视频

1.操作界面功能介绍 2.时间线的使用 拖动前端后端缩减时长&#xff0c;有多个素材可以拖动调节前后顺序拼接。 分割视频 删除

基于无迹卡尔曼滤波的路面附着系数估计算法

基于无迹卡尔曼滤波的路面附着系数估计算法 附赠自动驾驶学习资料和量产经验&#xff1a;链接 路面附着系数作为车辆底盘动力学反馈控制中的重要变量&#xff0c;对它的精确估计直接关系到控制系统的平稳运行和车辆行驶安全。但是由于无法通过直接测量获得某些状态参数或者测…

手册更新 | RK3588开发板适配Android13系统

iTOP-RK3588开发板使用手册更新&#xff0c;后续资料会不断更新&#xff0c;不断完善&#xff0c;帮助用户快速入门&#xff0c;大大提升研发速度。 本次更新内容为《iTOP-3588开发板源码编译手册》&#xff0c;RK3588开发板适配了Android13系统&#xff0c;手册同步添加了And…

@所与人「要复现」文献调研与需求收集

鉴于上次的「下一个要知道什么」调查结果&#xff0c;我发现「复现文献」的呼声不是一般的高&#xff0c;那是相当的高呐&#xff01; 所以&#xff01;新的调查又来了&#xff01;文献数量和类型实在是太太太太太太庞大了&#xff01;所以我就想征询一下大家的需求&#xff0c…

新人必看,轻松学会品牌360百科词条创建

品牌在当今互联网时代的重要性不言而喻。随着人们对信息的需求和获取渠道的多样化&#xff0c;品牌需要在各个平台上展示自己的形象&#xff0c;其中包括360百科这样的综合性知识平台。创建360百科词条可以为品牌增加曝光度、提升信誉度&#xff0c;进而吸引更多潜在客户和粉丝…

机器学习实战17-高斯朴素贝叶斯(GaussianNB)模型的实际应用,结合生活中的生动例子帮助大家理解

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下机器学习实战17-高斯朴素贝叶斯(GaussianNB)模型的实际应用&#xff0c;结合生活中的生动例子帮助大家理解。GaussianNB&#xff0c;即高斯朴素贝叶斯模型&#xff0c;是一种基于概率论的分类算法&#xff0c;广泛应…

HCIA复习

OSI --开放式系统互联参考模型 --- 7层参考模型 TCP/IP协议栈道 --- 4层或5层 OSI&#xff1a; 应用层 抽象语言 -->编码 表示层 编码-->二进制 表示层以下都是二进制-----data&#xff08;数据&#xff09; 会话层 提供应用程序的会话地址 上三层为应用…