《操作系统真象还原》第十一章——用户进程
文章目录
- 前言
- 为什么要有TSS
- TSS简介
- TSS描述符和TSS结构
- 现代操作系统采用的任务切换方式
- 定义并初始化TSS
- 修改global.h
- 编写tss.c
- 测试
- 实现用户进程
- 实现用户进程的原理
- 维护虚拟地址空间,修改thread.h
- 为进程创建页表和3特权级栈,修改memory.c
- 进入特权级3
- 用户进程创建的流程
- 实现用户进程——上
- 修改global.h
- 编写process.c
- bss简介
- 实现用户进程——下
- 完整的process.c
- 对应的process.h
- 用户进程的调度
- 修改schedule函数
- 测试用户进程
- main.c
- makefile
- 结果截图
- 结语
前言
上章博客链接:
本章研究用户进程,内容分为三节:为什么要有tss,定义并初始化tss,实现用户进程。实现用户进程下设10个小节。第一部分中,ldt相关知识和cpu原生多任务,我们先略过。
为什么要有TSS
TSS简介
TSS是Task State Segment的缩写,即任务状态段。
原文:CPU执行任务时,需要把任务运行所需要的数据加载到寄存器、栈和内存中。于是问题来了,任务的数据和指令是CPU的处理对象,任务的执行要占用一套存储资源,如寄存器和内存,这些存储资源中装的是任务的数据和指令,它们属于 CPU的大餐,但CPU很不情愿直接用内存这个低速的、不方便的容器就餐,它最喜欢的容器是寄存器, 因为它的速度和CPU很般配,这才能让CPU吃得更爽。因此内存中的数据往往被加载到高速的寄存器后 再处理,处理完成后,再将结果回写到低速的内存中,所以,任何时候,寄存器中的内容才是任务的最新 状态。采取轮流使用CPU的方式运行多任务,当前任务在被换下CPU时,任务的最新状态,也就是寄存 器中的内容应该找个地方保存起来,以便下次重新将此任务调度到CPU上时可以恢复此任务的最新状态, 这样任务才能继续执行,否则就出错了。
人们给每个任务都关联了一个任务状态段。当加载新任务时,CPU 自动把当前任务 (旧任务)的状态存入当前任务的TSS,然后将新任务TSS中的数据载入到对应的寄存器中,这就实现了任务 切换。TSS就是任务的代表,CPU用不同的TSS区分不同的任务,因此任务切换的本质就是TSS的换来换去。
以上是理想情况,和实际不同。
TSS描述符和TSS结构
tss本质是内存上的一段区域,同样有tss的段描述符,称为tss描述符,同样保存在gdt里,结构如下。
重点解释以下type段b位。B表示busy位,B位为0时,表示任务不繁忙,B位为1时,表示任务繁忙。
任务繁忙有两方面的含义,一方面就是指此任务是否为当前正在CPU上运行的任务。另一方面是指 此任务嵌套调用了新的任务,CPU正在执行新任务,此任务暂时挂起,等新任务执行完成后CPU会回到 此任务继续执行,所以此任务马上就会被调度执行了。
B位存在的意义可不是单纯为了表示任务忙不忙,而是为了给当前任务打个标记,目的是避免当前任 务调用自己,也就是说任务是不可重入的。
以下是tss的结构截图
主要再说明一下三个特权级切换栈吧。三组栈用来保存特权级切换时目标栈的栈指针。首先,除非从中断返回,cpu不允许直接从高特权级转移到低特权级。所以我们不会从比3更低的特权级进入3,因而没有ss3和esp3。
我们的操作系统效仿linux,只用到0和3两个特权级,只要设置ss0和esp0即可。
有专门的tss寄存器tr,用来保存tss的起始地址和偏移大小。
将tss加载到寄存器TR的 指令是ltr,其指令格式为:ltr “16位通用寄存器”或“16位内存单元”
。寄存器或内存单元必须是gdt的选择子
现代操作系统采用的任务切换方式
现代操作系统为什么不用多个TSS实现任务切换?
简而言之,效率低,便携差,不够灵活。
结论:我们使用TSS唯一的理由是为0特权级的任务提供栈。
Linux为每个CPU创建一个TSS,在各个CPU上的所有任务共享同一 个TSS,各 CPU的TR寄存器保存各CPU上的TSS,在用 ltr指令加载TSS后,该TR寄存器永远指向同 一个TSS,之后再也不会重新加载TSS。
Linux 在 TSS中只初始化了SS0、esp0和I/O位图字段,除此之外TSS便没用了,就是个空架子,不 再做保存任务状态之用。 那任务的状态信息保存在哪里呢? 是这样的,当CPU由低特权级进入高特权级时,CPU会“自动”从TSS中获取对应高特权级的栈指 针(TSS是CPU内部框架原生支持的嘛,当然是自动从中获取新的栈指针)。我们具体说一下,Linux只 用到了特权3级和特权0级,因此CPU从3特权级的用户态进入0特权级的内核态时(比如从用户进程 进入中断),CPU自动从当前任务的TSS中获取SS0和esp0字段的值作为0特权级的栈,然后Linux“手 动”执行一系列的push指令将任务的状态的保存在0特权级栈中,也就是TSS中SS0和esp0所指向的栈。
总结:任务信息一样存在栈里。
定义并初始化TSS
修改global.h
在kenel目录下。
// 这个文件定义了段选择子的属性和IDT描述符的属性
#ifndef __KERNEL_GLOBAL_H
#define __KERNEL_GLOBAL_H
#include "../lib/kernel/stdint.h"
/*----------GDT段描述符属性----------*/
#define DESC_G_4K 1 // 粒度为4k
#define DESC_D_32 1 // 操作数为32位
#define DESC_L 0 // 64位模式
#define DESC_AVL 0 // 软件位,置0
#define DESC_P 1 // 段是否有效
#define DESC_DPL_0 0 // 4个特权级
#define DESC_DPL_1 1
#define DESC_DPL_2 2
#define DESC_DPL_3 3
/*S说明:代码段和数据段是存储段,tss和门描述符属于系统段*S为1时表示存储段,S为0时表示系统段*/
#define DESC_S_CODE 1
#define DESC_S_DATA 1
#define DESC_S_SYS 0#define DESC_TYPE_CODE 8 // 1000
#define DESC_TYPE_DATA 2 // 0010
#define DESC_TYPE_TSS 9 // 1001/*----------段选择子属性----------*/
#define RPL0 0 // 段特权级
#define RPL1 1
#define RPL2 2
#define RPL3 3#define TI_GDT 0
#define TI_LDT 1#define SELECTOR_K_CODE ((1 << 3) + (TI_GDT << 2) + RPL0) // 内核代码段
#define SELECTOR_K_DATA ((2 << 3) + (TI_GDT << 2) + RPL0) // 内核数据段
#define SELECTOR_K_STACK SELECTOR_K_DATA // 内核栈段,和数据段用一个内存段
#define SELECTOR_K_GS ((3 << 3) + (TI_GDT << 2) + RPL0) // 显存段// GDT四号位置是tss
#define SELECTOR_U_CODE ((5 << 3) + (TI_GDT << 2) + RPL3) // 用户代码段
#define SELECTOR_U_DATA ((6 << 3) + (TI_GDT << 2) + RPL3) // 用户数据段
#define SELECTOR_U_STACK SELECTOR_U_DATA // 用户栈段/*----------64位GDT描述符----------*/
// 把上面定义的GDT位属性连接起来,构建64位的GDT描述符
#define GDT_ATTR_HIGH \((DESC_G_4K << 7) + (DESC_D_32 << 6) + (DESC_L << 5) + (DESC_AVL << 4))
#define GDT_CODE_ATTR_LOW_DPL3 \((DESC_P << 7) + (DESC_DPL_3 << 5) + (DESC_S_CODE << 4) + DESC_TYPE_CODE)
#define GDT_DATA_ATTR_LOW_DPL3 \((DESC_P << 7) + (DESC_DPL_3 << 5) + (DESC_S_DATA << 4) + DESC_TYPE_DATA)/*----------TSS描述符属性----------*/
// 专门定义一下TSS属性
#define TSS_DESC_D 0
#define TSS_ATTR_HIGH \((DESC_G_4K << 7) + (TSS_DESC_D << 6) + (DESC_L << 5) + (DESC_AVL << 4) + 0x0)
#define TSS_ATTR_LOW \((DESC_P << 7) + (DESC_DPL_0 << 5) + (DESC_S_SYS << 4) + DESC_TYPE_TSS)
#define SELECTOR_TSS ((4 << 3) + (TI_GDT << 2) + RPL0)/*----------定义GDT描述符的结构----------*/
struct gdt_desc
{uint16_t limit_low_word; // 低16位段界限uint16_t base_low_word; // 低16位段基址uint8_t base_mid_byte; // 中8位段基址uint8_t attr_low_byte; // 8位段属性uint8_t limit_high_byte_and_attr_high_byte; // 4位段界限+4位段属性uint8_t base_hight_byte; // 高8位段基址
};/*----------IDT中断描述符表属性----------*/
#define IDT_DESC_P 1
#define IDT_DESC_DPL0 0
#define IDT_DESC_DPL1 1
#define IDT_DESC_DPL2 2
#define IDT_DESC_DPL3 3
#define IDT_DESC_32_TYPE 0xE // 32位的门
#define IDT_DESC_16_TYPE 0x6 // 16位的门,不会用到,定义它只为和 32 位门区分#define IDT_DESC_ATTR_DPL0 ((IDT_DESC_P << 7) + (IDT_DESC_DPL0 << 5) + IDT_DESC_32_TYPE)
#define IDT_DESC_ATTR_DPL3 ((IDT_DESC_P << 7) + (IDT_DESC_DPL3 << 5) + IDT_DESC_32_TYPE)/*----------用户进程相关属性----------*/
#define EFLAGS_MBS (1 << 1) // 此项必须要设置
#define EFLAGS_IF_1 (1 << 9) // if位1,屏蔽中断信号,关中断
#define EFLAGS_IF_0 0 // 开中断
#define EFLAGS_IOPL_3 (3 << 12) // 测试用,允许用户进程跳过系统调用直接操作硬件
#define EFLAGS_IOPL_0 (0 << 12) // 默认状态
#define DIV_ROUND_UP(X, STEP) ((X + STEP - 1) / STEP) // 实现除法后向上取整
#define PG_SIZE 4096
#endif
编写tss.c
我们创建一个新的文件夹userprog,用户进程相关文件都放在这里。
#include "./tss.h"
#include "../lib/kernel/stdint.h"
#include "../kernel/global.h"
#include "../lib/kernel/print.h"
#include "../lib/string.h"#define PG_SIZE 4096 // 标准页大小
#define GDT_BASE_ADDR 0xc0000903 // gdt基地址,可以用info gdt查看
/*TSS结构,由程序员定义,CPU维护*/
struct tss
{uint32_t backlink; // 上一个TSS的指针uint32_t *esp0; // 特权级栈uint32_t ss0;uint32_t *esp1;uint32_t ss1;uint32_t *esp2;uint32_t ss2;uint32_t cr3;uint32_t (*eip)(void); // 各种寄存器uint32_t eflags;uint32_t eax;uint32_t ecx;uint32_t edx;uint32_t ebx;uint32_t esp;uint32_t ebp;uint32_t esi;uint32_t edi;uint32_t es;uint32_t cs;uint32_t ss;uint32_t ds;uint32_t fs;uint32_t gs;uint32_t ldt; // ldtuint32_t trace;uint32_t io_base; // io位图
};
static struct tss tss;/*更新tss中esp0字段的值为 pthread的0级栈*/
void upedate_tss_esp(struct task_struct *pthread)
{tss.esp0 = (uint32_t *)((uint32_t)pthread + PG_SIZE);
}/*创建gdt描述符*/
static struct gdt_desc make_gdt_desc(uint32_t *desc_addr, uint32_t limit,uint8_t attr_low, uint8_t attr_high)
{uint32_t desc_base = (uint32_t)desc_addr;struct gdt_desc desc;desc.limit_low_word = limit & 0x0000ffff;desc.base_low_word = desc_base & 0x0000ffff;desc.base_mid_byte = ((desc_base & 0x00ff0000) >> 16);desc.attr_low_byte = (uint8_t)attr_low;desc.limit_high_byte_and_attr_high_byte =(((limit & 0x00ff0000) >> 16) + (uint8_t)attr_high);desc.base_hight_byte = ((desc_base & 0xff000000) >> 24);return desc;
}/*在gdt中创建tss并重新加载gdt*/
void tss_init()
{put_str("tss_init start!\n");uint32_t tss_size = sizeof(tss);memset(&tss, 0, tss_size);tss.ss0 = SELECTOR_K_STACK; // 0特权级栈就是内核栈,将选择子赋给ss0字段tss.io_base = tss_size;// 注册TSS*((struct gdt_desc *)(GDT_BASE_ADDR + 0x20)) =make_gdt_desc((uint32_t *)&tss, tss_size - 1, TSS_ATTR_LOW, TSS_ATTR_HIGH);// 注册用户级代码段和数据段*((struct gdt_desc *)(GDT_BASE_ADDR + 0x28)) =make_gdt_desc((uint32_t *)0, 0xfffff, GDT_CODE_ATTR_LOW_DPL3, GDT_ATTR_HIGH);*((struct gdt_desc *)(GDT_BASE_ADDR + 0x30)) =make_gdt_desc((uint32_t *)0, 0xfffff, GDT_DATA_ATTR_LOW_DPL3, GDT_ATTR_HIGH);/*构建GDT的操作数,用于LGDT指令,传递信息到GDTR寄存器*结构:|16位GDT限长 (Limit)|32位 GDT基地址(Base Address)|16位保留(通常为0)|*Limit类似数组下标,需要-1*/uint64_t gdt_operand =((8 * 7 - 1) | ((uint64_t)(uint32_t)GDT_BASE_ADDR << 16));// 将GDT信息和TSS信息用lgdt和ltr指令分别写入GDTR和TR寄存器asm volatile("lgdt %0" : : "m"(gdt_operand));asm volatile("ltr %w0" : : "r"(SELECTOR_TSS));put_str("tss_init done!\n");
}
对应的tss.h,比较短。
#ifndef __USERPROG_TSS_H
#define __USERPROG_TSS_H
void upedate_tss_esp(struct task_struct *pthread);
void tss_init();
#endif
编码部分基本完成。
测试
首先是修改init
// 完成所有的初始化工作
#include "./init.h"
#include "../lib/kernel/stdint.h"
#include "../lib/kernel/print.h"
#include "./interrupt.h"
#include "../device/timer.h"
#include "./memory.h"
#include "../thread/thread.h"
#include "../device/console.h"
#include "../device/keyboard.h"
#include "../userprog/tss.h"/*负责初始化所有模块 */
void init_all()
{put_str("init_all\n");idt_init(); // 中断初始化mem_init(); // 内存初始化timer_init(); // 定时器初始化thread_init(); // 线程初始化console_init(); // 控制台初始化,最好放到开中断之前keyboard_init(); // 键盘初始化tss_init(); // TSS和GDT初始化
}
main.c需要把中断关闭,也不需要多线程。
makefile:
BUILD_DIR = ./build
ENTRY_POINT = 0xc0001500
AS = nasm
CC = gcc
LD = ld
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/ -I thread/ -I userprog
ASFLAGS = -f elf
CFLAGS = -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
LDFLAGS = -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \$(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o \$(BUILD_DIR)/debug.o $(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o \$(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o $(BUILD_DIR)/list.o \$(BUILD_DIR)/switch.o $(BUILD_DIR)/sync.o $(BUILD_DIR)/console.o \$(BUILD_DIR)/keyboard.o $(BUILD_DIR)/ioqueue.o $(BUILD_DIR)/tss.o \################ c代码编译 ##################
$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h \lib/kernel/stdint.h kernel/init.h kernel/debug.h \kernel/memory.h thread/thread.h kernel/interrupt.h \device/console.h device/ioqueue.h device/keyboard.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/init.o: kernel/init.c kernel/init.h lib/kernel/print.h \lib/kernel/stdint.h kernel/interrupt.h device/timer.h \kernel/memory.h thread/thread.h device/console.h \device/keyboard.h userprog/tss.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/interrupt.o: kernel/interrupt.c kernel/interrupt.h \lib/kernel/stdint.h kernel/global.h kernel/io.h \lib/kernel/print.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/timer.o: device/timer.c device/timer.h lib/kernel/stdint.h \kernel/io.h lib/kernel/print.h kernel/interrupt.h \thread/thread.h kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/debug.o: kernel/debug.c kernel/debug.h \lib/kernel/print.h kernel/interrupt.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/string.o: lib/string.c lib/string.h \kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/memory.o: kernel/memory.c kernel/memory.h \lib/kernel/stdint.h lib/kernel/bitmap.h kernel/debug.h \lib/string.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/bitmap.o: lib/kernel/bitmap.c lib/kernel/bitmap.h \lib/string.h kernel/interrupt.h lib/kernel/print.h \kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/thread.o: thread/thread.c thread/thread.h \lib/kernel/stdint.h lib/kernel/list.h lib/string.h \kernel/memory.h kernel/interrupt.h kernel/debug.h \lib/kernel/print.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/list.o: lib/kernel/list.c lib/kernel/list.h \lib/kernel/stdint.h kernel/interrupt.h kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/sync.o: thread/sync.c thread/sync.h \lib/kernel/stdint.h thread/thread.h kernel/debug.h \kernel/interrupt.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/console.o: device/console.c device/console.h \lib/kernel/print.h thread/sync.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/keyboard.o: device/keyboard.c device/keyboard.h \lib/kernel/print.h kernel/interrupt.h kernel/io.h \lib/kernel/stdint.h device/ioqueue.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/ioqueue.o: device/ioqueue.c device/ioqueue.h \lib/kernel/stdint.h thread/thread.h thread/sync.h \kernel/interrupt.h kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/tss.o: userprog/tss.c userprog/tss.h \lib/kernel/stdint.h thread/thread.h kernel/global.h \lib/kernel/print.h lib/string.h$(CC) $(CFLAGS) $< -o $@############## 汇编代码编译 ###############
$(BUILD_DIR)/kernel.o: kernel/kernel.S$(AS) $(ASFLAGS) $< -o $@$(BUILD_DIR)/print.o: lib/kernel/print.S$(AS) $(ASFLAGS) $< -o $@$(BUILD_DIR)/switch.o: thread/switch.S$(AS) $(ASFLAGS) $< -o $@############## 连接所有目标文件 #############
$(BUILD_DIR)/kernel.bin: $(OBJS)$(LD) $(LDFLAGS) $^ -o $@.PHONY : mk_dir hd clean allmk_dir:if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fihd:dd if=$(BUILD_DIR)/kernel.bin \of=/home/hongbai/bochs/bin/c.img \bs=512 count=200 seek=10 conv=notruncclean:cd $(BUILD_DIR) && rm -f ./*build: $(BUILD_DIR)/kernel.binall: mk_dir build hd
调试了一会,解决了一些代码上的错误,博客里的代码已经修改
正确运行截图
用info gdt查看gdt
可以看到我们的GDT表多了三个项,包括TSS项。
实现用户进程
这一节很长很长,书中下面分了十个小节,我的博客可能会把一些小节合并,做好准备吧。
实现用户进程的原理
我们打算在线程的基础上实现进程。先回顾我们的线程创建流程:

我们只需要把function替换成创建进程的函数,就完成了最主要的工作。
维护虚拟地址空间,修改thread.h
我们知道,进程是享有内存空间的,当然是虚拟的4GB内存,我们给每个进程维护一个虚拟地址池,用来记录这个进程的虚拟中,哪些地址已经被分配,哪些还能继续用。
只加入了几个头文件,task_struct结构体多了一个成员。
struct task_struct {uint32_t *self_kstack; // 线程自己的内核栈栈顶指针enum thread_status status; // 线程的状态uint8_t priority; // 线程的优先级uint8_t ticks; // 线程的时间片,在处理器上运行的时间滴答数uint32_t elapsed_ticks; // 线程的运行时间,也就是这个线程已经执行了多久char name[16]; // 线程的名字struct list_elem general_tag; // 用于线程在一般队列中的节点struct list_elem all_list_tag; // 用于线程在thread_all_list队列中的节点uint32_t *pgdir; // 如果是进程,这是进程的页表的虚拟地址,线程则置为NULLstruct virtual_addr userprog_v_addr; //用户进程的虚拟地址,后续转化为物理地址后存入cr3寄存器uint32_t stack_magic; // 线程栈的魔数,边界标记,用来检测栈溢出
};
回顾cr3寄存器:我们在启动分页时,需要把页目录基址物理地址存入cpu的cr3寄存器,在寻址时需要用到。
为进程创建页表和3特权级栈,修改memory.c
我们在创建进程时,需要给每一个进程单独创建一套页目录表-页表结构(二级页表)。另外我们之前的创建线程过程运行在内核态0特权级,但是我们的用户进程要运行在3特权级,所以需要创建3特权级栈。
这两部分工作完成在memory,修改memory.c。
// 实现memery.h中的函数
#include "./memory.h"
#include "./debug.h"
#include "../lib/kernel/print.h"
#include "../lib/string.h"
#include "../thread/thread.h"
#include "../thread/sync.h"#define PAGE_SIZE 4096 // 定义页面大小为4KB
// 关于位图地址,安排在0xc009a000,我的整个系统支持4个4kb位图,共管理512MB的内存
#define MEM_BITMAP_BASE 0xc009a000 // 内核内存池位图基地址
#define K_HEAP_START 0xc0100000 // 内核堆起始地址#define PDE_INDEX(addr) ((addr & 0xffc00000) >> 22) // 获取页目录项索引
#define PTE_INDEX(addr) ((addr & 0x003ff000) >> 12) // 获取页表项索引struct pool
{struct bitmap pool_bitmap; // 内存池位图struct lock lock; // 创建用户进程会用到,让用户进程申请内存的行为互斥uint32_t phy_addr_start; // 物理内存池起始地址uint32_t pool_size; // 内存池大小
};
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 bit_idx_start = -1; // 位图索引uint32_t vaddr_start = 0; // 虚拟地址uint32_t cnt = 0; // 计数器if (pf == PF_KERNEL){ // 内核内存池// 扫描位图,找到空余的pg_cnt个位,对应相等的页bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt);if (bit_idx_start == -1){ // 没有找到合适的位置return NULL;}// 找到后把位图中pg_cnt个位置设置为1,表示这些页已分配while (cnt < pg_cnt){bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt, 1); // 设置位图cnt++;}vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PAGE_SIZE; // 计算虚拟地址}else{ // 用户内存池/*整体和内核内存池申请过程一致,进行扫描-设置-更新三个过程*因为我们是通过线程建立用户进程,需要先获取目前线程状态*/struct task_struct *cur = running_thread();bit_idx_start = bitmap_scan(&cur->userprog_v_addr.vaddr_bitmap, pg_cnt);while (cnt < pg_cnt){bitmap_set(&cur->userprog_v_addr.vaddr_bitmap, bit_idx_start + cnt, 1);cnt++;}vaddr_start = cur->userprog_v_addr.vaddr_start + bit_idx_start * PAGE_SIZE;// 这是在用户内存,最大页起点就是分界线0xc0000000-一个页大小ASSERT((uint32_t)vaddr_start < 0xc0000000 - PAGE_SIZE);}return (void *)vaddr_start; // 返回虚拟地址
}/* 得到虚拟地址vaddr对应的pte的指针 */
uint32_t *pte_ptr(uint32_t vaddr)
{uint32_t *pte = (uint32_t *)(0xffc00000 + ((vaddr & 0xffc00000) >> 10) + PTE_INDEX(vaddr) * 4); // 计算页表项地址return pte; // 返回页表项地址
}/* 得到虚拟地址vaddr对应的pde的指针 */
uint32_t *pde_ptr(uint32_t vaddr)
{uint32_t *pde = (uint32_t *)(0xfffff000 + PDE_INDEX(vaddr) * 4); // 计算页目录项地址return pde; // 返回页目录项地址
}/* 在m_pool 指向的物理内存池中分配一个物理页,成功返回页物理地址,失败返回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); // 设置位图uint32_t page_phyaddr = m_pool->phy_addr_start + bit_idx * PAGE_SIZE; // 计算物理地址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); // 获取页表项指针if (*pde & 0x00000001){ // 页目录项存在ASSERT(!(*pte & 0x00000001));if (!(*pte & 0x00000001)){ // 页表项不存在*pte = page_phyaddr | PG_US_U | PG_RW_W | PG_P_1; // 设置页表项}else{ // 页表项存在PANIC("page_table_add: pte repeat");*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; // 设置页目录项memset((void *)((uint32_t)pte & 0xfffff000), 0, PAGE_SIZE); // 清空页表ASSERT(!(*pte & 0x00000001));*pte = page_phyaddr | PG_US_U | PG_RW_W | PG_P_1; // 设置页表项}
}/* 分配pg_cnt个页空间,成功则返回起始虚拟地址,失败时返回NULL */
/* 上面三个函数的合成 */
void *malloc_page(enum pool_flags pf, uint32_t pg_cnt)
{ASSERT(pg_cnt > 0 && pg_cnt < 3840); // 检查页数void *vaddr_start = vaddr_get(pf, pg_cnt); // 获取虚拟地址if (vaddr_start == NULL){ // 没有找到合适的地址return NULL;}uint32_t vaddr = (uint32_t)vaddr_start; // 虚拟地址起始位置uint32_t 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 += PAGE_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 * PAGE_SIZE); // 清空内存}return vaddr; // 返回虚拟地址
}/* 从用户内存池申请pg_cnt页内存 */
void *get_user_page(uint32_t pg_cnt)
{lock_acquire(&user_pool.lock); // 保证互斥void *vaddr = malloc_page(PF_USER, pg_cnt);if (vaddr != NULL){memset(vaddr, 0, pg_cnt * PAGE_SIZE);}lock_release(&user_pool.lock);return vaddr;
}/* 将虚拟地址vaddr和内存池物理地址关联 */
/* 仅支持一页*/
void *get_a_page(enum pool_flags pf, uint32_t vaddr)
{struct pool *mem_pool = (pf == PF_KERNEL) ? &kernel_pool : &user_pool;lock_acquire(&mem_pool->lock);struct task_struct *cur = running_thread();int bit_idx = -1;if (cur->pgdir != NULL && pf == PF_USER){bit_idx = (vaddr - cur->userprog_v_addr.vaddr_start) / PAGE_SIZE;ASSERT(bit_idx > 0);bitmap_set(&cur->userprog_v_addr.vaddr_bitmap, bit_idx, 1);}else if (cur->pgdir == NULL && pf == PF_KERNEL){bit_idx = (vaddr - kernel_vaddr.vaddr_start) / PAGE_SIZE;ASSERT(bit_idx > 0);bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx, 1);}else{PANIC("get_a_page:not allow kernel alloc userspace or user alloc kernelspace by get_a_page");}void *page_phyaddr = palloc(mem_pool);if (page_phyaddr == NULL){return NULL;}page_table_add((void *)vaddr, page_phyaddr);lock_release(&mem_pool->lock);return (void *)vaddr;
}/*得到虚拟地址映射到的物理地址*/
uint32_t addt_v_to_p(uint32_t vaddr)
{uint32_t *pte = pte_ptr(vaddr);return ((*pte & 0xfffff000) + (vaddr & 0x00000fff));
}/* 初始化内存池 */
void mem_pool_init(uint32_t all_mem)
{put_str(" mem_pool_init start\n");uint32_t page_table_size = 256 * PAGE_SIZE; // 计算页表使用的内存的大小uint32_t used_mem = page_table_size + 0x100000; // 计算已用内存uint32_t free_mem = all_mem - used_mem; // 计算总可用内存uint16_t all_free_pages = free_mem / PAGE_SIZE; // 计算总可用页数uint16_t kernel_free_pages = all_free_pages / 2; // 内核可用页数uint16_t user_free_pages = all_free_pages - kernel_free_pages; // 用户可用页数uint32_t kbm_len = kernel_free_pages / 8; // 内核位图长度uint32_t ubm_len = user_free_pages / 8; // 用户位图长度uint32_t kp_start = used_mem; // 内核内存池起始地址uint32_t up_start = kp_start + kernel_free_pages * PAGE_SIZE; // 用户内存池起始地址kernel_pool.phy_addr_start = kp_start; // 设置内核内存池起始地址user_pool.phy_addr_start = up_start; // 设置用户内存池起始地址kernel_pool.pool_size = kernel_free_pages * PAGE_SIZE; // 设置内核内存池大小user_pool.pool_size = user_free_pages * PAGE_SIZE; // 设置用户内存池大小kernel_pool.pool_bitmap.btmp_bytes_len = kbm_len; // 设置内核位图长度user_pool.pool_bitmap.btmp_bytes_len = ubm_len; // 设置用户位图长度kernel_pool.pool_bitmap.btmp_bits = (void *)MEM_BITMAP_BASE; // 设置内核位图地址user_pool.pool_bitmap.btmp_bits = (void *)(MEM_BITMAP_BASE + kbm_len); // 设置用户位图地址/* 输出内存池信息 */put_str(" all_free_pages: ");put_int(all_free_pages);put_char('\n'); // 总内存512mb,使用了2mb,可用页 510mb/4kb=127000put_str(" kernel_free_pages: ");put_int(kernel_free_pages);put_char('\n');put_str(" user_free_pages: ");put_int(user_free_pages);put_char('\n');put_str(" kernel_pool_bitmap_start: ");put_int((int)kernel_pool.pool_bitmap.btmp_bits);put_char('\n');put_str(" kernel_pool_phy_addr_start: ");put_int(kernel_pool.phy_addr_start);put_char('\n');put_str(" user_pool_bitmap_start: ");put_int((int)user_pool.pool_bitmap.btmp_bits);put_char('\n');put_str(" user_pool_phy_addr_start: ");put_int(user_pool.phy_addr_start);put_char('\n');/* 将池内位图置0 */bitmap_init(&kernel_pool.pool_bitmap); // 初始化内核位图bitmap_init(&user_pool.pool_bitmap); // 初始化用户位图/* 初始化内核虚拟地址的位图 */kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_len; // 设置内核虚拟地址位图长度kernel_vaddr.vaddr_bitmap.btmp_bits =(void *)(MEM_BITMAP_BASE + kbm_len + ubm_len); // 设置内核虚拟地址位图地址kernel_vaddr.vaddr_start = K_HEAP_START; // 设置内核虚拟地址起始位置bitmap_init(&kernel_vaddr.vaddr_bitmap); // 初始化内核虚拟地址位图lock_init(&kernel_pool.lock);lock_init(&user_pool.lock);put_str(" mem_pool_init done\n");
}/* 内存管理初始化入口 */
void mem_init(void)
{put_str("mem_init start\n");uint32_t mem_bytes_total = (*(uint32_t *)(0xb00)); // 之前loader在开启分页时就获取了全部内存的大小,放到了0xb00中mem_pool_init(mem_bytes_total); // 初始化内存池put_str("mem_init done\n");
}
对应的,修改memory.h
// 声明了虚拟将地址池结构体
#ifndef __KERNEL_MEMORY_H
#define __KERNEL_MEMORY_H
#include "../lib/kernel/stdint.h"
#include "../lib/kernel/bitmap.h"/* 内存池标记 */
enum pool_flags
{PF_KERNEL = 1, // 内核内存池PF_USER = 2, // 用户内存池
};#define PG_P_1 1 // 页表项的存在
#define PG_P_0 0 // 页表项的不存在
#define PG_RW_R 0 // 可读可执行
#define PG_RW_W 2 // 可读可写可执行
#define PG_US_S 0 // 内核特权级
#define PG_US_U 4 // 用户特权级
// 虚拟地址结构体,内部有一个位图结构体,还有一个虚拟地址起始位置
struct virtual_addr
{struct bitmap vaddr_bitmap; // 虚拟地址位图uint32_t vaddr_start; // 虚拟地址起始位置
};extern struct pool kernel_pool; // 内核内存池
extern struct pool user_pool; // 用户内存池
uint32_t *pte_ptr(uint32_t vaddr); /* 得到虚拟地址vaddr对应的pte的指针 */
uint32_t *pde_ptr(uint32_t vaddr); /* 得到虚拟地址vaddr对应的pde的指针 */
void *malloc_page(enum pool_flags pf, uint32_t pg_cnt); /* 分配pg_cnt个页空间,成功则返回起始虚拟地址,失败时返回NULL */
void *get_kernel_pages(uint32_t pg_cnt); /* 从内核物理内存池中申请pg_cnt页内存 */
void *get_user_page(uint32_t pg_cnt);
void *get_a_page(enum pool_flags pf, uint32_t vaddr);
uint32_t addt_v_to_p(uint32_t vaddr);
void mem_pool_init(uint32_t all_mem); // 内存池初始化
void mem_init(void); // 内存管理初始化
#endif
关于这部分代码,由于有一些是之前写的,感觉有点绕,等这章结束后还需要反复理解。
进入特权级3
如何从0特权级进入3特权级?一般情况下肯定是不行,必须借助中断返回iret才能实现。我们模拟一个中断返回过程,实现进入低特权级。以下是关键点。
栈指的是intr_struct,定义在thread.h,初始包含中断号和各种寄存器
- 关键点1:从中断返回,必须要经过intr_exit,即使是“假装”
- 关键点2:必须提前准备好用户进程所用的栈结构,在里面填装好用户进程的上下文信息, 借一系列pop出栈的机会,将用户进程的上下文信息载入CPU的寄存器,为用户进程的运行准备好环境。
- 关键点3:我们要在栈中存储的CS选择子,其RPL 必须为3。即人为设定后续的用户进程请求访问内存时,永远是3特权级。
- 关键点4:栈中段寄存器的选择子必须指向DPL为3的内存段。即我们的目的地是3特权级的内存段。
- 关键点5:必须使栈中eflags的IF位为1。即可以继续响应中断。
- 关键点6:必须使栈中eflags的IOPL位为0。 即不允许用户进程访问硬件。
复习特权级:CPL是当前正在执行的代码段的特权级
DPL是段描述符、门描述符中的特权级
RPL是段选择子的低两位,表示请求访问时的权限,人为设定
允许访问的公式是:max(cpl,rpl)<=dpl
用户进程创建的流程
目前我们完成了内存管理的更新,算是给用户进程创建提供了工具,马上就要开始具体的实现细节了,郑钢老师特意放了两个流程图,便于我们从整体上把握用户进程创建的过程。
实现用户进程——上
修改global.h
新增了几个宏定义,关于bool之类的我都定义在stdint里了,就不再写了。
/*----------用户进程相关属性----------*/
#define EFLAGS_MBS (1 << 1) // 此项必须要设置
#define EFLAGS_IF_1 (1 << 9) // if位1,屏蔽中断信号,关中断
#define EFLAGS_IF_0 0 // 开中断
#define EFLAGS_IOPL_3 (3 << 12) // 测试用,允许用户进程跳过系统调用直接操作硬件
#define EFLAGS_IOPL_0 (0 << 12) // 默认状态
#define DIV_ROUND_UP(X, STEP) ((X + STEP - 1) / STEP) // 实现除法后向上取整
#define PG_SIZE 4096
编写process.c
位置在userprog目录下,目前是第一部分,总共有三个函数
#include "./process.h"
#include "../kernel/global.h"
#include "../lib/kernel/stdint.h"
#include "../lib/kernel/print.h"
#include "../thread/thread.h"
#include "./userprog.h"
#include "../kernel/debug.h"
#include "./tss.h"extern void intr_exit(void); // 相关实现在kernel.s里,是中断返回程序/*构建用户进程初始化上下文信息*/
void start_process(void *filename_)
{void *function = filename_; // 用户进程中,具体程序的入口struct task_struct *cur = running_thread(); // 进程栈// 关于以下两行代码,在《真象还原》的509、510页有非常清晰的解释cur->self_kstack += sizeof(struct thread_stack); // 留出中断栈空间struct intr_stack *proc_stack = (struct intr_stack *)cur->self_kstack;// 初始化8个通用寄存器proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy = 0;proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0;// 显存段初始化proc_stack->gs = 0;// 初始化ds,ds保存数据段基址,ss保存栈段基址,都指向用户数据段选择子proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA;// 初始化cs:ip,cs保存代码段基址,指向用户代码段选择子proc_stack->eip = function;proc_stack->cs = SELECTOR_U_CODE;// 初始化elfgsproc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1);// 初始化ss:spproc_stack->esp = (void *)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE);proc_stack->ss = SELECTOR_U_DATA;// 通过内联汇编,欺骗cpu,让它进行一次中断返回,把proc_stack中的数据压入cpuasm volatile("movl %0,%%esp;jmp intr_exit" : : "g"(proc_stack) : "memory");
}/*激活页表,pthread可能是用户进程或者内核线程,*如果是内核线程,重新加载内核页表,否则加载进程的页表*/
void page_dir_activate(struct task_struct* pthread){// 默认值就是内核线程的页目录表基址uint32_t page_dir_phy_addr = 0x100000;if(pthread->pgdir != NULL){ // 说明pthread是一个用户进程,激活进程的二级页表结构page_dir_phy_addr = addt_v_to_p((uint32_t)pthread->pgdir);}// 通过内联汇编,更新cr3中页目录表的物理地址基址asm volatile("movl %0,%%cr3" : : "r"(page_dir_phy_addr) : "memory");
}/*激活线程或进程的页表,更新tss中的esp0为 0特权级的栈*/
void process_activate(struct task_struct *pthread){ASSERT(pthread != NULL);//激活页表page_dir_activate(pthread);//如果是内核线程,没有特权级转变,不需要更新pcb里的esp0//如果是用户进程,需要更新esp0if(pthread->pgdir!=NULL){update_tss_esp(pthread);}
}
proc_stack->esp = (void *)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE);
这一行代码涉及到一个宏定义,目录是userprog/userprog.h
#ifndef __USERPROG_USERPROG_H
#define __USERPROG_USERPROG_H
/*关于下面这个宏定义的取值,参考P511*我们模仿c程序的内存分布,用户进程虚拟地址从高到低分别是:*命令行参数和环境变量、栈、堆、bss、data、text*/
#define USER_STACK3_VADDR (0xc0000000 - 0x1000)#endif
我觉得难点是地址判断,栈顶指针的移动是和最早初始化task_struct有关的,要结合那块的代码去看,可以参考书上509、510页。
bss简介
这一小节插在用户进程编写中间。下图是linux下c程序的内存方案。
复制书上的原文解释一下:在C程序的内存空间中,位于低处的三个段是代码段、数据段和bss段,它们由编译器 和链接器规划地址空间,在程序被操作系统加载之前它们地址就固定了。而堆是位于bss段的上面,栈是 位于堆的上面,它们共享4GB空间中除了代码段、数据段及顶端命令行参数和环境变量等以外的其余可 用空间,它们的地址由操作系统来管理,在程序加载时为用户进程分配栈空间,运行过程中为进程从堆中 分配内存。堆向上扩展,栈向下扩展,因此在程序的加载之初,操作系统必须为堆和栈分别指定起始地址。
在Linux 中,堆的起始地址是固定的,这是由struct mm_struct 结构中的 start_brk 来指定的,堆的结 束地址并不固定,这取决于堆中内存的分配情况,堆的上边界是由同结构中的brk来标记的。
关于bss,bss 中的数据是未初始化的全局变量和局部静态变量,程序运行后才会为它们赋值,因此 在程序运行之初,里面的数据没意义,由操作系统的程序加载器将其置为0就可以了,虽然这些未初始化的全 局变量及局部静态变量起初是用不上的,但它们毕竟也是变量,即使是短暂的生存周期也要占用内存,必须 提前为它们在内存中“占好座”,bss区域的目的也正在于此,就是提前为这些未初始化数据预留内存空间。
bss数据最终会被归并到数据段,所以我们不需要单独知道bss的结束地址,只需要确保堆的起始地址在用户进程地址最高的段之上即可。
实现用户进程——下
这部分是三个函数,接着上面的process.c写,贴出完整的程序
完整的process.c
#include "./process.h"
#include "../kernel/global.h"
#include "../lib/kernel/stdint.h"
#include "../thread/thread.h"
#include "./userprog.h"
#include "../kernel/debug.h"
#include "./tss.h"
#include "../device/console.h"
#include "../lib/string.h"
#include "../kernel/interrupt.h"/*构建用户进程初始化上下文信息*/
void start_process(void *filename_)
{void *function = filename_; // 用户进程中,具体程序的入口struct task_struct *cur = running_thread(); // 进程栈// 关于以下两行代码,在《真象还原》的509、510页有非常清晰的解释cur->self_kstack += sizeof(struct thread_stack); // 留出中断栈空间struct intr_stack *proc_stack = (struct intr_stack *)cur->self_kstack;// 初始化8个通用寄存器proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy = 0;proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0;// 显存段初始化proc_stack->gs = 0;// 初始化ds,ds保存数据段基址,ss保存栈段基址,都指向用户数据段选择子proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA;// 初始化cs:ip,cs保存代码段基址,指向用户代码段选择子proc_stack->eip = function;proc_stack->cs = SELECTOR_U_CODE;// 初始化elfgsproc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1);// 初始化ss:spproc_stack->esp = (void *)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE);proc_stack->ss = SELECTOR_U_DATA;// 通过内联汇编,欺骗cpu,让它进行一次中断返回,把proc_stack中的数据压入cpuasm volatile("movl %0,%%esp;jmp intr_exit" : : "g"(proc_stack) : "memory");
}/*激活页表,pthread可能是用户进程或者内核线程,*如果是内核线程,重新加载内核页表,否则加载进程的页表*/
void page_dir_activate(struct task_struct *pthread)
{// 默认值就是内核线程的页目录表基址uint32_t page_dir_phy_addr = 0x100000;if (pthread->pgdir != NULL){ // 说明pthread是一个用户进程,激活进程的二级页表结构page_dir_phy_addr = addt_v_to_p((uint32_t)pthread->pgdir);}// 通过内联汇编,更新cr3中页目录表的物理地址基址asm volatile("movl %0,%%cr3" : : "r"(page_dir_phy_addr) : "memory");
}/*激活线程或进程的页表,更新tss中的esp0为 0特权级的栈*/
void process_activate(struct task_struct *pthread)
{ASSERT(pthread != NULL);// 激活页表page_dir_activate(pthread);// 如果是内核线程,没有特权级转变,不需要更新pcb里的esp0// 如果是用户进程,需要更新esp0if (pthread->pgdir != NULL){update_tss_esp(pthread);}
}/*创建页目录表,成功返回页目录的虚拟地址,失败返回-1*/
uint32_t *create_page_dir(void)
{/*申请内存空间只能在内核态进行*/uint32_t *page_dir_vaddr = get_kernel_pages(1);if (page_dir_vaddr == NULL){console_put_str("create_page_dir error:get_kernel_page failed!\n");return NULL;}/*1.复制页表*/// 0x300 = 768,一个项4个字节 0xfffff000是最后一个分页,它同时映射页目录表本身,关键词:递归页表memcpy((uint32_t *)((uint32_t)page_dir_activate + 0x300 * 4), (uint32_t *)(0xfffff000 + 0x300 * 4), 1024);/*2.更新页目录基址*/uint32_t new_page_dir_phy_addr = addt_v_to_p((uint32_t)page_dir_vaddr);page_dir_vaddr[1023] = new_page_dir_phy_addr | PG_US_U | PG_RW_W | PG_P_1;return page_dir_vaddr;
}/*创建用户进程虚拟地址位图*/
void create_user_vaddr_bitmap(struct task_struct *user_prog)
{// 我们为用户进程定的起始地址是USER_VADDR_START// 该值定义在 process.h 中,其值为 0x8048000,可以通过readelf查看user_prog->userprog_v_addr.vaddr_start = USER_VADDR_START;uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8, PG_SIZE);user_prog->userprog_v_addr.vaddr_bitmap.btmp_bits = get_kernel_pages(bitmap_pg_cnt);user_prog->userprog_v_addr.vaddr_bitmap.btmp_bytes_len = (0xc0000000 - USER_VADDR_START) / PG_SIZE / 8;bitmap_init(&user_prog->userprog_v_addr.vaddr_bitmap); // 通过初始化填充0,代表内存已经被使用
}/*通过线程创建用户进程*/
#define default_prio 31 // 临时定义
void process_execute(void *filename, char *name)
{struct task_struct *thread = get_kernel_pages(1);init_thread(thread, name, default_prio); // 初始化线程create_user_vaddr_bitmap(thread); // 位图thread_create(thread, start_process, filename); // 线程结构体-具体功能(创建进程)-线程名thread->pgdir = create_page_dir(); // 页目录表enum intr_status old_status = intr_disable();ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));list_append(&thread_ready_list, &thread->general_tag);ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));list_append(&thread_all_list, &thread->all_list_tag);intr_set_status(old_status);
}
对应的process.h
#ifndef __USERPROG_PROCESS_H
#define __USERPROG_PROCESS_H
#define USER_VADDR_START 0x8048000
extern void intr_exit(void); // 相关实现在kernel.s里,是中断返回程序void start_process(void *filename_);
void page_dir_activate(struct task_struct *pthread);
void process_activate(struct task_struct *pthread);
uint32_t *create_page_dir(void);
void create_user_vaddr_bitmap(struct task_struct *user_prog);
void process_execute(void *filename, char *name);#endif
用户进程的调度
修改schedule函数
其实也就加了一行,我贴出完整的schedule函数好了
/* 实现任务调度 */
void schedule(void)
{ASSERT(intr_get_status() == INTR_OFF); // 确保中断关闭struct task_struct *cur = running_thread(); // 获取当前线程pcbif (cur->status == TASK_RUNNING){ // 如果当前线程是运行状态ASSERT(!elem_find(&thread_ready_list, &cur->general_tag)); // 确保当前线程不在就绪队列list_append(&thread_ready_list, &cur->general_tag); // 将当前线程加入就绪队列cur->status = TASK_READY; // 设置当前线程状态为就绪cur->ticks = cur->priority; // 重置时间片}else{}ASSERT(!list_empty(&thread_ready_list)); // 确保就绪队列不为空struct list_elem *thread_tag = list_pop(&thread_ready_list); // 从就绪队列中取出一个线程节点struct task_struct *next = (struct task_struct *)((uint32_t)thread_tag & 0xfffff000);next->status = TASK_RUNNING; // 设置下一个线程状态为运行process_activate(next); // 激活任务页表switch_to(cur, next); // 任务切换
}
测试用户进程
最后一个小节了,我们要修改main.c,编写makedown
main.c
// 内核的入口函数
#include "../lib/kernel/print.h"
#include "./init.h"
#include "../thread/thread.h"
#include "../device/console.h"
#include "./interrupt.h"
#include "../userprog/process.h"void k_thread_a(void *);
void k_thread_b(void *);
void u_prog_a(void);
void u_prog_b(void);
int test_var_a = 0, test_var_b = 0;
int main(void)
{put_str("HongBai's OS kernel\n");init_all(); // 初始化所有模块thread_start("k_thread_a", 31, k_thread_a, "argA ");thread_start("k_thread_b", 31, k_thread_b, "argB ");process_execute(u_prog_a, "user_prog_a");process_execute(u_prog_b, "user_prog_b");intr_enable();while (1){};
}void k_thread_a(void *arg)
{while (1){console_put_str(" v_a:0x");console_put_int(test_var_a);}
}void k_thread_b(void *arg)
{while (1){console_put_str(" v_b:0x");console_put_int(test_var_b);}
}void u_prog_a(void)
{while (1){test_var_a++;}
}void u_prog_b(void)
{while (1){test_var_b++;}
}
makefile
BUILD_DIR = ./build
ENTRY_POINT = 0xc0001500
AS = nasm
CC = gcc
LD = ld
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/ -I thread/ -I userprog -I userprog/
ASFLAGS = -f elf
CFLAGS = -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
LDFLAGS = -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \$(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o \$(BUILD_DIR)/debug.o $(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o \$(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o $(BUILD_DIR)/list.o \$(BUILD_DIR)/switch.o $(BUILD_DIR)/sync.o $(BUILD_DIR)/console.o \$(BUILD_DIR)/keyboard.o $(BUILD_DIR)/ioqueue.o $(BUILD_DIR)/tss.o \$(BUILD_DIR)/process.o################ c代码编译 ##################
$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h \lib/kernel/stdint.h kernel/init.h kernel/debug.h \kernel/memory.h thread/thread.h kernel/interrupt.h \device/console.h userprog/process.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/init.o: kernel/init.c kernel/init.h lib/kernel/print.h \lib/kernel/stdint.h kernel/interrupt.h device/timer.h \kernel/memory.h thread/thread.h device/console.h \device/keyboard.h userprog/tss.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/interrupt.o: kernel/interrupt.c kernel/interrupt.h \lib/kernel/stdint.h kernel/global.h kernel/io.h \lib/kernel/print.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/timer.o: device/timer.c device/timer.h lib/kernel/stdint.h \kernel/io.h lib/kernel/print.h kernel/interrupt.h \thread/thread.h kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/debug.o: kernel/debug.c kernel/debug.h \lib/kernel/print.h kernel/interrupt.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/string.o: lib/string.c lib/string.h \kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/memory.o: kernel/memory.c kernel/memory.h \lib/kernel/stdint.h lib/kernel/bitmap.h kernel/debug.h \lib/string.h thread/sync.h thread/thread.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/bitmap.o: lib/kernel/bitmap.c lib/kernel/bitmap.h \lib/string.h kernel/interrupt.h lib/kernel/print.h \kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/thread.o: thread/thread.c thread/thread.h \lib/kernel/stdint.h lib/kernel/list.h lib/string.h \kernel/memory.h kernel/interrupt.h kernel/debug.h \lib/kernel/print.h userprog/process.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/list.o: lib/kernel/list.c lib/kernel/list.h \lib/kernel/stdint.h kernel/interrupt.h kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/sync.o: thread/sync.c thread/sync.h \lib/kernel/stdint.h thread/thread.h kernel/debug.h \kernel/interrupt.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/console.o: device/console.c device/console.h \lib/kernel/print.h thread/sync.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/keyboard.o: device/keyboard.c device/keyboard.h \lib/kernel/print.h kernel/interrupt.h kernel/io.h \lib/kernel/stdint.h device/ioqueue.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/ioqueue.o: device/ioqueue.c device/ioqueue.h \lib/kernel/stdint.h thread/thread.h thread/sync.h \kernel/interrupt.h kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/tss.o: userprog/tss.c userprog/tss.h \lib/kernel/stdint.h thread/thread.h kernel/global.h \lib/kernel/print.h lib/string.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/process.o: userprog/process.c userprog/process.h \kernel/global.h lib/kernel/stdint.h thread/thread.h \userprog/userprog.h kernel/debug.h userprog/tss.h \device/console.h lib/string.h kernel/interrupt.h$(CC) $(CFLAGS) $< -o $@############## 汇编代码编译 ###############
$(BUILD_DIR)/kernel.o: kernel/kernel.S$(AS) $(ASFLAGS) $< -o $@$(BUILD_DIR)/print.o: lib/kernel/print.S$(AS) $(ASFLAGS) $< -o $@$(BUILD_DIR)/switch.o: thread/switch.S$(AS) $(ASFLAGS) $< -o $@############## 连接所有目标文件 #############
$(BUILD_DIR)/kernel.bin: $(OBJS)$(LD) $(LDFLAGS) $^ -o $@.PHONY : mk_dir hd clean allmk_dir:if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fihd:dd if=$(BUILD_DIR)/kernel.bin \of=/home/hongbai/bochs/bin/c.img \bs=512 count=200 seek=10 conv=notruncclean:cd $(BUILD_DIR) && rm -f ./*build: $(BUILD_DIR)/kernel.binall: mk_dir build hd
结果截图
还有比较严重的bug,明天调试吧。
结语
两天完成了第11章,只能说内容比我想象的更多吧。最后还没调试好,估计可能是内存分页出了问题。今天太晚了,明天继续调试。
今天力扣刷题两道,涉及双指针,相关博客正在路上。
目标是5.1-5.3弄完12、13章。
相关文章:
《操作系统真象还原》第十一章——用户进程
文章目录 前言为什么要有TSSTSS简介TSS描述符和TSS结构现代操作系统采用的任务切换方式 定义并初始化TSS修改global.h编写tss.c测试 实现用户进程实现用户进程的原理维护虚拟地址空间,修改thread.h为进程创建页表和3特权级栈,修改memory.c进入特权级3用户…...
第 11 届蓝桥杯 C++ 青少组中 / 高级组省赛 2020 年真题答和案解析
一、选择题 第 1 题 单选题 题目:表达式 ‘6’ - ‘1’ 的值是 ( ) A. 整数 5 B. 字符 5 C. 表达式不合法 D. 字符 6 答案:A 解析:在 C++ 中,字符常量以 ASCII 码形式存储。6 的 ASCII 码为 54,1 的 ASCII 码为 49,二者相减结果为 5,是整数类型,因此选 A。 第 2 题 …...
ES搜索知识
GET /categories/1/10?name手机 // 按名称过滤 GET /categories/1/10?type电子产品 // 按类型过滤 GET /categories/1/10?name手机&type电子产品 // 组合过滤 查询参数 ApiOperation(value "获取商品分类分页列表")GetMapping("{page}/{limit}")…...
Java高阶程序员学习计划(详细到天,需有一定Java基础)
🤟致敬读者 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉📘博主相关 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息文章目录 Java高阶程序员学习计划(详细到天,需有一定Java基础)第一阶段(30天)Java基础:Java生态工具链:设计模式与编码规范:第二阶段(15天…...
SALOME源码分析: SMESH模块
本文分析SALOME GEOM模块。 注1:限于研究水平,分析难免不当,欢迎批评指正。 注2:文章内容会不定期更新。 一、核心组件 1.1 SMESHGUI 二、关键流程 网络资料 SALOME: Introduction to MESH Modulehttps://docs.salome-platform…...
提高程序灵活性和效率的利器:Natasha动态编译库【.Net】
从零学习构建一个完整的系统 今天推荐一个针对C#动态编译库,动态编译的使用场景有很多: 1、动态代码生成:可以根据用户的输入或者系统配置动态执行C#代码,比如很多Web在线编译器,就是这个原理; 2、代码反…...
Cangjie Magic在医疗领域的应用:智能体技术如何重塑医疗数字化
文章目录 1. Cangjie Magic是什么?有什么优势?2. Cangjie Magic与Python的区别与优势对比技术特性对比医疗场景案例对比案例1:电子病历自然语言处理案例2:ICU实时监护系统 3. Cangjie Magic的学习成本与性价比学习门槛性价比优势 …...
MySQL基础关键_002_DQL(一)
目 录 一、初始化 二、简单查询 1.部分语法规则 2.查询一个字段 (1)查询员工编号 (2)查询员工姓名 3.查询多个字段 (1)查询员工编号、姓名 (2)查询部门编号、名称、位置 …...
从高端制造到民生场景:天元轻量化软件的“破局”之路
近期,清华大学航空发动机研究院(以下简称“清华航发院”)正式引入天元轻量化软件,用于其相关设计与3D可视化研究工作。 作为国内领先的3D轻量化解决方案提供商,天元轻量化软件一直致力于为各行业提供高效、灵活、安全…...
本地部署大模型的方式有哪些
本地部署大模型的方式主要分为 应用部署 和 源码部署 两大类,具体分类及特点如下: 一、应用部署(适合新手) 特点:无需编程基础,通过厂商提供的工具直接安装使用,支持图形化界面和命令行操作&am…...
libevent详解
目录 一、安装libevent库 二、libevent 三、基于 libevent 实现信号处理与定时任务 四、基于 libevent 的事件驱动 TCP 服务端代码 一、安装libevent库 sudo su apt install libevent-dev 二、libevent libevent 是一个轻量级网络i/o库,i/o框架库,…...
Solon Cloud Gateway 补充
说明 在「使用 Solon Cloud Gateway 替换Spring Cloud Gateway 」的文章中,有评论说不知道响应式。当时看的是 Solon Cloud Gateway 使用响应式接口,由 Solon-Rx 来实现,是基于 reactive-streams 封装的 RxJava 极简版。目前仅一个接口 Com…...
海外社交软件技术深潜:实时互动系统与边缘计算的极限优化
一、毫秒级延迟之战:下一代实时通信架构 1.1 全球实时消息投递体系设计 图表 代码 性能基准测试(跨大西洋传输): 协议/算法组合 平均延迟 99分位延迟 丢包恢复率 WebSocketTLSBBRv2 142ms 298ms 78% QUIC自定义CC 112ms 201ms 92%…...
直播美颜SDK是什么?跨平台美颜SDK开发与接入全解析
当下,越来越多的直播平台、短视频App、社交娱乐应用,开始在画面美化方面加大投入。本文将围绕直播美颜SDK是什么、其背后的核心技术、如何实现跨平台开发、以及接入流程等关键问题,为你全面解析这一技术热点。 一、直播美颜SDK到底是什么&am…...
自学S32k144(18)————芯片锁死问题及成功解锁流程
1.锁死原因 温度过高flash异常操作静电等电压异常问题。。。。 本人出现情况:之前开发板不知什么原因,发生短路,重新置换芯片后,发现芯片在S32DS中无法正常烧录 判断可能是由于焊接时温度过高导致锁死。需解锁芯片。 2.解决方法…...
【免费数据】2000-2020年中国4km分辨率逐日气象栅格数据(含9个气象变量)
逐日气象数据是在很多研究中都会用到的数据,例如验证气候模拟、分析陆地生态系统变化以及识别气候变化下的极端天气条件等研究,尤其是高精度的逐日气象数据对于研究者来说更为常用。 本次我们为大家带来的是2000-2020年中国4km分辨率逐日气象栅格数据&a…...
Android Compose 无网络状态处理全指南:从基础到高级实践
Android Compose 无网络状态界面处理全方案 引言 在移动应用开发中,网络连接不稳定是常见场景。优雅地处理无网络状态能显著提升用户体验。Jetpack Compose 提供了强大的工具来实现各种网络状态下的界面展示。本文将全面介绍在 Compose 中处理无网络状态的多种方案…...
网络规划和设计
1.结构化综合布线系统包括建筑物综合布线系统PDS,智能大夏布线系统IBS和工业布线系统IDS 2.GB 50311-2016综合布线系统工程设计规范 GB/T 50312-2016综合布线系统工程验收规范 3.结构化布线系统分为6个子系统: 工作区子系统;水平布线子系…...
Learning vtkjs之ImageMarchingSquares
体积 等值线处理 介绍 vtkImageMarchingSquares - 对图像(或来自体积的切片)进行等值线处理 给定一个指定的等值,使用Marching Squares算法(3D Marching Cubes算法的2D版本)生成等值线。 效果 自己增加了两个小球&…...
前端跨域问题详解:原因、解决方案与最佳实践
引言 在现代Web开发中,跨域问题是前端工程师几乎每天都会遇到的挑战。随着前后端分离架构的普及和微服务的发展,跨域请求变得愈发常见。本文将深入探讨跨域问题的本质、各种解决方案以及在实际开发中的最佳实践。 一、什么是跨域问题? 1.1…...
第五届图像、视觉与智能系统国际会议(ICIVIS 2025)参会通知
大会官网: http://www.icivis.net/ 官方邮箱:icivis163.com 会议地点:杭州师范大学仓前校区(余杭塘路2318号) 会议时间:2025年5月23日-5月25日 主办单位:杭州师范大学 1.一般会员注册 提交注册表以后…...
PCB设计工艺规范(二)基本布局要求
基本布局要求 1.PCBA加工工序2.对器件以及PCB布局要求 资料来自网络,仅供学习使用。 1.PCBA加工工序 制成板的元件布局应保证制成板的加工工序合理,以便于提高制成板加工效率和直通率。 PCB 布局选用的加工流程应使加工效率最高。 常用 PCBA 的6种主流…...
SWIG 和 JNA / JNI 等 C 接口封装工具及进行 C 接口的封装
SWIG 相关 SWIG 是什么 SWIG 是一个软件开发工具,是一个 封装 C/C++ 动态库供其他编程语言调用的神器。 使用它可以简化不同语言与与 C/C++ 语言的交互。简单点说,SWIG 是一个编译器,它以 C/C++的声明为输入,创建从其他语言包括常见的脚本语言如 Javascript、Perl、PHP、…...
【Bootstrap V4系列】学习入门教程之 布局
Bootstrap V4 学习入门教程之 布局 一、容器1.1 All-in-one 一体化1.2 Fluid 流体1.3 Responsive 快速响应 二、栅格系统2.1 网格选项2.2 自动布局列等宽等宽多线 2.3、设置一列宽度2.4、可变宽度内容 一、容器 容器是Bootstrap中最基本的布局元素,在使用我们的默认…...
Nginx功能全解析:你的高性能Web服务器解决方案
Nginx是一款开源的高性能HTTP和反向代理服务器,同时也是IMAP/POP3/SMTP代理服务器。自2004年推出以来,Nginx因其卓越的性能、稳定性和丰富的功能而广泛应用于各种规模的网站。本文将深入解析Nginx的主要功能,帮助你充分利用这款强大的Web服务…...
OpenAI 2025 4月最新动态综述
2025年4月,OpenAI在人工智能领域持续引领创新浪潮,发布了多项重磅新产品和技术,推动AI进入更加实用和智能的新阶段。以下是近期OpenAI的重点动态整理: 1. GPT-5预览版发布,迈入通用AI工业化时代 2025年4月15日&#…...
综合案例建模(1)
文章目录 滚花手扭螺丝机箱封盖螺丝螺丝孔锥形垫片 滚花手扭螺丝 前视基准面画草图,旋转生成主体 倒角0.5 顶面,草图转换实体引用,去复制边线 生成螺旋线路径 顶面绘制草图 上一步画的草图沿螺旋线扫描切除 镜像扫描特征 阵列镜像扫描特征 创…...
ComfyUI 学习笔记,案例3:img2img
背景 ComfyUI 学习笔记,第三个案例 img2img,官网文档的 安装篇 部分找到桌面版,于是就下载了桌面版本,运行第三个案例。 注意要点: 桌面版安装时检测到本机设备不符合时,需要手动选择安装配置࿰…...
水利水电安全员A证考试核心知识点
水利水电安全员A证考试核心知识点 水利水电安全员A证考试主要考查安全生产管理、法律法规、专业技术及应急处理能力,以下是核心知识点: 1. 安全生产法律法规 《安全生产法》:明确企业主体责任、从业人员权利与义务、事故追责等。 《水利工…...
Verilog仿真模块--真随机数生成器
前言 在进行功能仿真时,总是希望仿真条件能覆盖尽量多的情况,因此,经常需要产生随机数作为仿真的输入。Verilog 和 SV 中有能够产生随机数的系统函数 $random,可惜的是此函数产生的随机数是伪随机数,重新再跑一次仿真&…...
融合AI助力医疗提效,华奥系医务系统助力医院数字化升级!
医疗资源供需优化一直是医院关注的重点问题。据此,华奥系科技推出华奥系智能医务管理系统,并基于DeepSeek-R1大模型,自主研发将AI智能诊疗助手融入系统。以“智能驱动效率、数据赋能管理”为核心,打造覆盖医院全场景的数字化解决方…...
UDP报文结构
文章目录 简介UDP报文结构解析UDP的特点使用 UDP 的注意事项 简介 用户数据报协议(User Datagram Protocol,UDP)是传输层的一种无连接协议,它与TCP相比,没有复杂的连接建立、维护和拆解过程,在传输效率上具…...
综合开发-手机APP远程控制PLC1500柱灯的亮灭
要通过 Unity3D 开发的手机 App 控制 电气柜上面的柱灯,需要WIFI模块作为桥梁,按照以下步骤实现: 1. 硬件准备(硬件部分) 所需材料 ESP32开发板(如ESP32-WROOM-32&a…...
4:机器人目标识别无序抓取程序二次开发
判断文件是否存在 //判断文件在不在 int HandEyeCalib::AnsysFileExists(QString FileAddr) {QFile File1(FileAddr);if(!File1.exists()){QMessageBox::warning(this,QString::fromLocal8Bit("提示"),FileAddrQString::fromLocal8Bit("文件不存在"));retu…...
数据结构篇:线性表的另一表达—链表之单链表(下篇)
目录 1.前言 2.是否使用二级指针 3.插入/删除 3.1 pos位置前/后插入 3.2 查找函数 3.3 pos位置删除 3.4 pos位置后面删除 3.5 函数的销毁 4.断言问题 4.1 断言pphead 4.2 断言*pphead 5.三个文件的代码 5.1 头文件 5.2 具体函数实现 5.3 测试用例 1.前言 之前是讲…...
C# 异步详解
C# 异步编程详解 一、异步编程基础概念 1. 同步 vs 异步 同步(Synchronous):任务按顺序执行,前一个任务完成后才会执行下一个异步(Asynchronous):任务可以非阻塞地启动,主线程可以继续执行其他操作 2. 异步编…...
X²+1素数问题
X1素数问题是与哥德巴赫猜想和孪生素数猜想同时代的著名数学难题。是否有无穷个正整数x,使得x1总是素数? 其困难程度不亚于哥德巴赫猜想。特别是100多年以来,许许多多一流数论学者对这个问题进行了研究。 X1素数 X1素数是一个著名的猜想&…...
【自定义控件实现最大高度和最大宽度实现】
背景 开发中偶尔遇到控件宽度或者高度在自适应的情况下,有个边界值,也就是最大值。 比如高度自适应的情况下最大高度300dp这种场景。 实现 关键节点代码: Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)…...
基于C++的IOT网关和平台4:github项目ctGateway交互协议
初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github:codetoys,所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C++的,可以在任何平台上使用。 源码指引:github源码指引_初级代码游戏的博客-CSDN博客 …...
DeepSeek谈《凤凰项目 一个IT运维的传奇故事》
《凤凰项目:一个IT运维的传奇故事》(The Phoenix Project: A Novel About IT, DevOps, and Helping Your Business Win)是Gene Kim、Kevin Behr和George Spafford合著的一部小说,通过虚构的故事生动展现了IT运维中的核心挑战和Dev…...
Spyglass:官方Hands-on Training(一)
相关阅读 Spyglasshttps://blog.csdn.net/weixin_45791458/category_12828934.html?spm1001.2014.3001.5482 本文是对Spyglass Hands-on Training中第一个实验的翻译(有删改),Lab文件可以从以下链接获取。Spyglass Hands-on Traininghttps:…...
10.idea中创建springboot项目_jdk17
10.idea中创建springboot项目_jdk17 1. 准备工作 安装 JDK 17: 确保已安装 JDK 17,并配置环境变量 JAVA_HOME 指向 JDK 17 的安装路径。在 IntelliJ IDEA 中验证 JDK 配置:File → Project Structure → SDKs。 安装 IntelliJ IDEA&#x…...
指令级并行(ILP)和线程级并行(TLP)的区别,GCC -O3优化会展开循环吗?
1. GCC 自动循环展开是怎么展开的? 当你使用 -O3 这样的优化选项时,GCC 会分析你的循环。如果它认为展开循环有利可图,它会做类似这样的事情(概念上的): 原始循环 (Conceptual C Code): for (int i 0; i …...
hadoop伪分布式模式
以下是 Hadoop 伪分布式模式(Pseudo-Distributed Mode)的环境搭建步骤。伪分布式模式下,Hadoop 的各个组件(如 HDFS、YARN、MapReduce)以独立进程运行,但所有服务均部署在单台机器上,模拟多节点…...
C++入门小馆: 模板
嘿,各位技术潮人!好久不见甚是想念。生活就像一场奇妙冒险,而编程就是那把超酷的万能钥匙。此刻,阳光洒在键盘上,灵感在指尖跳跃,让我们抛开一切束缚,给平淡日子加点料,注入满满的pa…...
# 基于 Python 和 jieba 的中文文本自动摘要工具
基于 Python 和 jieba 的中文文本自动摘要工具 在信息爆炸的时代,快速准确地提取文本核心内容变得至关重要。今天,我将介绍一个基于 Python 和 jieba 的中文文本自动摘要工具,帮助你高效地从长文本中提取关键信息。 一、背景与需求 在处理…...
.NET平台用C#在PDF中创建可交互的表单域(Form Field)
在日常办公系统开发中,涉及 PDF 处理相关的开发时,生成可填写的 PDF 表单是一种常见需求,例如员工信息登记表、用户注册表、问卷调查或协议确认页等。与静态 PDF 不同,带有**表单域(Form Field)**的文档支持…...
Azure AI Foundry实战:从零开始构建智能应用
1. 引言 在人工智能快速发展的今天,如何高效地开发和部署AI应用已成为众多开发者和企业关注的焦点。微软的Azure AI Foundry应运而生,为AI应用开发提供了一站式解决方案。本文将带您深入了解Azure AI Foundry,并通过实战指南,帮助您从零开始构建智能应用。 2. Azure AI Found…...
YOLO视觉模型可视化训练与推理测试工具
推荐一款YOLO可视化训练测试工具: 对于yolo的训练,新手小白往往无从下手,本章推荐的这款工具可以非常轻易的帮您从模型训练到测试到部署。 下载地址http://www.voouer.com/yolo 可以点击此处跳转。 下载成功后打开这款工具,将会出现图形化界面,类似于下图所示: 当前页是可视…...
数据清洗的定义跟实际操作
数据清洗的定义 数据清洗(Data Cleaning) 是指对原始数据进行处理,以纠正、删除或填补不完整、不准确、重复或无关的数据,使其符合分析或建模的要求。数据清洗是数据预处理的关键步骤,直接影响后续分析和机器学习模型…...