MacOS环境-手写操作系统-29-进程切换

简介: MacOS环境-手写操作系统-29-进程切换

进程切换

1.简介

操作系统内核开发 一个及其重要的模块是进程以及进程调度


在大学的操作系统课堂上 研究进程和相关调度算法 是一块耗时耗力的内容


市面上 讲解操作系统进程概念以及调度算法的内容可谓是汗牛充栋 记得我以前读相关内容时 看到很多算法流程图 伪码说明等等


但无论描述的如何详细 但只要我无法动手实践 那么也只能是隔靴搔痒 心中困顿 始终无法排解


从本节开始 我们看看 如何通过代码实践的方式 把各种天花乱坠的进程算法落地实现


2.代码

进程的创建 主要是为了实现多任务


就算只有一个CPU 我们也应该可以一边听歌 一边写邮件


既然需要多个任务“同时进行” 那么就需要每个任务在运行时 不能互相干扰


一个任务对数据的读取 绝对不可以影响别的进程的数据


一般而言 对于单CPU硬件来说 多任务其实是一种假象


他们同时运行 其实不过是CPU快速在各个任务间切换的结果而已


当一个任务从前台切换到后台时 需要把当前进程运行所需要的各种信息保存好


当下次进程重新切换回前台时 需要把当时保存好的信息重新加载 这样进程就能顺利的”死灰复燃“了


2.1 基本数据结构的说明

我们先看一个用户切换进程的数据结构 就能大概了解进程的相关特性 以及切换时需要保存什么内容了


代码文件multi_task.h


struct TSS32 {
    int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3;
    int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;
    int es, cs, ss, ds, fs, gs;
    int ldtr, iomap;
};


上面的数据结构 称为一个任务门描述符 是intel X86架构的CPU专门供给的


当发生任务切换时 CPU通过加装上面给定的数据结构


将当前进程的相关信息写入TSS32 从而实现当前进程的运行环境保护


我们看看里面的相关字段


(1)eflags 是进程运行时的状态字段 这个字段用于决定当前硬件中断是否打开 是否有运算溢出等信息


在我们内核的汇编代码部分 有一个专门的函数叫io_load_eflags 这个函数就是专门用来加载或存储这个字段的


(2)当前进程需要保留的还有各个用于运行时的通用寄存器 像eax,ebx等等


(3)需要关注的是cs, ss ,ds, 等段寄存器 这些寄存器指向的是全局描述符表中的相关表项


cs指向的全局描述符 说明的是一段内存的起始地址和大小 这段内存是当前进程代码所在地


ds指向的描述符 说明的内存是当前进程用于存储数据的内存


ss指向的描述符也说明一段内存 这段内存用来当做进程运行时的栈来使用


因此这一系列段寄存器必须小心保存 一旦他们的数值错误 进程的运行就会产生混乱甚至奔溃


其他的字段我们暂时用不上 先不必花费精力来了解


TSS32数据结构


长度为104字节 但是我们的结构体总共有104字节 这多出的一字节 是为了使用方便而已 没有多余意义


当我们初始化了TSS32后 在全局描述符表中 需要专门分配一个描述符来指向这块TSS32内存


这种描述符 称为:任务门


multi_task.h中 包含全局描述符数据结构的定义
struct SEGMENT_DESCRIPTOR {
    short limit_low, base_low;
    char base_mid, access_right;
    char limit_high, base_high;
};
void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar);
#define AR_TSS32        0x0089

set_segmdesc这个函数用来实现对一个描述符的设置


同样 在内核的汇编部分 也存在对描述符进行设置的代码


这个函数其实就是把汇编部分的逻辑用C语言重新实现了一遍


2.2 进程切换代码的说明

multi_task.c的实现

#include "multi_task.h"

void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
{
    if (limit > 0xfffff) {
        ar |= 0x8000; /* G_bit = 1 */
        limit /= 0x1000;
    }
    sd->limit_low    = limit & 0xffff;
    sd->base_low     = base & 0xffff;
    sd->base_mid     = (base >> 16) & 0xff;
    sd->access_right = ar & 0xff;
    sd->limit_high   = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0);
    sd->base_high    = (base >> 24) & 0xff;
    return;
}

上面这段代码 作用是设置一个全局描述符 它的功能跟我们在内核汇编部分实现的一模一样


当我们初始化好一个TSS32数据结构 同时构造一个全局描述符指向这个TSS32数据块后


然后通过一条CPU指令 把这个数据库加载到CPU中 这条指令是LTR


我们在内核的汇编部分专门封装了这条指令 以便内核的C语言部分调用 代码如下(kernel.asm):


load_tr:
        LTR  [esp + 4]
        ret


这条指令执行后 当有任务切换时 CPU会把当前进程的相关信息写入到TSS32数据结构中


这个结构就是通过上面指令存入CPU的


同时 我们的内核创建一个新的TSS32数据结构 把要切换的进程的相关信息写入到这个数据结构中


CPU把老进程的信息存储到第一个TSS32中 从第二个TSS32中把新进程的信息加载起来


这样就实现了进程的新老交替


我们现在内核的汇编部分添加几个描述符用于指向不同的TSS32结构


代码如下(kernel.asm)


LABEL_GDT:
....
LABEL_DESC_6:       Descriptor        0,      0fffffh,       0409Ah
LABEL_DESC_7:       Descriptor        0,      0,       0
LABEL_DESC_8:       Descriptor        0,      0,       0
LABEL_DESC_9:       Descriptor        0,      0,       0


LABEL_DESC_6 LABEL_DESC_7 LABEL_DESC_8 LABEL_DESC_9


这几个描述符是为了实现任务切换而新增的


具体使用 我们下面会详细说明


Descriptor是内核的汇编部分对全局描述符的定义


其跟C语言部分的SEGMENT_DESCRIPTOR是完全等价的


内核的C语言部分,在CMain函数里

void CMain(void) {
....
static struct TSS32 tss_a, tss_b;
    struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *)get_addr_gdt();
    tss_a.ldtr = 0;
    tss_a.iomap = 0x40000000;
    tss_b.ldtr = 0;
    tss_b.iomap = 0x40000000;
    set_segmdesc(gdt + 7, 103, (int) &tss_a, AR_TSS32);

    set_segmdesc(gdt + 8, 103, (int) &tss_a, AR_TSS32);

    set_segmdesc(gdt + 9, 103, (int) &tss_b, AR_TSS32);

    set_segmdesc(gdt + 6, 0xffff, task_b_main, 0x409a);

    load_tr(7*8);

    taskswitch8();
....
}

我们先定义了两个TSS32结构 分别是tss_a, tss_b,这两个结构将分别对应两个不同的任务


然后初始化两个字段ldtr 和 iomap.这两个字段的作用我们先不用关心 但它们的值不能乱写


gdt是全局描述符表的头地址 根据首地址片偏移7 对应的就是前面我们说的LABEL_DESC_7 其余的同理


接着 通过seg_segmdesc把tss_a的起始地址写入到描述符中


注意 我们对LABEL_DESC_8也同样写入tss_a 这是一个小技巧 纯粹是为了进行技术说明


下面我们会看到它的使用


set_segmdesc(gdt + 9, 103, (int) &tss_b, AR_TSS32);


把tss_b的地址写入到描述符LABEL_DESC_9


然后把描述符LABEL_DESC_7通过ltr指令加载到CPU中


我们知道LABEL_DESC_7对应的是tss_a 所以通过调用


load_tr(7*8);


CPU就知道tss_a的存在了


需要说明的是 上面代码中的7对应的就是描述符在整个表中的下标 为什么要乘以8呢?


乘以8相当于把下标数值左移3位 这是x86架构的规定


当要访问全局描述符表中的某个表项时 必须把下标左移3位 这样就会空出3个比特位 这3个位是有重要用处的


以后我们会涉及到


接着通过调用taskswitch8() 这时将进行一次任务切换


也就是进程的调度 这里需要我们注意理解


先看taskswitch8的代码实现


它的实现在内核的汇编部分kernel.asm


taskswitch8:
        jmp  8*8:0
        ret
    taskswitch7:
        jmp  7*8:0
        ret
    taskswitch6:
        jmp  6*8:0
        ret
    taskswitch9:
        jmp 9*8:0
        ret


我们最开始实现从实模式向保护模式跳转的时候 就使用过

jump 全局描述符下标*8 : 偏移地址

这种格式的代码指令 taskswitch8 的实现


就是让CPU跳转到下标为8的描述符所指向的内存 乘以8的原因 我们在前面解释了


下标为8的描述符对应的就是LABEL_DESC_8 我们前面曾经用代码

set_segmdesc(gdt + 8, 103, (int) &tss_a, AR_TSS32);

来设置过 也就是说 这个描述符指向的就是tss_a结构


并且这个描述符的属性是AR_TSS32 当CPU把该描述符加载后 读取该描述符的属性 发现属性是AR_TSS32


于是CPU知道当前这个描述符是指向一个TSS32结构的 那么加载这样的描述符就意味着要进行一次任务切换


于是它把当前任务的运行环境 也就是当前的各个寄存器的值


先存储到早先通过ltr加载的tss32结构中 然后再从此次加载的tss32结构中读取相关信息 进而执行新的任务


我们先把当前运行着CMain的任务切换到后台 然后通过读取tss_a中的数据


再次把切换回后台的任务重新加载执行


这样我们就是实现了一个任务的自我切换


那么怎么证明一个任务从自己切换到自己呢


当我们定义了tss_a结构时 只初始化了两个字段 分别是ldtr 和iomap 其他字段默认为0


由于发生了任务切换 CPU会把相关寄存器信息写入到tss_a的对应字段


这样 我们只要把其他字段打印出来 如果他们的值不再是0的话


那就意味着曾经有任务切换过 并且CPU把被切换的任务的相关信息写入到了tss_a数据结构中


于是我们通过代码打印出tss_a的相关字段

char *p = intToHexStr(tss_a.eflags);
    showString(shtctl, sht_back, 0, 0, COL8_FFFFFF, p);

    p = intToHexStr(tss_a.esp);
    showString(shtctl, sht_back, 0, 16, COL8_FFFFFF, p);

    p = intToHexStr(tss_a.es / 8);
    showString(shtctl, sht_back, 0, 32, COL8_FFFFFF, p);

    p = intToHexStr(tss_a.cs / 8);
    showString(shtctl, sht_back, 0, 48, COL8_FFFFFF, p);

    p = intToHexStr(tss_a.ss / 8);
    showString(shtctl, sht_back, 0, 64, COL8_FFFFFF, p);

    p = intToHexStr(tss_a.ds / 8);
    showString(shtctl, sht_back, 0, 80, COL8_FFFFFF, p);

    p = intToHexStr(tss_a.gs / 8);
    showString(shtctl, sht_back, 0, 96, COL8_FFFFFF, p);

    p = intToHexStr(tss_a.fs / 8);
    showString(shtctl, sht_back, 0, 112, COL8_FFFFFF, p);

    p = intToHexStr(tss_a.cr3);
    showString(shtctl, sht_back, 0, 128, COL8_FFFFFF, p);

3.编译和运行

make


java


img


大家看左上角的一排数字


对应的就是tass_a相关字段的内容


tss_a初始化时 这些字段都是默认为0的


但打印出来的时候 有一些不是0


我们又没有在代码里主动进行设置


这么说来 这些字段的设置 只能是CPU亲手写入的


也就是说 我们实现了一次当前任务到其自身的切换!

目录
相关文章
|
1月前
|
算法 调度 UED
深入理解操作系统:进程调度与优先级队列
【10月更文挑战第31天】在计算机科学的广阔天地中,操作系统扮演着枢纽的角色,它不仅管理着硬件资源,还为应用程序提供了运行的环境。本文将深入浅出地探讨操作系统的核心概念之一——进程调度,以及如何通过优先级队列来优化资源分配。我们将从基础理论出发,逐步过渡到实际应用,最终以代码示例巩固知识点,旨在为读者揭开操作系统高效管理的神秘面纱。
|
25天前
|
消息中间件 安全 算法
深入理解操作系统:进程管理的艺术
【10月更文挑战第38天】在数字世界的心脏,操作系统扮演着至关重要的角色。它不仅是硬件与软件的桥梁,更是维持计算机运行秩序的守夜人。本文将带你走进操作系统的核心——进程管理,探索它是如何协调和优化资源的使用,确保系统的稳定与高效。我们将从进程的基本概念出发,逐步深入到进程调度、同步与通信,最后探讨进程安全的重要性。通过这篇文章,你将获得对操作系统进程管理的全新认识,为你的计算机科学之旅增添一份深刻的理解。
|
29天前
|
算法 调度 UED
深入理解操作系统:进程管理与调度策略
【10月更文挑战第34天】本文旨在探讨操作系统中至关重要的一环——进程管理及其调度策略。我们将从基础概念入手,逐步揭示进程的生命周期、状态转换以及调度算法的核心原理。文章将通过浅显易懂的语言和具体实例,引导读者理解操作系统如何高效地管理和调度进程,保证系统资源的合理分配和利用。无论你是初学者还是有一定经验的开发者,这篇文章都能为你提供新的视角和深入的理解。
42 3
|
1月前
|
Linux 调度 C语言
深入理解操作系统:进程和线程的管理
【10月更文挑战第32天】本文旨在通过浅显易懂的语言和实际代码示例,带领读者探索操作系统中进程与线程的奥秘。我们将从基础知识出发,逐步深入到它们在操作系统中的实现和管理机制,最终通过实践加深对这一核心概念的理解。无论你是编程新手还是希望复习相关知识的资深开发者,这篇文章都将为你提供有价值的见解。
|
1月前
|
算法 调度 UED
深入理解操作系统的进程调度机制
本文旨在探讨操作系统中至关重要的组成部分之一——进程调度机制。通过详细解析进程调度的概念、目的、类型以及实现方式,本文为读者提供了一个全面了解操作系统如何高效管理进程资源的视角。此外,文章还简要介绍了几种常见的进程调度算法,并分析了它们的优缺点,旨在帮助读者更好地理解操作系统内部的复杂性及其对系统性能的影响。
|
1月前
深入理解操作系统:进程与线程的管理
【10月更文挑战第30天】操作系统是计算机系统的核心,它负责管理计算机硬件资源,为应用程序提供基础服务。本文将深入探讨操作系统中进程和线程的概念、区别以及它们在资源管理中的作用。通过本文的学习,读者将能够更好地理解操作系统的工作原理,并掌握进程和线程的管理技巧。
40 2
|
1月前
|
消息中间件 算法 Linux
深入理解操作系统之进程管理
【10月更文挑战第30天】在数字时代的浪潮中,操作系统作为计算机系统的核心,扮演着至关重要的角色。本文将深入浅出地探讨操作系统中的进程管理机制,从进程的概念入手,逐步解析进程的创建、调度、同步与通信等关键过程,并通过实际代码示例,揭示这些理论在Linux系统中的应用。文章旨在为读者提供一扇窥探操作系统深层工作机制的窗口,同时激发对计算科学深层次理解的兴趣和思考。
|
1月前
|
消息中间件 算法 调度
深入理解操作系统:进程管理与调度策略
【10月更文挑战第29天】本文将带领读者深入探讨操作系统中的核心组件之一——进程,并分析进程管理的重要性。我们将从进程的生命周期入手,逐步揭示进程状态转换、进程调度算法以及优先级调度等关键概念。通过理论讲解与代码演示相结合的方式,本文旨在为读者提供对进程调度机制的全面理解,从而帮助读者更好地掌握操作系统的精髓。
42 1
|
1月前
|
消息中间件 算法 调度
深入理解操作系统:进程管理的艺术
【10月更文挑战第33天】本文旨在揭示操作系统中进程管理的神秘面纱,带领读者从理论到实践,探索进程调度、同步以及通信的精妙之处。通过深入浅出的解释和直观的代码示例,我们将一起踏上这场技术之旅,解锁进程管理的秘密。
27 0
|
1月前
|
算法 Linux 调度
深入理解操作系统之进程调度
【10月更文挑战第31天】在操作系统的心脏跳动中,进程调度扮演着关键角色。本文将深入浅出地探讨进程调度的机制和策略,通过比喻和实例让读者轻松理解这一复杂主题。我们将一起探索不同类型的调度算法,并了解它们如何影响系统性能和用户体验。无论你是初学者还是资深开发者,这篇文章都将为你打开一扇理解操作系统深层工作机制的大门。
34 0