三十天自制操作系统(5)

第13天

在这之前的程序中每次新设置一个定时器都要创造一个队列与之对应,这样效率低而且导致操作系统主程序中逻辑复杂。我们先想办法把定时器的消息队列合并。怎么合并呢?首先想一下我们为什么之前要把定时器的消息队列分开设置,为每个定时器分配一个队列呢。主要是因为每个定时器超时所对应的操作不一样,因此为了区分不同定时器超时操作,所以才把队列分开。那如果我们给每个定时器往队列中放入数据的值都不同,再每个定时器超时的时候先判断队列中的数据属于哪个计时器,我们就可以分别进行不同的操作了,也达到了之前的目前。而且代码肯定更简单了,起码不用作那么多判断了。

接下来还要做很多计时器的优化工作,但是我们现在还没有考虑怎么样测试优化的结果。我们可以这样,在计时器3秒中断的时候把count设置为0,一直全力计数,然后在10秒中断的时候停止计数,并把结果显示在窗口中。如果数字越大说明系统执行速度越快,性能也就越好。

我们之前区分定时器中断时候,是用向消息队列中发送数据的不同来区分的。那能不能我们把鼠标和键盘使用的消息队列也跟定时器合在一起,也用向消息队列中传送的数据来区分呢?下面我们定义一下中断类型。

  • 0~1 光标闪烁定时器
  • 3 3秒定时器
  • 10 10秒定时器
  • 256~511 键盘输入(从键盘控制器读入的值再加上256)
  • 512~767 鼠标输入(从键盘控制器读入的值再加上 512)

我们之前的队列定的使用的数据类型为char只有8位,现在我们将其改为int类型。其他基本没有什么变化。只是在队列中读入键盘和鼠标灵数据的时候要分别减去256和512。

在处理定时器中断的时候主要的时间开销是找下一个超时定时器之后的移位操作,我们就是要想办法取消移位操作。

我们在TIMER结构体中新定义一个TIMER指针,指向下一个即将超时的定时器。

struct TIMER {
  struct TIMER *next;
  unsigned int timeout, flags;
  struct FIFO32 *fifo;
  int data;
};

其实就是把线性表改造成链表操作,中断处理程序这么改写

void inthandler20(int *esp)
{
  int i;
  struct TIMER *timer;
  io_out8(PIC0_OCW2, 0x60);
  timerctl.count++;
  if (timerctl.next > timerctl.count) {
    return;
  }
  timer = timerctl.t0; //首先把最前面的地址赋给timer
  for (i = 0; i < timerctl.using; i++) {
    //因为timers的定时器都处于运行状态,所以不确认flags
     if (timer->timeout > timerctl.count) {
      break;
    }
    //超时
    timer->flags = TIMER_FLAGS_ALLOC;
    fifo32_put(timer->fifo, timer->data);
    timer = timer->next; //下一个定时器的地址赋给timer
  }
  timerctl.using -= i;
/*新移位 */
  timerctl.t0 = timer;
  /* timerctl.next的设定*/
  if (timerctl.using > 0) {
    timerctl.next = timerctl.t0->timeout;
  } else {
    timerctl.next = 0xffffffff;
  }
  return;
}

定时器设置函数

 void timer_settime(struct TIMER *timer, unsigned int timeout)
{
  int e;
  struct TIMER *t, *s;
  timer->timeout = timeout + timerctl.count;
  timer->flags = TIMER_FLAGS_USING;
  e = io_load_eflags();
  io_cli();
  timerctl.using++;
  if (timerctl.using == 1) {
    timerctl.t0 = timer;
    timer->next = 0; 
    timerctl.next = timer->timeout;
    io_store_eflags(e);
    return;
  }
  t = timerctl.t0;
  if (timer->timeout <= t->timeout) {
    timerctl.t0 = timer;
    timer->next = t;
    timerctl.next = timer->timeout;
    io_store_eflags(e);
    return;
  }
  for (;;) {
    s = t;
    t = t->next;
    if (t == 0) {
      break; 
    }
    if (timer->timeout <= t->timeout) {
      s->next = timer; 
      timer->next = t; 
      io_store_eflags(e);
      return;
    }
  }
  s->next = timer;
  timer->next = 0;
  io_store_eflags(e);
  return;
}

虽然程序变长了,但是由于引入了链表的概念,不再需要做移位操作,在定时器多的情况下,效率绝对比线性表慢慢移位要快很多。

分析一下上面程序变长的原因。主要是插入链表的时候产生了4种情况:1、运行中的定时器只有一个;2、插入到最前面的情况;3、插入到中间的情况;4、插入到最后的情况。

为了减少插边链表时候所考虑的情况,我们引入了哨兵的概念。也就是设置一个永无存在且总是在最后到期的定时器。有了这个定时器之后,插入链表的时候就只有2种情况了,插入到最前面的情况和插入到中间的情况。

修改之后的定时器设置函数

void timer_settime(struct TIMER *timer, unsigned int timeout)
{
  int e;
  struct TIMER *t, *s;
  timer->timeout = timeout + timerctl.count;
  timer->flags = TIMER_FLAGS_USING;
  e = io_load_eflags();
  io_cli();
  t = timerctl.t0;
  if (timer->timeout <= t->timeout) {
    timerctl.t0 = timer;
    timer->next = t; 
    timerctl.next = timer->timeout;
    io_store_eflags(e);
    return;
  }
  for (;;) {
    s = t;
    t = t->next;
    if (timer->timeout <= t->timeout) {
      s->next = timer; 
      timer->next = t; 
      io_store_eflags(e);
      return;
    }
  }
}

可以看出简化了不少。简化后的中断处理函数

void inthandler20(int *esp)
{
  struct TIMER *timer;
  io_out8(PIC0_OCW2, 0x60); 
  timerctl.count++;
  if (timerctl.next > timerctl.count) {
    return;
  }
  timer = timerctl.t0; 
  for (;;) {
    if (timer->timeout > timerctl.count) {
      break;
    }
    timer->flags = TIMER_FLAGS_ALLOC;
    fifo32_put(timer->fifo, timer->data);
    timer = timer->next; 
  }
  timerctl.t0 = timer;
  timerctl.next = timer->timeout;
  return;
}

第14天

目前我们的操作系统使用的分辨率为320*200,我们可以想办法把分辨率提高上去。以前设置分辨率的时候是用ah = 0; al = 画面模式;设置的。更大一点的画面模试叫作VBE。在很早时候电脑规格是由IBM公司制定的 ,当然也规定了显卡画面模式,各家显卡公司就以IBM的标准制作显卡。但是后来显卡公司的技术力量超过了IBM,原来的显卡标准已经不适用了,各家显卡公司就制定了自己的标准。为了让操作系统和应用程序能使用各家公司的显卡,各家显卡公司联合起来成立了VESA(Video Electronics Standards Association),也就是视频电子标准协会。这个协会制定了显示通用的设定方法,也制作了专门的BIOS。这个BIOS被称作VESA BIOS extension,简称为VBE。切换到VBE使用ax = 0x4f02; bx = 画面模式。

  • 0x101 6404808位彩色
  • 0x103 8006008位彩色
  • 0x105 10247688位彩色
  • 0x107 128010248位彩色

想要提高分辨率要先查询一下机器支持不支持VBE的显示模式。

MOV     AX,0x9000
MOV     ES,AX
MOV     DI,0
MOV     AX,0x4f00
INT     0x10
CMP     AX,0x004f
JNE     scrn320

先把es赋值为0x9000,di赋值为0,ax赋值为0x4f00,然后int 0x10。如果ax变为0x004f的话就说明有VBE不是这个值的话就只能使用320*200的分辨率了。

接下来检查VBE的版本是不是2.0以上,如果不是2.0以上那也不能使用高分辨率。

MOV     AX,[ES:DI+4]
CMP     AX,0x0200
JB      scrn320 

然后再检查0x105的画面模式能不能使用

MOV     CX,VBEMODE
MOV     AX,0x4f01
INT     0x10
CMP     AX,0x004f
JNE     scrn320

好,如果证明0x105可以使用了,我们再确认0x105的画面信息,重要的信息有6个。

  • word[es:di+0x00] 模式属性,bit7不是1就不好办(能加上0x4000)
  • word[es:di+0x12] X的分辨 率
  • word[es:di+0x14] Y的分辨率
  • byte[es:di+0x19] 颜色数,必须为8
  • byte[es:di+0x1b] 颜色的指定方法,必须为4,调色板模式
  • word[es:di+0x28] VRAM的地址

上面的6个重要属性我们只要确定3个,都是正确的之后,就可以把这些信息定入指定的内存了,然后跳过scrn320程序段直接进入高级画面模式了。

接下来做键盘输入的处理。我们已经可以从键盘中断处理程序中取到键盘控制寄存器中的值了。这个值我具体对应哪个按键被按下或者松开有一个对应的表格。其中按键松开时的值为按键按下时值加上0x80。可以创建一个数组,从0开始按照这个表格把键盘的扫描码换成字符的ASCII码。之前我们已经用ASCII码创建过字体文件,然后根据获取的ASCII码在屏幕上显示出来。

如果我们把光标按照键盘输入情况左右移动也很简单。首先定义一个cursor_x变量,用于存储光标的位置。一开始鼠标靠近窗口的最左边。然后判断键盘输入的按键是否是需要显示的按键,如果需要显示,那么在cursor_x位置开写入对应的字符,然后cursor_x+8,然后在新的cursor_x位置重新画出光标。

这本书还实现了按住鼠标拖动实现鼠标跟着鼠标指针动。但是跟我们平时windows下移动鼠标的不同,这里只是简单得实现。处理鼠标循环的部分中,先判断鼠标左键是不是已经按下,如果按下,那把窗口马上移动到鼠标所在的位置。

第15天

这一天想办法实现多任务。所谓的多任务就是CPU在快速得切换各个任务,使电脑使用者感觉CPU在同时处理不同的任务。切换任务的速度不能太快也不能太慢。因为切换任务也是有成本的,如果太快,切换任伤的开销就太大,如果太慢,还不如不要多作务,因为把应太慢。

当CPU处理任务切换时,会先把寄存器中的值全部写入内存,然后把运行另一个任务所需要的CPU寄存器的值从内存中读取出来,这样就完成了一次切换。这写入内存和读取内存所需要的时间就一任务切换所需要的开销。
寄存器写入内存的数据结构叫做“任务状态段”(task status segment)。

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;
};

TSS32结构体中共有26个int变量,合计104字节第一行保存的不是寄存器数据,而是与任务育设置 相关的其它数据。第二行是32位寄存器。第二行是16位寄存器,但是我们还是用32位内存空间存储它。第4行也是与任务有关的其它设置。我们暂时将ldtr设置为0,将iomap设置为0x40000000。

要进行任务切换要用jmp指令。jmp指令分两种,第一种只改写EIP也就是所谓的near模式;第二种改写cs和eip,就是所谓的far模式。如果一条jmp指令的目标地址段不是可执行的代码,而是tss的话,cpu就不会执行通常的改写cs和eip,而是将这条指令理解为任务切换。CPU中还有一个TR寄存器,task register,它的作用是让CPU记住当前正在运行哪一个任务,我们给TR赋值的时候,必须把GDT编号乘以8。给TR赋值不能用MOV指令,有一个专门的指令:LTR。下面我们看一下tss结构体如何赋值:

task_b_esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024;
tss_b.eip = (int) &task_b_main;
tss_b.eflags = 0x00000202; /* IF = 1; */
tss_b.eax = 0;
tss_b.ecx = 0;
tss_b.edx = 0;
tss_b.ebx = 0;
tss_b.esp = task_b_esp;
tss_b.ebp = 0;
tss_b.esi = 0;
tss_b.edi = 0;
tss_b.es = 1 * 8;
tss_b.cs = 2 * 8;
tss_b.ss = 1 * 8;
tss_b.ds = 1 * 8;
tss_b.fs = 1 * 8;
tss_b.gs = 1 * 8;

我们先从后6个段寄存器赋值开始看,我们给cs赋值为GDT2号,其他是GDT1号,和bootpack.c使用了相同的地址段。然后是eip,我们把task_b_main的函数地址赋给它。然后是esp,也就是栈地址,我们新申请了64K内存空间,给taskB。如果和原来的主函数使用同样的栈那些切换任务的时候肯定会出问题。

我们先这样尝试多任务:在主函数定时器10秒超时的时候切换到任务b,然后在任务b的定时候5秒超时的时候切换回任务a。

void task_b_main(void)
{
  struct FIFO32 fifo;
  struct TIMER *timer;
  int i, fifobuf[128];
  fifo32_init(&fifo, 128, fifobuf);
  timer = timer_alloc();
  timer_init(timer, &fifo, 1);
  timer_settime(timer, 500);
  for (;;) {
    io_cli();
    if (fifo32_status(&fifo) == 0) {
      io_sti();
      io_hlt();
    } else {
      i = fifo32_get(&fifo);
      io_sti();
      if (i == 1) { 
        taskswitch3(); 
      }
    }
  }
}

已经实现了两个任务之间的跳转。接下来我们实现A,B两个任务每过0.02秒就转换一次。先设置一个定时器time_ts变量,超时的时间为0.02秒,如果超时向队列中发送0x2。任务A可以接受鼠标和键盘输入的,我们很容易确认任务A是否在运行,问题就出在任务B我们如何确定任务B也能正常运行呢?在任务B中设置一个计数器,每0.01秒在屏幕上写出计数器的数值就可以了。
但是碰到一个问题,如何才能让任务B知道sht_back,只有让任务B知道这个地址才能在桌面上显示数字。我们使用栈来传递数据。首先要知道C语言的函数传递参数就是用栈,比如C语言的函数void setA(int a);这个函数被调用后,函数会从esp+4的内存中取出a这个数值。我们这里就使用C语言的这个特性。在跳到任务B的时候传递一个参数,任务B的函数声明为 void task_b_main(struct SHEET *sht_back);这样运行任务B的时候可以直接使用sht_back这个变量。我们先将任务B的esp减去8,然后把esp+4内存地址的值赋为sht_back,这样就可以了,任务B函数在使用sht_back变量值的时候就是esp+4。

目前我们都是在任务中直接写切换任务的程序段,如果要真正实现多任务最好是写一段程序调动任务之间的切换,而不是证任务自己切换。

struct TIMER *mt_timer;
int mt_tr;

void mt_init(void)
{
  mt_timer = timer_alloc();
  timer_settime(mt_timer, 2);
  mt_tr = 3 * 8;
  return;
}

void mt_taskswitch(void)
{
  if (mt_tr == 3 * 8) {
    mt_tr = 4 * 8;
  } else {
    mt_tr = 3 * 8;
  }
  timer_settime(mt_timer, 2);
  farjmp(0, mt_tr);
  return;
 }

以上是实现任务切换的函数,这里设置了一个0.02秒的计时器,然后在计时器中断里如果出现这个中断时调用mt_taskwitch这个函数就可以了,两个任务的程序中就不需要自己写切换任务的程序了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容