从0实现32位操作系统-实现简单进程切换

本文分享了作者在学习操作系统课程后,通过实现简单进程切换和TSS(任务状态段)来巩固知识的过程。介绍了进程的概念,如何在单任务系统中添加新任务,并详细展示了TSS的使用和任务切换的代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

从0实现32位操作系统-实现简单进程切换

本部分内容是本人学习了李述铜老师的操作系统课程之后想巩固知识点的笔记分享。有任何问题都可以在评论区或者直接加我的qq:2511010742联系。我也是在学习的小白,希望可以和大家共同进步。

一、前情回顾

在上一篇博客中我们实现了一些操作系统的中断异常处理程序,添加了一个除零异常的案例。使得操作系统在出现异常时能够正常的执行对应的异常处理。本节中将实现一个简单的进程切换案例。

二、进程

我在本站上有一篇关于进程和线程的文章,点击这里,主要以线程为主。
进程是操作系统进行资源分配和保护的基本单位,进程是程序的一次执行过程。进程是动态产生的,也是动态消亡的,它拥有自己的生命周期。
在操作系统中,进程主要由以下三部分组成:

  1. 进程控制块(PCB)
    1. 进程描述信息
    2. 进程控制与管理信息
    3. 资源分配清单
    4. CPU相关信息
  2. 数据段 即进程运行过程中各种数据(比如程序中定义的变量)
  3. 程序段 就是程序的代码(指令序列)

这些复杂的进程信息会在以后的章节中完成,本节只是先实现简单的任务切换。

三、添加任务

在之前的程序中我们的操作系统在进入内核初始化函数 kernel_init 函数之后会进入 init_main 函数,我们可以把这个函数当作我们的第一个任务,目前程序是一个单任务执行状态。

_start:
    push %ebp
    mov %esp, %ebp
    mov 0x8(%ebp), %eax

    # kernel_init(boot_info)
    push %eax
    call kernel_init

    jmp $KERNEL_SELECTOR_CS, $gdt_reload
    
gdt_reload:
    mov $KERNEL_SELECTOR_DS, %ax
	mov %ax, %ds
	mov %ax, %ss
	mov %ax, %es
	mov %ax, %fs
	mov %ax, %gs

    mov $(stack + KERNEL_STACK_SIZE), %esp
    jmp init_main
    # 接下来就会跳到init_main()函数执行

	void init_main(void) {
    	int count = 0;
    	for(;;) {
        	log_printf("int main: %d", count++);
    	}
	}
	// 目前只有这一个任务

我们知道程序在执行的时候是串行执行的,如果不做一些特殊的操作,是不可以同时执行两个任务的。我们在init_main函数之外再创建一个任务init_task_entry任务。

	void init_main(void) {
    	int count = 0;
    	for(;;) {
        	log_printf("int main: %d", count++);
    	}
	}

	void init_task_entry(void) {
    	int count = 0;
    	for(;;) {
        	log_printf("int task: %d", count++);
    	}
	}

这是两个不同的任务,每个任务里面都有自己的一个循环程序,所以是不会同时执行这两个任务的。接下来将讲述关于进程任务切换的TSS相关知识。

四、TSS(任务状态段)

也是位于内存中的结构体,可以利用它来进行任务切换,在intel开发手册中给出了TSS的结构图
在这里插入图片描述
在现代操作系统中,都没有使用这种方法进行任务切换,这种方式的切换速度是非常慢的,一条指令要消耗将近200多个时钟周期,但是我们这个操作系统使用了这个方法。

在一个多任务环境中,当发生了任务切换,需保护现场,因此每个任务的应当用一个额外的内存区域保存相关信息,即任务状态段(TSS);TSS格式固定,104个字节,处理器固件能识别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;

每一个任务都有这样一个TSS结构体信息,将TSS视为一种特殊的内存信息,每一个TSS都有一个对应的TSS描述符,和之前的GDT表中的描述符元素是一样的。由于本人能力有限,这段内容文字描述会显得有些抽象,下面直接上代码,配合代码注释会显得较为清晰一些。

// 定义一个任务task_t结构体,每个任务都会有一个对应的结构体
typedef struct _task_t {
    tss_t tss;		// TSS结构体
    int tss_sel;	// 对应的tss选择子
}task_t;

// 初始化tss信息
static int tss_init(task_t* task, uint32_t entry, uint32_t esp) {
    int tss_sel = gdt_alloc_desc();	// 在GDT表中找到一个空闲的描述符
    if(tss_sel < 0) {
        log_printf("alloc tss failed.\n");
        return -1;
    }
    
    // 对段信息进行设置,选择子为tss选择子,基地址为每个任务task结构体中的tss结构体
    // 段存在   最高优先级   TSS段描述符模式
    segment_desc_set(tss_sel, (uint32_t)&task->tss, sizeof(tss_t),
        SEG_P_PRESENT | SEG_DPL0 | SEG_TYPR_TSS);

	// 将tss信息清零
    kernel_memset(&task->tss, 0, sizeof(tss_t));
    task->tss.eip = entry;	// tss.eip设置为任务的入口地址
    task->tss.esp = task->tss.esp0 = esp;   // 特权及0,所以是esp0
    task->tss.ss = task->tss.ss0 = KERNEL_SELECTOR_DS;	// 数据段
    task->tss.es = task->tss.ds = task->tss.fs = task->tss.gs = KERNEL_SELECTOR_DS;
    task->tss.cs = KERNEL_SELECTOR_CS;	// 代码段
    task->tss.eflags = EFLAGS_IF | EFLAGS_DEFAULT;

    task->tss_sel = tss_sel;
    return 0;
}

int task_init(task_t* task, uint32_t entry, uint32_t esp) {
    ASSERT(task != (task_t*)0);

    tss_init(task, entry, esp);
    return 0;
}

以上代码的作用是将每个任务对应的tss结构体信息加载到对应的tss描述符中,并设置对应的描述符参数,这样加载好对应的信息之后就可以进行任务切换了。

static task_t first_task;	// 第一个任务的task结构体,对应init_main任务
static uint32_t init_task_stack[1024];	// 第二个任务所需要的栈,用来保存对应的栈信息
static task_t init_task;	// 第二个任务的task结构体,对应init_task_entry任务

// 代码一开始是运行在init_main任务中的,我们在此函数中进行各个任务的初始化
/*
	第二个参数为init_task_entry任务的入口地址
	因为栈是从高地址向低地址增长的,所以传的是init_task_stack[1024]
*/
task_init(&init_task, (uint32_t)init_task_entry, (uint32_t)&init_task_stack[1024]);
// 初始化第一个任务
task_init(&first_task, 0, 0);
// 在切换任务时,CPU会将当前任务的寄存器数据保存在当前tr寄存器所指的tss结构体中,
// 然后将新的tss数据复制到新的任务寄存器中。
// 因为先运行的是first_task对应的任务
// 所以这里先将first_task.tss_sel写入tr寄存器中
// 在切换任务时就会按照计划进行切换
write_tr(first_task.tss_sel);

// 我们的任务切换函数是这样封装的
void switch_to_tss(int tss_sel) {
    far_jump(tss_sel, 0);   // 因为tss_sel已经存在了目标任务的入口地址,就不需要偏移量了
}
// 直接跳转的下一个任务对应的选择子中

以上的操作初始化了两个任务init_task和first_task对应init_task_entry和init_main这两个函数,下面开始切换任务

void init_main(void) {
    task_init(&init_task, (uint32_t)init_task_entry, (uint32_t)&init_task_stack[1024]);
    task_init(&first_task, 0, 0);
    write_tr(first_task.tss_sel);

    int count = 0;
    for(;;) {
        log_printf("int main: %d", count++);
        task_switch_from_to(&first_task, &init_task);
    }
}

void init_task_entry(void) {
    int count = 0;
    for(;;) {
        log_printf("int task: %d", count++);
        task_switch_from_to(&init_task, &first_task);
    }
}

在这里插入图片描述
可以看到目前程序是运行到init_task_entry入口处,可以看到左边,上半部分是CPU调试信息,下半部分是init_task中的tss结构体信息,可以看到esp和eip寄存器的值是相同的。
在这里插入图片描述
可以看到是两个任务交替运行的

总结

以上,就实现了两个任务的简单切换。接下来会丰富进程的相关信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值