三十天自制操作系統(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到這個地址,那個這個程序就運行了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容