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

第17天

前一天写的多任务操作系统有个BUG,如果只启动了任务a,但是任务b0-2都没有启动的话,操作系统就崩溃了,因了任务a没有输入的情况下,就从任务中删除了,操作系统就会寻找下一个任务,便是找不到。所以我们根据之前的经验,找一个“哨兵”,总是在一个任务idle,在最底层,如果没有任务的话,操作系统就运行这个任务。

void task_idle(void)
{
  for (;;) {
    io_hlt();
  }
}

这个任务在主程序初始化多任务的时候就创建,也就是在task_init函数中创建。把idle任务的LEVEL设置为MAX_TASKLEVELS - 1。

创建一个控制台窗口,并作为一个独立的任务。

sht_cons = sheet_alloc(shtctl);
buf_cons = (unsigned char *) memman_alloc_4k(memman, 256 * 165);
sheet_setbuf(sht_cons, buf_cons, 256, 165, -1); 
make_window8(buf_cons, 256, 165, "console", 0);
make_textbox8(sht_cons, 8, 28, 240, 128, COL8_000000);
task_cons = task_alloc();
task_cons->tss.esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024 - 8;
task_cons->tss.eip = (int) &console_task;
task_cons->tss.es = 1 * 8;
task_cons->tss.cs = 2 * 8;
task_cons->tss.ss = 1 * 8;
task_cons->tss.ds = 1 * 8;
task_cons->tss.fs = 1 * 8;
task_cons->tss.gs = 1 * 8;
*((int *) (task_cons->tss.esp + 4)) = (int) sht_cons;
task_run(task_cons, 2, 2); /* level=2, priority=2 */

现在操作系统总共有两个窗口,第一个是task_a,第二个是console。这两个窗口都有光标闪动,主程序也能响应键盘的TAB消息,当TAB按下时判断key_to变量的值,如果为0说明task_a的标口标题也蓝色,也就是看上去操作系统的焦点在这个窗口上。如果为1则把操作系统的焦点改变为console窗口上,也就是把console窗口的标题变为蓝色,task_a变为灰色。但是当输入字符的时候,出问题了,因为console没有显示字符方面的功能,而且只有主程序能响应键盘中断,主程序是不知道console任务的消息队列地址的。下面我们就要解决这个问题。

仔细一想,其实每个任务都需要接收数据,也就是接收键盘、鼠标或者其他信号的输入,那么必然需要一个队列。那我们就把队列直接和TASK数据结构绑在一起好了。每个任务一开始先申请队列存储空间,然后调用task_now函数可以获取当前任务,也就能获取这个任务所对应的队列了。task_a中取得键盘的中断值,以前是直接显示到task_a所对应的窗口中,现在要先判断key_to的值,如果为1则往console的消息队列中写入值,而不直接显示。现在我们可以输入英文、数字和符号了,但还无法输入"!"和“%”。接下来解决这个问题。

我们建立两个keytable,keytable0和keytable1,这个两个数组英文字母都差不多,主要的差别是一些符号,比如@,!等需要按shift才能显示的符号。我们增加一个变量key_shift,当左加shift按下时为1,右边shift按下时为2,左右两边都按下时为3,都不按时为0。然后处理键盘输入的时候判断key_shift的值,如果为不为0则查每2个表的值,如果为0则查第一个表的值。

更进一步,如果要区分字母的大小写,那么就需要同时判断CapLock和Shift了。

  • CapsLock : off && shift : off -> 小写
  • CapsLock : off && shift : on -> 大写
  • CapsLock : on && shift : off -> 大写
  • CapsLock : on && shift :on -> 小写

我们已经能取得shift的值了,如何取得CapsLock的值呢?在我们进行32位模式之前,通过BIOS获取的键盘的状态值终于派上用场了。我们保存在binfo->leds中。这是一个字节变量,第4位为ScrollLock状态,第5位为NumLock状态,第6位为CapsLock状态。

当我们键盘按下CapsLock、NumLock、ScrollLock得到的扫描码分别是0x3a,0x45,ox46,我们可以在接收到这三个值时根据情况改写binfo->leds的值。

我们改写binfo->leds中的值的时候也要对键盘上的指示灯作相应的处理。处理的步骤如下:

  1. 读取状态寄存器,等待bit 1的值变为0
  2. 向数据输出0x60写入要发送的1字节数据
  3. 等待键盘返回1字节的信息
  4. 返回的信息如果为0xfa,表明1个字节的数据已成功发送,如果为0xfe则表明发送失败。

要控制键盘LED的状态,需要按上述方法执行两次,向键盘发送EDXX数据,其中XX bit 0 代表ScrollLock,bit 1代表NumLock, bit 2 代表CapsLock。0表示熄灭,1代表点亮。

第18天

今天我们先优化一个光标使之更符合我们的操作习惯。首先我们习惯如果操作系统的焦点在哪个窗口,那么哪个窗口的光标就应该闪烁,而其它窗口的光标就消息了。首先根据这个我们先修改程序。

首先做简单的,先考虑控制任务a的光标,定义一个变量cursor_c,如果为负值的话,光标就不闪烁,如果为正值,那么光标闪烁。对于任务b,我们如何让任务a传递指令,让任务b的光标闪烁或者不闪烁呢?很简单,跟传递键盘数据一样,利用消息队列。我们这么定义,如果让任务b光标闪烁,那么发送2,不闪烁那么就发送3。

现在让console任务影应回车键,当按下回车的时候让任务a向任务b发送10 + 256消息,console中已经有cursor_x变量用于保存光标横座标的值,我们再定义一个cursor_y变量用于保存光标纵座标的值。console接收到消息的时候将这一行的光标擦除,然后cursor += 16就行了。

让console任务窗口支持窗口滚动也很简单,其实就是将第2行开始上移一行,然后将最后一行画黑。

突然我们发现对于console窗口,我们已经可以输入命令了:已经支持回车,已经支持滚动。console窗口中每按下一个键盘,就保存在内存中,然后按下回车的时候,读取内存中的字符串,如果这个字符串跟我们预期中的字符串相同则执行一定的操作。如果不同,则在窗口中定入"Bad Command"字符串。

这一节我们努力实现三个命令:mem,cls,dir。

每次按下回车的时候我们用strcmp函数判断所输入的字符串是否符合我们预想的命令。比如,strcmp(cmd, "cls") == 0 则说明cmd中的字符串就是"cls"字符串。

  • cls命令:将console窗口全部重新画成黑色。
  • mem命令:先跟之前一样将任务a中的memtotal变量通过栈传遵纪守法到console任务中然后在窗口中画出来

接下来重点讲dir命令。软盘中保存文件名的地址为0x002600。文件名的保存格式如下:

struct FILEINFO {
  unsigned char name[8];//文件名,不足8字节用空格补充,文件名都是大写字母
  unsigned char ext[3];//文件后缀,扩展名
  unsigned char type;//文件类型,0x01只读文件,0x02隐藏文件,0x04系统文件,0x08非文件信息,0x10目录
  char reserve[10];//保留字节,无用处
  unsigned short time;//存放文件的时间
  unsigned short date;//存放文件的日期
  unsinged short clustno;//簇号,表示文件从哪个扇区开始存放
  unsigned int size;//文件的大小。
};

文件名的第一个字节为0xe5代表该文件已被删除,第一个字节为0x00,代表这一段文件不包含文件名信息,文件信息最多可以存放224个。程序中我们先判断文件名的首个字节,如果为0x00则说名接下来没有文件了。然后将0x00之前的FILEINFO结构体中name,ext,size三个字段的信息打印到屏幕上这个命令就完成了。

第19天

今天来实现type命令,这个命令的功能是显示文件的内容。

我们要显示文件的内容首选要知道文件所在的位置。前一天的FILEINFO结构体中有一个字段,clustno,这个字段表示文件从哪个扇区开始,那么就好解决了。

磁盘映像中的地址 = clustno * 512 + 0x003e00

程序的逻辑这个样子。首先判断前4个字符是不是"type"字符串,如果是的话再从第6个字符开始读取与磁盘中的文件名比较,如果相等说明文件存在。然后读取文件中的size字段的值,clustno字段的值。通过上面的公式可以知道所在的位置,然后循环size次,将文件起始地址的内容读画在console窗口中。

虽然勉强算完成了这个命令但是还有不足之处,首先我们目前还不支持制表符,换行符,回车符;这3个字符编码如下0x09,ox0a,ox0d。windows系统中的换行符编码是0x0d,ox0a。而linux中的换行只有0x0d。我们的策略是这样如果碰到0x0a就直接忽略,碰到0x0a就换行,碰到0x09就在当前位置到下一个制表符之间填充空格,将制表位设定在第0,4,8,12个字符这样4的倍数的位置。

解决了以上一个字符编码之后,又发现一个不足之处。我们从clustno字段的值计算出文件中的首地址,但是按照windows管理磁盘方法,保存大于512字节的文件时,有时候并不是存入连续的扇区中。

对于文件下一段存放在哪里,在磁盘中是有记录的,我们只要分析这个记录,就可以正确计算出所有内容在磁盘中的地址了。这个记录存放在位于0柱面,0磁头,2扇区开始的9个扇区中,相当于映像中的0x000200~0x0013ff中。这9个扇区的记录被称为FAT,file allocation table,也就是文件分配表。 FAT的大小为4608字节,由于FAT是很重要的数据,错误的话直接会导致软盘中的数据无法读取,所有一个软盘中一共有2个FAT,这两个FAT是一模一样的。第二份FAT 位于0x001400~0x0025ff。两个FAT是连续存放的。

一个软盘一共有2880个扇区,FAT实际就是保存扇区号,如果FAT每个扇区号用8位表示的话只能表示0255号,不够有。如果用16位表示的话,可以表示065535扇区,又太浪费。所以微软选用了一个折中的方法,用12位表示一个扇区,这就是所谓的FAT压缩算法。用12位表示的话能表示0~4095扇区,够用且不会太浪费。

FAT的解码算法如下:以三个字节为一个单位,第二个字节的低4位与第一个字节组成一个12位的数字。第二个字节的高4位与第三个字节的组成一个12位的数。其中第二字节的低4位做为12位数的高4位;第二字节的高4位做为12位数的低4位。以下是FAT的解码算法:

void file_readfat(int *fat, unsigned char *img)
{
  int i, j = 0;
  for (i = 0; i < 2880; i += 2) {
    fat[i + 0] = (img[j + 0]      | img[j + 1] << 8) & 0xfff;
    fat[i + 1] = (img[j + 1] >> 4 | img[j + 2] << 4) & 0xfff;
    j += 3;
  }
  return;

}

然后我们就可以根据解码得到的fat数组读取文件了。

void file_loadfile(int clustno, int size, char *buf, int *fat, char *img)
{
  int i;
  for (;;) {
    if (size <= 512) {
      for (i = 0; i < size; i++) {
        buf[i] = img[clustno * 512 + i];
      }
      break;
    }
    for (i = 0; i < 512; i++) {
      buf[i] = img[clustno * 512 + i];
    }
    size -= 512;
    buf += 512;
    clustno = fat[clustno];
  }
  return;
}

写这段程序的想法很简单,首先创建一个fat数组,然后把软盘中的FAT解码,解码后的数据放到fat数组中。再根据fat数组把软盘上的文件读到内存中。读完之后再显示到屏幕上。

接下来我们要写每一个应用程序了。

先写一个简单的程序,编译好后放进软盘中。

fin:
  HLT
  JMP fin

然后运行上面读取文件的方法这个HLT.HRB文件。再为以这个文件为内容空间做为程序的入口地址,设定好GDT,再jmp到这个地址,那个这个程序就运行了。

推荐阅读更多精彩内容