当前位置: 首页 > news >正文

从0写自己的操作系统(4)实现简单的任务切换

这是前面的一些文章
从0写自己的操作系统(1) boot->loader引导程序
从0写自己的操作系统(2) loader->kernel加载操作系统内核
从0写自己的操作系统(3)x86操作系统的中断和异常处理

TSS结构

在这里插入图片描述

/*** tss描述符*/
typedef struct _tss_t {uint32_t pre_link;uint32_t esp0, ss0, esp1, ss1, esp2, ss2;uint32_t cr3;uint32_t eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;uint32_t es, cs, ss, ds, fs, gs;uint32_t ldt;uint32_t iomap;
}tss_t;

esp一般为栈顶
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
要做任务切换,需要在GDT表中添加TSS描述符

TSS描述符结构

在这里插入图片描述
jmp 跟上tss选择子进行跳转 具体的状态转换和恢复都是硬件自动完成的

static int tss_init (task_t * task, int flag, uint32_t entry, uint32_t esp) {// 为TSS分配GDTint tss_sel = gdt_alloc_desc();if (tss_sel < 0) {log_printf("alloc tss failed.\n");return -1;}segment_desc_set(tss_sel, (uint32_t)&task->tss, sizeof(tss_t),SEG_P_PRESENT | SEG_DPL0 | SEG_TYPE_TSS);// tss段初始化kernel_memset(&task->tss, 0, sizeof(tss_t));// 分配内核栈,得到的是物理地址uint32_t kernel_stack = memory_alloc_page();if (kernel_stack == 0) {goto tss_init_failed;}// 根据不同的权限选择不同的访问选择子int code_sel, data_sel;if (flag & TASK_FLAG_SYSTEM) {code_sel = KERNEL_SELECTOR_CS;data_sel = KERNEL_SELECTOR_DS;} else {// 注意加了RP3,不然将产生段保护错误code_sel = task_manager.app_code_sel | SEG_RPL3;data_sel = task_manager.app_data_sel | SEG_RPL3;}task->tss.eip = entry;task->tss.esp = esp ? esp : kernel_stack + MEM_PAGE_SIZE;  // 未指定栈则用内核栈,即运行在特权级0的进程task->tss.esp0 = kernel_stack + MEM_PAGE_SIZE;task->tss.ss0 = KERNEL_SELECTOR_DS;task->tss.eip = entry;task->tss.eflags = EFLAGS_DEFAULT| EFLAGS_IF;task->tss.es = task->tss.ss = task->tss.ds = task->tss.fs = task->tss.gs = data_sel;   // 全部采用同一数据段task->tss.cs = code_sel; task->tss.iomap = 0;// 页表初始化uint32_t page_dir = memory_create_uvm();if (page_dir == 0) {goto tss_init_failed;}task->tss.cr3 = page_dir;task->tss_sel = tss_sel;return 0;
tss_init_failed:gdt_free_sel(tss_sel);if (kernel_stack) {memory_free_page(kernel_stack);}return -1;
}

tss_sel 是一个段选择子,而不是段描述符本身。它是一个 16 位结构,包含 GDT 索引、TI 位和特权级 RPL。CPU 通过 tss_sel >> 3 计算出 GDT 表项索引,从而找到真实的段描述符数据。我们设置 GDT 表项内容,用的是 segment_desc_set;而 tss_sel 是后续加载到 TR 寄存器中,让 CPU 识别当前任务 TSS 的唯一标识。

ESP EIP

esp 是栈顶指针,决定了函数调用时参数和返回地址的存储位置;而 eip 是指令指针,表示当前 CPU 执行的机器码位置。它们配合完成函数调用、返回、中断处理等流程,但并不指向彼此。TSS 中保存的 esp 和 eip 表示这个任务在被切换出去时的运行状态,后续切换回来时,CPU 会用 eip 恢复执行地址,用 esp 恢复栈环境,从而无缝继续执行。

相关理解:

说法是否正确解释
esp 是用户态的栈顶是的,用于函数/系统调用等
esp0 是内核态的栈顶中断或系统调用进入内核时使用
“栈顶”是不是“最后结束的位置”?栈顶是当前调用栈的起点(向低地址增长),不是终点
eip 是栈顶指向的代码地址?eipesp 无直接指向关系,esp 里可能保存的是“函数返回地址”,但不是 “eip = *esp”

✅ 那 esp 有什么用?

功能场景
调用函数时保存返回地址call 指令时 push eip
保存参数、局部变量栈帧 = 参数 + 变量
系统调用压栈用户态寄存器保存中断前用户上下文
中断返回恢复 esp 状态iret 从栈恢复 esp

✅ 总结:

esp 是栈顶指针,决定了函数调用时参数和返回地址的存储位置;而 eip 是指令指针,表示当前 CPU 执行的机器码位置。它们配合完成函数调用、返回、中断处理等流程,但并不指向彼此。TSS 中保存的 esp 和 eip 表示这个任务在被切换出去时的运行状态,后续切换回来时,CPU 会用 eip 恢复执行地址,用 esp 恢复栈环境,从而无缝继续执行

tr寄存器跳转下一个任务

TR 寄存器(Task Register) 是 x86 架构中实现任务管理和特权栈切换的核心机制之一,尤其在使用 TSS(任务状态段)时非常关键。TR 是 x86 中的 Task Register(任务寄存器),它保存了当前活动任务的 TSS 段选择子(Selector),通过它,CPU 可以访问当前任务的 TSS,实现任务切换、栈切换等功能。

TR 寄存器作用总结

功能描述
指向当前 TSS保存当前任务对应的 TSS 段选择子(即 GDT 中的哪一项)
任务切换入口ljmp 到一个 TSS 段描述符时,CPU 会更新 TR
栈切换时用到 esp0中断时,CPU 会通过 TR 定位当前任务的 TSS,然后使用其中的 esp0ss0
硬件任务切换支持x86 早期支持使用 TSS 做硬任务切换,TR 是其状态入口

ss0 是 TSS 中的内核态栈段选择子,当 CPU 从用户态(CPL=3)切换到内核态(CPL=0)时,自动将 ss0 加载到 SS 寄存器,用于切换到内核态栈。

🧠 TR 是怎么设置的?

✅ 使用 ltr 指令加载:

mov ax, tss_selector
ltr ax         ; Load Task Register
  • 你必须先把 TSS 的段描述符写入 GDT
  • 然后用 ltr 将其加载到 TR
  • 之后 CPU 会自动使用它进行特权栈切换(用户态 → 内核态)

因为 tss_sel 是一个指向 TSS 段的 GDT 选择子,当你执行 ljmp tss_sel:0 时,不是普通的代码段跳转,而是触发了 硬件级任务切换机制 —— CPU 自动从目标 TSS 中加载完整上下文(EIP、ESP、CR3…)并跳转。
在这里插入图片描述

另一种任务切换的方式

手动保存

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

.text.global simple_switch
simple_switch:movl 4(%esp), %eax   // 取from->stackmovl 8(%esp), %edx   // 取to->stack// 保存前一任务的状态push %ebppush %ebxpush %esipush %edi// 切换栈mov %esp, (%eax)    // from->stack = espmov %edx, %esp      // esp = to->stack// 加载下一任务的栈pop %edipop %esipop %ebxpop %ebpret.global exception_handler_syscall.extern do_handler_syscall

看起来都是 mov,但它们语义完全不同:

movl 4(%esp), %eax   // 取from->stack
mov %esp, (%eax)    // from->stack = esp
指令类型含义
mov src, reg加载把内存/立即数放进寄存器
mov reg, [addr]存储把寄存器内容写入某个内存地址

汇编指令虽然语法形式看起来一致,但是由左右操作数结构决定含义的,类似于 C 语言的 a = b;,你得看 a 和 b 的含义才能知道语义。

尽管两条 mov 指令语法形式相似,但它们的语义完全不同:第一条是读取参数,第二条是保存 esp。汇编是一种显式控制语言,赋值完全依赖操作数而不是上下文,所以只要我在使用 %eax 之前显式地把它赋值为 from->stack,就不会有“旧值脏数据”的风险。这个流程非常重要,因为栈顶 esp 的准确保存关系到任务是否能正确恢复。

单独修改 esp 不会影响 eip,因为它们是两个独立寄存器。真正会让 eip 改变的,是像 ret 或 iret 这样的指令,它们会从 esp 指向的栈里弹出新的执行地址,赋值给 eip。所以在任务切换中,我们常先切 esp,再执行 ret 来跳转回新任务保存的 eip,实现上下文恢复。

http://www.lryc.cn/news/581710.html

相关文章:

  • FileZilla二次开发实战指南:C++架构解析与界面功能扩展
  • 在Ubuntu 24.04上部署Zabbix 7.0对服务器进行监控
  • 【机器学习笔记Ⅰ】13 正则化代价函数
  • [2025CVPR]一种新颖的视觉与记忆双适配器(Visual and Memory Dual Adapter, VMDA)
  • SSL 终结(SSL Termination)深度解析:从原理到实践的全维度指南
  • Python Bcrypt详解:从原理到实战的安全密码存储方案
  • 用户中心Vue3项目开发2.0
  • 2048小游戏实现
  • 线性代数--AI数学基础复习
  • 深度学习6(多分类+交叉熵损失原理+手写数字识别案例TensorFlow)
  • Chunking-free RAG
  • Web-API-day2 间歇函数setInterval与事件监听addEvenListener
  • 【Note】《Kafka: The Definitive Guide》第四章:Kafka 消费者全面解析:如何从 Kafka 高效读取消息
  • Apache Spark 4.0:将大数据分析提升到新的水平
  • A O P
  • 金融级B端页面风控设计:操作留痕与异常预警的可视化方案
  • 深度学习篇---深度学习常见的应用场景
  • 容声W60以光水离子科技实现食材“主动养鲜”
  • [Qt] visual studio code 安装 Qt插件
  • FastAPI + Tortoise-ORM + Aerich 实现数据库迁移管理(MySQL 实践)
  • 深度学习 必然用到的 线性代数知识
  • 嵌入式 数据结构学习(五) 栈与队列的实现与应用
  • React Ref 指南:原理、实现与实践
  • 【PyTorch】PyTorch中torch.nn模块的卷积层
  • 零基础,使用Idea工具写一个邮件报警程序
  • Solidity——什么是状态变量
  • 计算机网络:(七)网络层(上)网络层中重要的概念与网际协议 IP
  • Kafka “假死“现象深度解析与解决方案
  • UI前端大数据可视化进阶:交互式仪表盘的设计与应用
  • 数据驱动实时市场动态监测:让商业决策跑赢时间