从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分配GDT
    int 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->stack
	movl 8(%esp), %edx   // 取to->stack

	// 保存前一任务的状态
	push %ebp
	push %ebx
	push %esi
	push %edi

	// 切换栈
	mov %esp, (%eax)    // from->stack = esp
  	mov %edx, %esp      // esp = to->stack

	// 加载下一任务的栈
	pop %edi
	pop %esi
	pop %ebx
	pop %ebp
  	ret

     .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,实现上下文恢复。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值