二、4.makefile、断言和位图内存池

news2024/9/22 11:28:37

在 Linux 中,文件分为属性和数据两部分,每个文件有三种时间,分别用于记录与文件属性和文件数据相关的时间,这三个时间分别是 atime、 mtime 、 ctime 。

  1. atime,即 access time,和示访问文件数据部分时间,每次读取文件数据部分时就会更新剧me,强调下,是读取文件数据(内容)时改变 atime,比如 cat 或 less 命令查看文件就可以更新 atime,而 ls 命令则不会。
  2. ctime,即 change time,表示文件属性或数据的改变时间,每当文件的属性或数据被修改时,就会更新 ctime,也就是说 ctime 同时跟踪文件属性和文件数据变化的时间。
  3. mtime,即 modify time,表示文件数据部分的修改时间,每次文件的数据被修改时就会更新 mtime 。在上面说过啦, ctime 也跟踪数据变化时间,所以,当文件数据被修改时, mtime 和 ctime 一同更新。
1:2 # 目标文件:依赖文件
	echo hello # 表示若2文件的mtime比1文件的mtime新,就执行这个命令

可直接指定目标进行执行:make 1

# 伪目标
all: # 不存在真实目标文件all且未给出依赖文件,此时all是伪目标名
	echo hello

# 为了避免伪目标和真实目标文件同名的情况
.PHONY:clean
clean:
	...

捕获

test2.o:test2.c
	gcc -c -o test2.o test2.c
test1.o:test1.c
	gcc -e -o test1.o test1.c
test.bin:test1.o test2.o
	gcc -o test.bin testl.o test2.o
all:test.bin
	@echo "compile done"

此 makefile 还是蛮简单的,第 1~4 行都是在准备.o 目标文件,第 5~6 行是将.o 文件生成二进制文件test.bin。第 7 行的目标 all 是为了编译 test.bin,咱们要执府的命令是 make all,借此分析下 make 的执行流程。

  1. make 未找到文件 GNUmakefile,便继续找文件 makefile,找到后,根据命令的参数 all,从文件中找到 all 所在的规则
  2. make 发现 all 的依赖文件 test.bin 不存在,于是就去找以 test.bin 为目标文件的规则
  3. 在第 5 行终于找到了 test.bin 的规则,但 make 发现, test.bin 的依赖文件 test1.o 和 test2.o 都不存在,于是先去找以 test1.o 为目标文件的规则
  4. 同样经过千辛万苦,在第 3 行找到了生成 test1.o 的规则,但它的依赖文件是 test1.c ,由于 test1.o 本身不存在,所以不用再查看 test1.c 的 mtime,直接执行此规则的命令,即第 4 行的 gcc -c -o test1.o test1.c,用 test1.c 来编译 test1.o
  5. 生成 test1.o 后,执行流程返回到 test.bin 所在的规则,即第 5 行,此时 make 发现 test2.o 也不存在,于是继续递归查找目标 test2.o
  6. 同样,在第 1 行发现 test2.o 所在的规则,由于 test2.o 本身不存在,也不再检查其所依赖文件 test2.c的 mtime,直接执行规则中的编译命令 gcc -c -o test2.o test2.c 生成 test2.o
  7. 生成 test2.o。后,此时执行流程又回到了第 5 行, make 发现两个依赖文件 test1.o 和 test2.o 都准备齐了,于是执行本规则的命令,即第 6 行的 gcc-o test.bin test1.o test2.o,将这两个目标文件生成可执行文件test.bin
  8. test.bin 终于生成了,此时回到了第 2 步目标 all 所在的规则,于是执行所在规则中的命令@echo”compile done”,打印宇符串表示编译完成。提醒一下,虽然 all 被当作了真实目标文件来处理,但我们给出的命令并不是为了生成它,所以它同伪目标的作用类似,大伙儿不要感到奇怪。因为在前面我们己经解释过啦, make+makefile 并不是为了编译或生成文件,它们只是为了执行规则中的命令,无所谓命令是什么

makefile 变量定义的格式:变量名=值(字符串),多个值之间用空格分开。make 程序在处理时会用空格将值打散,然后遍历每一个值。另外,值仅支持字符串类型,即使是数字也被当作字符串来处理 。

变量引用的格式:$(变量名)。这样,每次引用变量时,变量名就会被其值(宇符串)替换。

注意,虽然变量的值会被当作字符串类型处理,但不能将其用双引号或单引号括起来,否则双引号或单引号也会被当作变量值的一部分。比如 var =’file.c’ 的值并不是 file.c ,而是 ‘file.c’ 。当引用变量$(var)做依赖文件时, make 会去找名为 ‘file.c’ 的目标,而不是 file.c 。

obj files = testl.o test2.o
test.bin:$(objfiles)
	gcc -o test.bin $(objfiles)

make 还支持一种自动化变量,此变量代表一组文件名,无论是目标文件名,还是依赖文件名,此变量值的范围属于这组文件名集合,也就是说,自动化变量相当于对文件名集合循环遍历一遍 。 对于不同的文件名集合,有不同的自动化变量名,下面列举一些。

  • @,表示规则中的目标文件名集合,如果存在多个目标文件, @,表示规则中的目标文件名集合,如果存在多个目标文件, @,表示规则中的目标文件名集合,如果存在多个目标文件,@则表示其中每一个文件名。助记,’@’很像是 at, aim at,表示瞄准目标 。
  • $<,表示规则中依赖文件中的第 1 个文件。助记,‘<’ 很像是集合的最左边,也就是第 1 个。
  • $^,表示规则中所有依赖文件的集合,如果集合中有重复的文件, $^ 会自动去重。助记, ‘^’ 很像从上往下罩的动作,能罩住很大的范围,所以称为集合。
  • $?,表示规则中,所有比目标文件 mtime 更新的依赖文件集合。助记,'?'表示疑问, make 最大的疑问就是依赖文件的 mtime 是否比目标文件的 mtime 要新。

实现内核使用的 ASSERT 断言

在断言打印信息前,应该把中断关掉

/* 定义中断的两种状态:
 * INTR_OFF值为0,表示关中断,
 * INTR_ON值为1,表示开中断 */
enum intr_status {		 // 中断状态
    INTR_OFF,			 // 中断关闭
    INTR_ON		         // 中断打开
};

#define EFLAGS_IF   0x00000200       // eflags寄存器中的if位为1
#define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl; popl %0" : "=g" (EFLAG_VAR))

/* 开中断并返回开中断前的状态*/
enum intr_status intr_enable() {
   enum intr_status old_status;
   if (INTR_ON == intr_get_status()) {
      old_status = INTR_ON;
      return old_status;
   } else {
      old_status = INTR_OFF;
      asm volatile("sti");	 // 开中断,sti指令将IF位置1
      return old_status;
   }
}

/* 关中断,并且返回关中断前的状态 */
enum intr_status intr_disable() {     
   enum intr_status old_status;
   if (INTR_ON == intr_get_status()) {
      old_status = INTR_ON;
      asm volatile("cli" : : : "memory"); // 关中断,cli指令将IF位置0
      return old_status;
   } else {
      old_status = INTR_OFF;
      return old_status;
   }
}

/* 将中断状态设置为status */
enum intr_status intr_set_status(enum intr_status status) {
   return status & INTR_ON ? intr_enable() : intr_disable();
}

/* 获取当前中断状态 */
enum intr_status intr_get_status() {
   uint32_t eflags = 0; 
   GET_EFLAGS(eflags);
   return (EFLAGS_IF & eflags) ? INTR_ON : INTR_OFF;
}

实现ASSERT

#ifndef __KERNEL_DEBUG_H
#define __KERNEL_DEBUG_H
void panic_spin(char* filename, int line, const char* func, const char* condition);

/***************************  __VA_ARGS__  *******************************
 * __VA_ARGS__ 是预处理器所支持的专用标识符。
 * 代表所有与省略号相对应的参数. 
 * "..."表示定义的宏其参数可变.*/
#define PANIC(...) panic_spin (__FILE__, __LINE__, __func__, __VA_ARGS__)
 /***********************************************************************/

#ifdef NDEBUG
   #define ASSERT(CONDITION) ((void)0) // 让ASSERT等于0,即删除ASSERT
#else
   #define ASSERT(CONDITION)                                      \
      if (CONDITION) {} else {                                    \
  /* 符号#让编译器将宏的参数转化为字符串字面量 */		  \
	 	PANIC(#CONDITION);                                       \
      }
#endif /*__NDEBUG */
#endif /*__KERNEL_DEBUG_H*/

我们传给 panic_spin 的其中一个参数是 __VA_ARGS_。同样作为参数的还有__FILE__ __LINE__ __func__ ,这三个是预定义的宏,分别表示被编译的文件名、被编译文件中的行号、被编译的函数名。

调用 PANIC 的形式为 PANIC(#CONDITION),即形参CONDITION,其中字符#的作用是让预处理器把 CONDITION 转换成字符串常量。比如 CONDITION 若为 var!= 0,#CONDITION的效果是变成了字符串 “var != 0”。

于是,传给 panic_spin 函数的第 4 个参数__VA_ARGS__实际类型为字符串指针。

/* 打印文件名,行号,函数名,条件并使程序悬停 */
void panic_spin(char* filename,	       \
                int line,	           \
                const char* func,      \
                const char* condition) \
{
    intr_disable();	// 因为有时候会单独调用panic_spin,所以在此处关中断。
    put_str("\n\n\n!!!!! error !!!!!\n");
    put_str("filename:");put_str(filename);put_str("\n");
    put_str("line:0x");put_int(line);put_str("\n");
    put_str("function:");put_str((char*)func);put_str("\n");
    put_str("condition:");put_str((char*)condition);put_str("\n");
    while(1);
}

内存地址池的概念是将可用的内存地址集中放到一个“池子”中,需要的时候直接从里面取出,用完后再放回去。由于在分页机制下有了虚拟地址和物理地址,为了有效地管理它们,我们需要创建虚拟内存地址池和物理内存地址池。

image-20230812192355839

如何规划物理内存池。

操作系统为了能够正常运行,需要自己预留一部分给自己使用,所以划分两个内存池分别给内核和用户进程使用

内存池中的内存也得按单位大小来获取,这个单位大小是 4kb,称为页,故,内存池中管理的是一个个大小为 4kb 的内存块,从内存池中获取的内存大小至少为 4kb 或者为 4KB 的倍数

虚拟内存地址池。

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

image-20230812192412382

/* 虚拟地址池,用于虚拟地址管理 */
struct virtual_addr {
    struct bitmap vaddr_bitmap; // 虚拟地址用到的位图结构 
    uint32_t vaddr_start;       // 虚拟地址起始地址
};


#define PG_SIZE 4096 // 页的尺寸

/***************  位图地址 ********************
 * 因为0xc009f000是内核主线程栈顶,0xc009e000是内核主线程的pcb.
 * 一个页框大小的位图可表示128M内存, 一页大小为 0x1000,再减去4页,位图位置安排在地址0xc009a000,
 * 这样本系统最大支持4个页框的位图,即512M */
#define MEM_BITMAP_BASE 0xc009a000
/*************************************/

/* 0xc0000000是内核从虚拟地址3G起. 0x100000意指跨过低端1M内存,使虚拟地址在逻辑上连续 */
#define K_HEAP_START 0xc0100000

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

struct pool kernel_pool, user_pool;      // 生成内核内存池和用户内存池
struct virtual_addr kernel_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;	  // 页表大小= 页的尺寸*页的数量
    // 第0和第768个页目录项指向同一个页表,第769~1022个页目录项共指向254个页表,共256个页框
    uint32_t used_mem = page_table_size + 0x100000;	  // 0x100000为低端1M内存
    uint32_t free_mem = all_mem - used_mem;
    uint16_t all_free_pages = free_mem / PG_SIZE;		  // 1页为4k,不管总内存是不是4k的倍数,
    // 对于以页为单位的内存分配策略,不足1页的内存不用考虑了。
    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; // Kernel BitMap的长度,位图中的一位表示一页,以字节为单位
    uint32_t ubm_length = user_free_pages / 8; // User BitMap的长度.

    uint32_t kp_start = used_mem;				  // Kernel Pool start,内核内存池的起始地址
    uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE;	  // User Pool start,用户内存池的起始地址

    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;

    /*********    内核内存池和用户内存池位图   ***********
 *   位图是全局的数据,长度不固定。
 *   全局或静态的数组需要在编译时知道其长度,
 *   而我们需要根据总内存大小算出需要多少字节。
 *   所以改为指定一块内存来生成位图.
 *   ************************************************/
    // 内核使用的最高地址是0xc009f000,这是主线程的栈地址.(内核的大小预计为70K左右)
    // 32M内存占用的位图是2k.内核内存池的位图先定在MEM_BITMAP_BASE(0xc009a000)处.
    kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE;

    /* 用户内存池的位图紧跟在内核内存池位图之后 */
    user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length);
    /******************** 输出内存池信息 **********************/
    put_str("      kernel_pool_bitmap_start:");put_int((int)kernel_pool.pool_bitmap.bits);
    put_str(" kernel_pool_phy_addr_start:");put_int(kernel_pool.phy_addr_start);
    put_str("\n");
    put_str("      user_pool_bitmap_start:");put_int((int)user_pool.pool_bitmap.bits);
    put_str(" user_pool_phy_addr_start:");put_int(user_pool.phy_addr_start);
    put_str("\n");

    /* 将位图置0*/
    bitmap_init(&kernel_pool.pool_bitmap);
    bitmap_init(&user_pool.pool_bitmap);

    /* 下面初始化内核虚拟地址的位图,按实际物理内存大小生成数组。*/
    kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_length;      // 用于维护内核堆的虚拟地址,所以要和内核内存池大小一致

    /* 位图的数组指向一块未使用的内存,目前定位在内核内存池和用户内存池之外*/
    kernel_vaddr.vaddr_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length + ubm_length);

    kernel_vaddr.vaddr_start = K_HEAP_START; // 虚拟内存池起始地址
    bitmap_init(&kernel_vaddr.vaddr_bitmap); // 虚拟内存池位图置0
    put_str("   mem_pool_init done\n");
}

/* 内存管理部分初始化入口 */
void mem_init() {
    put_str("mem_init start\n");
    // 三种BIOS方法获得内存容量,存入汇编变量total_mem_bytes中,物理地址为0xb00
    uint32_t mem_bytes_total = (*(uint32_t*)(0xb00)); // mem_bytes_total用于存储机器上安装的物理内存总量
    mem_pool_init(mem_bytes_total);	  // 初始化内存池
    put_str("mem_init done\n");
}

分配页内存

/* 内存池标记,用于判断用哪个内存池 */
enum pool_flags {
    PF_KERNEL = 1,    // 内核内存池
    PF_USER = 2	     // 用户内存池
};
// 页表项或页目录项的属性
#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 属性位值, 用户级



#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22) // 返回虚拟地址的高10位,即pde索引部分,此部分用于在页目录表中定位pde
#define PTE_IDX(addr) ((addr & 0x003ff000) >> 12) // 返回虚拟地址的中间10位,即pte索引部分,此部分用于在页表中定位pte

/* 在内核/用户虚拟内存池中连续申请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);// 将相应位置置1
        }
        vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
    } else {	
        // 用户内存池,将来实现用户进程再补充
    }
    return (void*)vaddr_start;
}

/* 得到虚拟地址vaddr对应的pte指针,实际返回的是能够访问vaddr所在pte的虚拟地址。*/
uint32_t* pte_ptr(uint32_t vaddr) {
    /* 先访问到页表自己 + \
    * 再用页目录项pde(页目录内页表的索引)做为pte的索引访问到页表 + \
    * 再用pte的索引做为页内偏移*/
    uint32_t* pte = (uint32_t*)(0xffc00000 + \  //高10位指向最后一个页目录项,让处理器自动在最后一个pde中取出页目录表物理地址
                                ((vaddr & 0xffc00000) >> 10) + \ //获取vaddr的页目录表索引,将参数vaddr的高10位(pde索引〉取出来,做新地址new_vaddr的中间10位(pte索引)
                                //处理器以为获得的是pte中的普通物理页地址,但其实是vaddr中高10位的pde索引所对应的pde里保存的页表的物理地址
                                PTE_IDX(vaddr) * 4); //处理器需要页内偏移地址(低12位),但其实是vaddr中的页表索引(页表偏移地址,中间10位),页表索引乘4补上
    return pte;
}

/* 得到虚拟地址vaddr对应的pde的指针 */
uint32_t* pde_ptr(uint32_t vaddr) {
    /* 0xfffff是用来访问到页表本身所在的地址 */
    uint32_t* pde = (uint32_t*)((0xfffff000) + PDE_IDX(vaddr) * 4);
    return pde;
}

/* 在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 = (m_pool->phy_addr_start + (bit_idx * PG_SIZE));//物理页地址 = 物理内存池的起始地址+物理页在内存池中的偏移地址
    return (void*)page_phyaddr;
}

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

    /************************   注意   *************************
 * 执行*pte,会访问到空的pde。所以确保pde创建完成后才能执行*pte,
 * 否则会引发page_fault。因此在*pde为0时,*pte只能出现在下面else语句块中的*pde后面。
 * *********************************************************/
    /* 先在页目录内判断目录项的P位,若为1,则表示该表已存在 */
    if (*pde & 0x00000001) {	 // 页目录项和页表项的第0位为P,此处判断目录项是否存在
        ASSERT(!(*pte & 0x00000001));

        if (!(*pte & 0x00000001)) {   // 只要是创建页表,pte就应该不存在,多判断一下放心
            *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);    // US=1,RW=1,P=1
        } else { //应该不会执行到这,因为上面的ASSERT会先执行。
            PANIC("pte repeat");
            *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);      // US=1,RW=1,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_phyaddr对应的物理内存清0,
       * 避免里面的陈旧数据变成了页表项,从而让页表混乱.
       * 访问到pde对应的物理地址,用pte取高20位便可.
       * 因为pte是基于该pde对应的物理地址内再寻址,
       * 把低12位置0便是该pde对应的物理页的起始*/
        memset((void*)((int)pte & 0xfffff000), 0, PG_SIZE);

        ASSERT(!(*pte & 0x00000001));
        *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);      // US=1,RW=1,P=1
    }
}

/* 分配pg_cnt个页空间,成功则返回起始虚拟地址,失败时返回NULL */
void* malloc_page(enum pool_flags pf, uint32_t pg_cnt) {
    ASSERT(pg_cnt > 0 && pg_cnt < 3840);//监督申请的内存页数是否超过了物理内存池的容量
    /***********   malloc_page的原理是三个动作的合成:   ***********
      1通过vaddr_get在虚拟内存池中申请虚拟地址
      2通过palloc在物理内存池中申请物理页
      3通过page_table_add将以上得到的虚拟地址和物理地址在页表中完成映射
***************************************************************/
    void* vaddr_start = vaddr_get(pf, pg_cnt);//因为虚拟地址是连续的,一次性申请虚拟地址
    if (vaddr_start == NULL) {
        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);
        if (page_phyaddr == NULL) {  // 失败时要将曾经已申请的虚拟地址和物理页全部回滚,在将来完成内存回收时再补充
            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) {	   // 若分配的地址不为空,将页框清0后返回
        memset(vaddr, 0, pg_cnt * PG_SIZE);//虚拟地址连续,所以直接用pg_cnt乘以PG_SIZE表示需置零的字节数
    }
    return vaddr;
}
int main(void) {
    put_str("I am kernel\n");
    init_all();

    void* addr = get_kernel_pages(3);
    put_str("\n get_kernel_page start vaddr is ");
    put_int((uint32_t)addr);
    put_str("\n");

    while(1);
    return 0;
}

一般的内存管理系统所管理的是那些空闲的内存,即己被使用的内存是不在内存池中的,"己使用的内存"当然包括内存管理相关数据结构所占的内存,位图就是用于管理内存的数据结构,这也是位图地址选为 0xc009a000 的原因,此地址位于低端 1MB 之内,这里面的内存几乎都被占用了,因此我们就不用考虑它占用的内存了

页表的作用是将虚拟地址转换成物理地址,其转换过程中涉及访问的页目录表、页目录项及页表项,都是通过真实物理地址访问的,否则若用虚拟地址访问它们的话,会陷入转换的死循环中不可自拔。

虚拟地址和物理地址的映射关系是在页表中完成的,本质上是在页表中添加此虚拟地址对应的页表项 pte,并把物理页的物理地址写入此页表项 pte 中。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SprintBoot Bean管理

SpringBoot中获取Bean对象 下面这段代码在测试类中进行&#xff0c;下面通过三种方式获取bean对象。 import org.springframework.context.ApplicationContext;// 注意一定要引入上面的依赖SpringBootTest class TliasWebManagementApplicationTests {Testvoid getBeanTest(){…

AI夏令营第三期 - 基于论文摘要的文本分类与关键词抽取挑战赛笔记

赛题&#xff1a;基于论文摘要的文本分类与关键词抽取 背景&#xff1a;高效的从海量医学文献中提取疾病诊断和治疗关键信息 任务&#xff1a;通过论文摘要判断论文是否为医学文献 样例 数据集&#xff1a;csv文件&#xff0c;字段&#xff1a;标题、作者、摘要、关键词 评价指…

uniapp 引入海康H5player实现视频监控的播放

uniapp直接调用海康H5player方法&#xff0c;只能在web浏览器页面正常播放&#xff0c;实机运行会因为找不到文件的相对路径而报错无法播放。因此需要通过web-view或iframe引入html的方式来实现实时视频监控的播放。具体步骤如下&#xff1a; 1、首先将海康h5player的相关文件…

服装定制小程序的秘诀

随着互联网的快速发展&#xff0c;越来越多的企业开始关注互联网商业模式的创新。其中&#xff0c;定制化服务成为了各行各业的关注焦点之一。尤其是在服装行业&#xff0c;定制化服装已经成为许多消费者的追求。而面对这一市场需求&#xff0c;如何创造成功的互联网新商业模式…

https非对称加密算法

非对称加密算法原理 在客户端公开公钥&#xff0c;服务端保存私钥 1.客户端第一次请求先请求443端口&#xff0c;从443端口下载公钥。 2.客户端将数据进行公钥算法进行加密&#xff0c;将秘文发送到服务端 服务端收到秘文后&#xff0c;通过私钥算法进行解密得到明文数据。…

【STM32】开发方式:寄存器、CMSIS、SPL、HAL、LL、RTOS

阅读本专栏其他文章&#xff0c;有助于理解本文。 文章目录 一、开发库选择1.1 概述1.2 CMSIS库1.3 SPL库1.4 HAL 库1.5 LL库1.6 寄存器开发 二、代码对比2.1 使用寄存器2.2 使用CMSIS库2.3 使用SPL库2.4 使用HAL库2.5 使用LL库2.6 使用RTOS 三、软件配置 一、开发库选择 1.1 …

【linux】使用rpm下载mysql

1/ 2/ 3/ 4/ 5/ 6/ 7/ 8/ 9/ 10/ 11/ 12/ 13/

JDBC回顾

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 JDBC回顾 前言一、JDBC1.JDBC是什么&#xff1f;2.如何使用&#xff1f;&#xff08;1&#xff09;注册驱动&#xff08;2&#xff09;获取连接&#xff08;3&#xff09;操作…

二叉树题目:二叉树的层序遍历

文章目录 题目标题和出处难度题目描述要求示例数据范围 解法思路和算法代码复杂度分析 题目 标题和出处 标题&#xff1a;二叉树的层序遍历 出处&#xff1a;102. 二叉树的层序遍历 难度 4 级 题目描述 要求 给你二叉树的根结点 root \texttt{root} root&#xff0c;返…

c++ day1

作业: 1&#xff0e;整理思维导图 2.定义一个命名空间Myspace&#xff0c;包含以下函数:将一个字符串中的所有单词进行反转&#xff0c;并输出反转后的结果。例如&#xff0c;输入字符串 为"Hello World"&#xff0c;输出结果为"olleH dlroW"&#xff0c;并…

python:pyqt5 + cef 模仿 mdict 界面

PyQt5 安装参见&#xff1a;python&#xff1a;PyQt5 简单示例 cefpython 入门 参考: Python GUI: cefpython3的简单分析和应用 pip install cefpython3 cefpython3-66.1-py2.py3-none-win_amd64.whl (69.0 MB) Successfully installed cefpython3-66.1 cd \Python37\Lib\si…

prometheus blackbox_exporter安装

目录 一、准备工作1.1 安装或关闭以下服务1.2 本次安装环境 二、安装blackbox_exporter2.1 下载并解压2.2配置2.3测试 三、配置blackbox_exporter3.1配置blackbox.yml3.2 开启blackbox_exporter3.3配置prometheus.yml 四、其他4.1server returned HTTP status 400 Bad Request …

Docker 存储驱动解析:选择最适合你的存储方案

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

分类预测 | MATLAB实现NGO-DBN北方苍鹰优化深度置信网络多特征输入分类预测

分类预测 | MATLAB实现NGO-DBN北方苍鹰优化深度置信网络多特征输入分类预测 目录 分类预测 | MATLAB实现NGO-DBN北方苍鹰优化深度置信网络多特征输入分类预测效果一览基本介绍模型描述程序设计参考资料 效果一览 基本介绍 MATLAB实现NGO-DBN北方苍鹰优化深度置信网络多特征输入…

【QT】重写QAbstractLIstModel,使用ListView来显示多列数据

qt提供了几个视图来进行信息的列表显示&#xff0c;QListView可以用来显示继承QStractListModel的字符串列表中的字符串&#xff0c;默认的模型里面只包含一列的内容&#xff1a; 这里以qml为例子&#xff0c;先新建一个qml的项目&#xff0c;示例代码如下&#xff1a; 先创建一…

查看所有数据库各表容量大小

查看所有数据库各表容量大小 1. 查看所有数据库各表容量大小2.查看指定数据库容量大小3. 查看所有数据库容量大小 1. 查看所有数据库各表容量大小 select table_schema as 数据库, table_name as 表名, table_rows as 记录数, truncate(data_length/1024/1024, 2) as 数据容量…

Redis 持久化的手段有哪些 ?RDB 和 AOF 有什么区别 ?

目录 1. Redis 持久化的手段有哪些 2. RDB 和 AOF 有什么区别 2.1 RDB 持久化 2.2 AOF 持久化 2.2.1 AOF 持久化策略有哪些 3. 混合持久化是如何执行的&#xff08;了解&#xff09; 1. Redis 持久化的手段有哪些 Redis 持久化的手段有三种&#xff1a; 快照方式&#…

模型预测笔记(二):结合SMOTE来进行数据不均衡处理实操

文章目录 数据不均衡危害如何解决SMOTE原理代码效果 数据不均衡危害 在模型预测中&#xff0c;数据不均衡是指不同类别的样本数量差异很大。这种情况可能会对模型的性能和结果产生一些危害&#xff1a; 偏斜的预测结果&#xff1a;由于某些类别的样本数量较少&#xff0c;模型…

Rides分布式缓存

分布式缓存 -- 基于Redis集群解决单机Redis存在的问题 单机的Redis存在四大问题&#xff1a; 1.Redis持久化 Redis有两种持久化方案&#xff1a; RDB持久化 AOF持久化 1.1.RDB持久化 RDB全称Redis Database Backup file&#xff08;Redis数据备份文件&#xff09;&#x…

从Web 2.0到Web 3.0,互联网有哪些变革?

文章目录 Web 2.0时代&#xff1a;用户参与和社交互动Web 3.0时代&#xff1a;语义化和智能化影响和展望 &#x1f389;欢迎来到Java学习路线专栏~从Web 2.0到Web 3.0&#xff0c;互联网有哪些变革&#xff1f; ☆* o(≧▽≦)o *☆嗨~我是IT陈寒&#x1f379;✨博客主页&#x…