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月前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
88 1
|
1月前
|
调度 开发者 Python
深入浅出操作系统:进程与线程的奥秘
在数字世界的底层,操作系统扮演着不可或缺的角色。它如同一位高效的管家,协调和控制着计算机硬件与软件资源。本文将拨开迷雾,深入探索操作系统中两个核心概念——进程与线程。我们将从它们的诞生谈起,逐步剖析它们的本质、区别以及如何影响我们日常使用的应用程序性能。通过简单的比喻,我们将理解这些看似抽象的概念,并学会如何在编程实践中高效利用进程与线程。准备好跟随我一起,揭开操作系统的神秘面纱,让我们的代码运行得更加流畅吧!
|
16天前
|
监控 搜索推荐 开发工具
2025年1月9日更新Windows操作系统个人使用-禁用掉一下一些不必要的服务-关闭占用资源的进程-禁用服务提升系统运行速度-让电脑不再卡顿-优雅草央千澈-长期更新
2025年1月9日更新Windows操作系统个人使用-禁用掉一下一些不必要的服务-关闭占用资源的进程-禁用服务提升系统运行速度-让电脑不再卡顿-优雅草央千澈-长期更新
2025年1月9日更新Windows操作系统个人使用-禁用掉一下一些不必要的服务-关闭占用资源的进程-禁用服务提升系统运行速度-让电脑不再卡顿-优雅草央千澈-长期更新
|
1月前
|
C语言 开发者 内存技术
探索操作系统核心:从进程管理到内存分配
本文将深入探讨操作系统的两大核心功能——进程管理和内存分配。通过直观的代码示例,我们将了解如何在操作系统中实现这些基本功能,以及它们如何影响系统性能和稳定性。文章旨在为读者提供一个清晰的操作系统内部工作机制视角,同时强调理解和掌握这些概念对于任何软件开发人员的重要性。
|
1月前
|
Linux 调度 C语言
深入理解操作系统:从进程管理到内存优化
本文旨在为读者提供一次深入浅出的操作系统之旅,从进程管理的基本概念出发,逐步探索到内存管理的高级技巧。我们将通过实际代码示例,揭示操作系统如何高效地调度和优化资源,确保系统稳定运行。无论你是初学者还是有一定基础的开发者,这篇文章都将为你打开一扇了解操作系统深层工作原理的大门。
|
1月前
|
存储 算法 调度
深入理解操作系统:进程调度的奥秘
在数字世界的心脏跳动着的是操作系统,它如同一个无形的指挥官,协调着每一个程序和进程。本文将揭开操作系统中进程调度的神秘面纱,带你领略时间片轮转、优先级调度等策略背后的智慧。从理论到实践,我们将一起探索如何通过代码示例来模拟简单的进程调度,从而更深刻地理解这一核心机制。准备好跟随我的步伐,一起走进操作系统的世界吧!
|
1月前
|
算法 调度 开发者
深入理解操作系统:进程与线程的管理
在数字世界的复杂编织中,操作系统如同一位精明的指挥家,协调着每一个音符的奏响。本篇文章将带领读者穿越操作系统的幕后,探索进程与线程管理的奥秘。从进程的诞生到线程的舞蹈,我们将一起见证这场微观世界的华丽变奏。通过深入浅出的解释和生动的比喻,本文旨在揭示操作系统如何高效地处理多任务,确保系统的稳定性和效率。让我们一起跟随代码的步伐,走进操作系统的内心世界。
|
1月前
|
运维 监控 Linux
Linux操作系统的守护进程与服务管理深度剖析####
本文作为一篇技术性文章,旨在深入探讨Linux操作系统中守护进程与服务管理的机制、工具及实践策略。不同于传统的摘要概述,本文将以“守护进程的生命周期”为核心线索,串联起Linux服务管理的各个方面,从守护进程的定义与特性出发,逐步深入到Systemd的工作原理、服务单元文件编写、服务状态管理以及故障排查技巧,为读者呈现一幅Linux服务管理的全景图。 ####
|
2月前
|
算法 Linux 调度
深入浅出操作系统的进程管理
本文通过浅显易懂的语言,向读者介绍了操作系统中一个核心概念——进程管理。我们将从进程的定义出发,逐步深入到进程的创建、调度、同步以及终止等关键环节,并穿插代码示例来直观展示进程管理的实现。文章旨在帮助初学者构建起对操作系统进程管理机制的初步认识,同时为有一定基础的读者提供温故知新的契机。
|
1月前
|
消息中间件 算法 调度
深入理解操作系统之进程管理
本文旨在通过深入浅出的方式,带领读者探索操作系统中的核心概念——进程管理。我们将从进程的定义和重要性出发,逐步解析进程状态、进程调度、以及进程同步与通信等关键知识点。文章将结合具体代码示例,帮助读者构建起对进程管理机制的全面认识,并在实践中加深理解。

热门文章

最新文章