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

news/2024/7/7 20:02:16

在 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://lihuaxi.xjx100.cn/news/1471498.html

相关文章

行业追踪,2023-08-22

自动复盘 2023-08-22 凡所有相&#xff0c;皆是虚妄。若见诸相非相&#xff0c;即见如来。 k 线图是最好的老师&#xff0c;每天持续发布板块的rps排名&#xff0c;追踪板块&#xff0c;板块来开仓&#xff0c;板块去清仓&#xff0c;丢弃自以为是的想法&#xff0c;板块去留让…

Reids 的整合 Spring Data Redis使用

大家好 , 我是苏麟 , 今天带来强大的Redis . REmote DIctionary Server(Redis) 是一个由 Salvatore Sanfilippo 写的 key-value 存储系统&#xff0c;是跨平台的非关系型数据库。 Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选…

docker之Consul环境的部署

目录 一.Docker consul的介绍 1.1template模板(更新) 1.2registrator&#xff08;自动发现&#xff09; 1.3agent(代理) 二.consul的工作原理 三.Consul的特性 四.Consul的使用场景 五.搭建Consul的集群 5.1需求 5.2部署consul 5.3主服务器[192.168.40.20] 5.4client部署&…

Vim学习(四)——命令使用技巧

命令模式 打开文本默认模式&#xff0c;按**【ESC】**重新进入 【/关键字】&#xff1a;搜索匹配关键字 G&#xff1a;最后一行 gg&#xff1a;第一行 hjkl:左下右上 yy: 复制一行 dd&#xff1a;删除一行 p:粘贴 u: 撤销插入模式 按**【i / a / o】**键均可进入文本编辑模式…

Linux 编译内核模块出现--Unknown symbol mcount

文章目录 Linux suse&#xff1a; # cat /etc/os-release NAME"SLES" VERSION"12-SP2" VERSION_ID"12.2" PRETTY_NAME"SUSE Linux Enterprise Server 12 SP2" ID"sles" ANSI_COLOR"0;32" CPE_NAME"cpe:/o:s…

深入理解CPU密集型与IO密集型任务、线程池如何选择?

目录 1、理解什么是CPU密集型与IO密集型 1.1 CPU密集型 概念解释 策略 1.2 I/O密集型 概念解释 策略 2、《Java并发编程实践》&#xff1a;具体的线程数该如何设置&#xff1f;线程池参数如何设置&#xff1f; 参数解释 3、总结 1、理解什么是CPU密集型与IO密集型 1.…

【Unity细节】Unity制作汽车时,为什么汽车会被弹飞?为什么汽车会一直抖动?

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 秩沅 原创 &#x1f636;‍&#x1f32b;️收录于专栏&#xff1a;unity细节和bug &#x1f636;‍&#x1f32b;️优质专栏 ⭐【…

设计师私藏的5个设计网站,你一定要知道。

这5个网站设计师都在用&#xff0c;找素材、找灵感一步到位&#xff0c;赶紧收藏~ 菜鸟图库 https://www.sucai999.com/?vNTYxMjky 菜鸟图库是一个非常大的素材库&#xff0c;站内包含设计、办公、自媒体、图片、电商等各行业素材。网站还为新手设计师提供免费的素材&#xf…