进程的初步实现

本文涉及的产品
公网NAT网关,每月750个小时 15CU
网络型负载均衡 NLB,每月750个小时 15LCU
应用型负载均衡 ALB,每月750个小时 15LCU
简介: 进程的初步实现

引言

  • 大家对上层应用程序中进程的使用应该不陌生,不过呢,本章节我们并不是介绍进程的使用,而是讲解操作系统中进程的具体实现
  • 本章节中所涉及的任务和进程是一个意思,只是叫法上的不同

一个问题

  • 计算机系统只有一个处理器,那么如何同时执行多个任务呢?

远古时期的计算机系统

  • 如下图所示,远古时期,处理器一次只执行一个任务,看起来没什么问题,一个处理器一次就只能执行一个任务,但是深入思考,当处理器进行外部 IO 操作时,由于外部 IO 操作速度很慢,而 CPU 的速度又特别快,此时 CPU 只能闲置在那等着,其它任务几乎处于空闲状态,只能等待当前任务执行完毕,CPU 资源就造成很大的浪费

  • 于是,人们自然的就想到,CPU 在进行 Task 1 的 IO 操作时,让处理器再去执行其它任务,多任务并行自然应运而生,当然,这里说的并行指的是宏观上的并行,从微观上看依旧是串行的,只是任务切换速度足够快,从人的感官上看是并行

任务的组成

  • 从操作系统角度考虑,一个任务主要由 4 个部分组成:代码、数据、栈、状态(任务执行时各个寄存器的值)

工程添加

  • 创建 "task.c" 文件,放到 "core" 文件夹下,并修改其下的 "BUILD.json", "src" 中添加 "task.c"
  • 创建 "task.h" 文件,把下面实现的任务相关的结构体定义写到该文件中,放到 "include" 文件夹下

任务的表示

  • 问题:如何在操作系统中表示一个任务呢?
  • 用面向对象的思想就是抽象出一个任务类,用 C 语言就是实现一个任务结构体
typedef struct TASK
{
    U32             id;                 // 任务 id
    U08*            name;               // 任务名称
    U08*            stack_addr;         // 任务栈基址
    U16             stack_size;         // 任务栈大小
    REG           reg;                // 任务上下文
} TASK;
  • reg: 任务上下文,即任务执行状态下所有寄存器的状态,这是一个结构体,比如当程序从 Task A 切换到 Task B 执行时,先把 Task A 的执行上下文保存到该数据结构中,然后,将 Task B 中该数据结构的值恢复给寄存器,这样就切换到 Task B 中执行了(程序跟着 cs:eip 指向的位置执行,当 cs:eip 由指向 Task A 改为指向 Task B 后,那么程序自动由 Task A 切换到 Task B 执行)。reg 定义如下:
typedef struct REG
{
    U32             gs;
    U32             fs;
    U32             es;
    U32             ds;
    U32             edi;
    U32             esi;
    U32             ebp;
    U32             kesp;
    U32             ebx;
    U32             edx;
    U32             ecx;
    U32             eax;
    U32             eip;
    U32             cs;
    U32             eflags;
    U32             esp;
    U32             ss;
} REG;

为什么上下文指的只是寄存器状态

  • 前面刚说过,一个任务主要由 4 个部分组成:代码、数据、栈、状态(寄存器),按理说上下文应该由这 4 个部分组成,为啥只有寄存器呢?
  • 原因:代码、数据、栈这 3 部分都可以提前准备,每个任务都有自己独立的代码、数据、栈,唯独寄存器不能够独有,因为处理器只有一套寄存器

实验:执行第一个任务(进程)

  • 其实在特权级相关章节中我们就已经实现了跳转到一个任务并执行,只不过那时候我们是用汇编实现
  • 实验代码:loader.asmtask.ctask.hmain
  • 参考以前实现过的代码 loader.asm 在当前的 loader.asm 添加任务状态段 TSS 和局部描述符表 LDT 相关代码(因为已经实现过,所以这里便不再重复讲解),只不过把 TASK A 的代码段转移到 kernel 中实现

执行第一个任务

  • 利用恢复上下文来实现执行任务,那么如何恢复上下文呢?

如何恢复上下文

  • 恢复上下文:根据 REG reg 数据结构中的值恢复到各个寄存器中
  • 恢复上下文即恢复寄存器,那么如何恢复寄存器呢?
  • 实现方式:首先将栈顶指针 esp 寄存器指向上下文数据结构 reg 的起始位置,把数据结构 reg 所处位置看成栈
  • ① 处可以借助 pop 指令将数据弹出到对应得寄存器中
  • ② 处其实也可以借助 pop 指令恢复寄存器,不过可以使用 popad 这一条命令替代【跳过 esp 寄存器(add esp, 4),放在这是因为入栈保存上下文时需要】
  • ③ 处 几条指令恰巧就是 iret 指令本质,在 中断处理与特权级转移 中详细介绍过,回顾一下iret:
  • 由此可见,REG reg 数据结构中各元素位置也是有一定顺序的,并不是随意排列的
  • 为啥不用 mov gs, xxx 恢复寄存器的值而是使用 pop gs 这种形式呢?原因就是 mov 指令可能会改变 eflags 寄存器的值,而 pop 指令不会改变 eflags 的值
  • 关键点:在恢复上下文的最后调用 iret 指令,iret 指令会修改 cs 和 eip 寄存器的值,当这两个寄存器改变之后,程序自动跳到 cs:eip 处执行,cs:eip 指向哪里,CPU 就跟着执行到哪里
  • 有了前面的知识,我们就可以实现实现内核切换到第一个任务(进程)了
  • 切换任务函数接口设计
  • 函数名称: E_RET SwitchTo(TASK* task)
  • 输入参数: TASK* task --任务指针
  • 输出参数: 无
  • 函数返回: E_OK:成功; E_ERR:失败
  • 切换任务函数实现
E_RET SwitchTo(TASK* task)
{
    // 取任务上下文(寄存器)基址
    U32* reg_base = (U32*)(&task->reg);
    // 检查参数合法性
    if(NULL == task || NULL == reg_base)
        return E_ERR;
    // 恢复上下文
    asm volatile(
                "movl %0, %%esp\n"      // 先将栈顶指针 esp 指向任务上下文 reg 的起始位置
                "popl %%gs\n"
                "popl %%fs\n"
                "popl %%es\n"
                "popl %%ds\n"
                "popal\n"               // popal(gcc) = popad(nasm)
                // "popl %%edi\n"
                // "popl %%esi\n"
                // "popl %%ebp\n"
                // "addl $4, %%esp\n"   // 跳过 esp 寄存器本身的恢复
                // "popl %%ebx\n"
                // "popl %%edx\n"
                // "popl %%ecx\n"
                // "popl %%eax\n"
                "iret\n"
                :
                : "r"(reg_base)         // %0 替换成 reg_base
                :
                );
}
  • 函数调用本身需要跳转地址再执行,函数调用(跳转)对处理器来说也是需要时间成本的,这涉及到 CPU 的 cache 缓冲机制,这里就不做讲解了。既然我们写的是操作系统,效率必须要考虑,可以用 C 语言宏定义的方式实现相同函数功能,宏在预编译阶段就会原地展开,这样就可以省去跳转的时间
#define SWITCH_TO(t)    (void)({    \
    U32* pBase = (U32*)(&(t)->reg); \
    asm volatile(                   \
        "movl %0, %%esp\n"          \
        "popl %%gs\n"               \
        "popl %%fs\n"               \
        "popl %%es\n"               \
        "popl %%ds\n"               \
        "popal\n"                   \
        "iret\n"                    \
        :                           \
        : "r"(pBase)                \
        );})
  • 接下来我们模拟一个任务(进程),然后调用任务切换函数,看看能够成功执行第一个任务(进程)
  • 准备一个任务所需的相关原料
TASK task = {0};      // 任务对象
U08 task_stack[512];    // 任务私有栈
void TaskAFunc(void)    // 任务执行函数
{
    printk("This is the first task.\n");
    while (1);
}
  • 初始化填充任务对象,最后再调用 SWITCH_TO 跳转到任务中执行
task.id = 1;
task.name = "Task A";
task.stack_addr = task_stack;
task.stack_size = 512;
task.reg.cs = LDT_CODE32_SELECTOR;
task.reg.gs = LDT_VIDEO_SELECTOR;
task.reg.ds = LDT_DATA32_SELECTOR;
task.reg.es = LDT_DATA32_SELECTOR;
task.reg.fs = LDT_DATA32_SELECTOR;
task.reg.ss = LDT_DATA32_SELECTOR;
task.reg.esp = (U32)task.stack_addr + task.stack_size;
task.reg.eip = (U32)TaskAFunc;
task.reg.eflags = 0x3002;
// SwitchTo(&task);
SWITCH_TO(&task);
  • 上面初始化填充时所需的相关宏定义我们临时在 main.c 中定义一下吧
// 段选择符属性定义
#define SA_RPL0      0                  // RPL = 0
#define SA_RPL1      1                  // RPL = 1
#define SA_RPL2      2                  // RPL = 2
#define SA_RPL3      3                  // RPL = 3
#define SA_TIG       0                  // TI = 0, GDT
#define SA_TIL       4                  // TI = 1, LDT
// 局部段选择符定义
#define LDT_VIDEO_INDEX         0
#define LDT_CODE32_INDEX        1 
#define LDT_DATA32_INDEX        2
#define LDT_VIDEO_SELECTOR     ((LDT_VIDEO_INDEX << 3) + SA_TIL + SA_RPL3)  
#define LDT_CODE32_SELECTOR    ((LDT_CODE32_INDEX << 3) + SA_TIL + SA_RPL3) 
#define LDT_DATA32_SELECTOR    ((LDT_DATA32_INDEX << 3) + SA_TIL + SA_RPL3)
  • 辛苦了那么久,最后运行效果怎么能不展示出来呢

  • 验证一下当前是否执行在特权级 3 状态,是否使用私有任务栈
  • 加两行代码,把私有任务栈的地址范围打印出来

printk("task stack start = %x\n",task_stack);

printk("task stack end = %x\n",task_stack+512);

  • 编译运行,从打印信息可以看到私有站地址范围是 0xE0A0 ~ 0xE2A0,此时,按 Ctrl+C 键,使用 “reg” 指令,查看到 esp 值为 0xE294,该值在任务私有栈的范围内,使用 “sreg” 指令,查看 cs 的值为 0xf, bit0-bit1 的值为 3,说明此时程序运行在特权级 3 状态下

任务(进程)创建函数

  • 上面的实验虽然取得了,但是初始化任务的工作我们也是临时实现的,现在我们就来把任务初始化填充工作封装到一个函数中吧
  • 任务(进程)创建函数接口设计
  • 函数名称: TaskCreat(TASK* task, TASK_FUNC pFunc, U08* stackAddr, U16 stackSize, U08* name)
  • 输入参数: TASK_FUNC pFunc --任务函数; U08* stackAddr --任务栈基址; U16 stackSize --任务栈大小; U08* name --任务名称
  • 输出参数: TASK* task --任务指针
  • 函数返回: E_OK:成功; E_ERR:失败
  • 具体的实现如下
E_RET TaskCreat(TASK* task, TASK_FUNC pFunc, U08* stackAddr, U16 stackSize, U08* name)
{
    // 检查参数合法性
    if(NULL == task || NULL == pFunc || NULL == stackAddr || 0 == stackSize)
        return E_ERR;
    task->name = name;
    task->stack_addr = stackAddr;
    task->stack_size = stackSize;
    task->reg.cs = LDT_CODE32_SELECTOR;
    task->reg.gs = LDT_VIDEO_SELECTOR;
    task->reg.ds = LDT_DATA32_SELECTOR;
    task->reg.es = LDT_DATA32_SELECTOR;
    task->reg.fs = LDT_DATA32_SELECTOR;
    task->reg.ss = LDT_DATA32_SELECTOR;
    task->reg.esp = (U32)task->stack_addr + task->stack_size; // 栈顶
    task->reg.eip = (U32)pFunc;
    task->reg.eflags = 0x3002;  // IOPL = 3, 允许任务(特权级 3)进行 /O 操作
    return E_OK;
}
相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
高可用应用架构
欢迎来到“高可用应用架构”课程,本课程是“弹性计算Clouder系列认证“中的阶段四课程。本课程重点向您阐述了云服务器ECS的高可用部署方案,包含了弹性公网IP和负载均衡的概念及操作,通过本课程的学习您将了解在平时工作中,如何利用负载均衡和多台云服务器组建高可用应用架构,并通过弹性公网IP的方式对外提供稳定的互联网接入,使得您的网站更加稳定的同时可以接受更多人访问,掌握在阿里云上构建企业级大流量网站场景的方法。 学习完本课程后,您将能够: 理解高可用架构的含义并掌握基本实现方法 理解弹性公网IP的概念、功能以及应用场景 理解负载均衡的概念、功能以及应用场景 掌握网站高并发时如何处理的基本思路 完成多台Web服务器的负载均衡,从而实现高可用、高并发流量架构
目录
相关文章
|
网络协议 调度 Python
进程小练习
进程小练习
|
4月前
|
存储 安全 Linux
进程与线程(一)进程相关
进程与线程(一)进程相关
32 1
|
6月前
|
存储 网络协议 算法
【进程与线程】最好懂的讲解
【进程与线程】最好懂的讲解
77 1
|
7月前
|
Linux API 调度
进程,任务
进程,任务
39 1
|
7月前
|
存储 Java Unix
什么是进程?
什么是进程?
71 0
|
7月前
|
Java Linux API
进程的认识
进程的认识
|
存储 算法 程序员
一定要知道的进程知识
一定要知道的进程知识
151 0
一定要知道的进程知识
|
存储 调度
关于进程的那些事
关于进程的那些事
91 0
|
NoSQL
2.1~2.5 进程
2.1~2.5 进程
135 0
2.1~2.5 进程

热门文章

最新文章

相关实验场景

更多