系统相关
首页 > 系统相关> > [自制操作系统] 第18回 实现用户进程(上)

[自制操作系统] 第18回 实现用户进程(上)

作者:互联网

一、前景回顾

  在上一回我们已经实现了键盘的驱动编写和环形缓冲区的实现,现在让我们来想这么一个问题:

  一直以来我们的程序都在最高特权级0下工作,这意味着任何程序都和操作系统平起平坐,可以改动任何资源。如果不改变这种现状的话,某个不听话的程序甚至可以给操作系统致命一击,取而代之,那么后果将不堪设想。所以从本回开始,我们便要开始着手实现用户进程,让我们的操作系统看起来更安全一点。

二、任务切换相关

  下面的是我自己的一些见解。

  如果让我来设计任务切换,比较简单的一种思路便是:

  首先我们常说的任务,就是一个程序而已,程序在内存中被分为代码段和数据段。所以我们表征多个任务,那么便是多个代码段和数据段而已。至于任务的切换,可能需要费点心思在软件层面上实现多任务调度机制。

  然后现在问题出现了:

  我们知道代码段和数据段需要在全局描述符表GDT中存储,一个任务需要两个描述符来存储,而我们知道全局描述符表GDT最多也就只有2^13=8192个段描述符,那么理论上也就只能容纳4096个任务,除此之外在软件层面上实现的多任务调度机制有点类似今天的用户态多线程,效率不高且安全性有诸多问题。

  所以我们来看看硬件厂商和CPU厂商是如何解决任务切换的问题的,其中最主要的就是LDTTSS

  首先是LDT。

  LDT是局部描述符表,用来存储每个任务自己的私有实体资源,也就是代码和数据。LDT的地址被保存在一个段描述符中,那么理论上我们现在可以支持8192个任务了。对于当前运行的任务,其LDT的地址被存储在LDTR寄存器中,这样CPU就能根据这个地址从中拿到任务所需要的资源。每切换一个任务时,需要用lldt指令重新加载新任务的LDT地址到LDTR寄存器中。
        
  随后便是TSS。

  单核CPU要想实现多任务,唯一的方法便是多任务共享一个CPU,也就是让多个任务轮流使用CPU。前面说道,LDT是每个任务的私有资源,所以不用担心多任务时,程序的运行资源会混乱。但这还不够。

  CPU执行任务时,需要把任务所需要的数据加载到寄存器、栈和内存中,因为CPU只能直接处理这些资源中的数据,这是CPU在设计之初时工程师们决定的。于是,问题来了,任务的数据和指令是CPU的处理对象,他们被存放在内存这个低速的容器中,对于CPU来讲,内存的速度太慢了,它最喜欢寄存器。因此内存中的数据往往被加载到高速的寄存器中后再处理,等处理完毕后再将结果写入到内存中,所以,任何时候,寄存器中的内容才是任务的最新状态。当任务被换下CPU后,任务的最新状态应该被保存在某个地方,以便下次重新将此任务调度到CPU时可以恢复此任务的最新状态,这样任务才能继续执行。

  于是TSS就出现了,TSS是程序员为任务单独定义的一个结构体变量,当加载新任务时,CPU自动把当前任务(旧任务)的状态存入当前任务的TSS,然后将新任务TSS中的数据载入到对应的寄存器中。  
                               
  TSS和其他段也是一样的,本质上是一片存储数据的内存区域,也需要某个描述符结构来描述它,这就是TSS描述符。
        
  和LDT一样,CPU对TSS的处理也采用了类似的方法,提供一个名为TR的寄存器来存放当前任务的TSS位置。
                   
  CPU原生支持的任务切换方式是针对每一个任务都有一个LDT和一个TSS结构,这种任务切换方式,在任务切换时效率比较低,所以现代操作系统并未采纳。现代操作系统放弃了LDT,只采用了TSS,但是也没有完全采纳。我们是效仿Linux的任务切换方式的,所以拿Linux为例。

  Linux为每一个CPU创建一个TSS,在各个CPU上的所有任务共享一个TSS,各CPU的TR寄存器保存各CPU上的TSS,也就是说在用ltr指令加载TSS后,该TR寄存器永远指向同一个TSS,之后再也不会切换了。在进程切换时只需要把TSS中的SS0和esp0更新为新任务的内核栈的段地址和栈指针。

  那么任务的状态信息保存在哪里呢?

  对于Linux来讲,Linux只在TSS中初始化esp0和SS0以及IO位图。当CPU从低特权级进入高特权级时,也就是3特权级的用户态到0特权级的内核态时(Linux只有两个特权级)CPU会自动从TSS中获取到0特权级的栈指针,然后Linux手动执行一系列的push指令将任务的状态保存在0特权级的栈中。这个地方先留一下悬念,等后面实现的时候会再次提到。

标签:操作系统,进程,编写,任务切换,数据,系统,寄存器
来源: