第17天
前一天寫的多任務(wù)操作系統(tǒng)有個(gè)BUG,如果只啟動(dòng)了任務(wù)a,但是任務(wù)b0-2都沒有啟動(dòng)的話,操作系統(tǒng)就崩潰了,因了任務(wù)a沒有輸入的情況下,就從任務(wù)中刪除了,操作系統(tǒng)就會(huì)尋找下一個(gè)任務(wù),便是找不到。所以我們根據(jù)之前的經(jīng)驗(yàn),找一個(gè)“哨兵”,總是在一個(gè)任務(wù)idle,在最底層,如果沒有任務(wù)的話,操作系統(tǒng)就運(yùn)行這個(gè)任務(wù)。
void task_idle(void)
{
for (;;) {
io_hlt();
}
}
這個(gè)任務(wù)在主程序初始化多任務(wù)的時(shí)候就創(chuàng)建,也就是在task_init函數(shù)中創(chuàng)建。把idle任務(wù)的LEVEL設(shè)置為MAX_TASKLEVELS - 1。
創(chuàng)建一個(gè)控制臺(tái)窗口,并作為一個(gè)獨(dú)立的任務(wù)。
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 */
現(xiàn)在操作系統(tǒng)總共有兩個(gè)窗口,第一個(gè)是task_a,第二個(gè)是console。這兩個(gè)窗口都有光標(biāo)閃動(dòng),主程序也能響應(yīng)鍵盤的TAB消息,當(dāng)TAB按下時(shí)判斷key_to變量的值,如果為0說明task_a的標(biāo)口標(biāo)題也藍(lán)色,也就是看上去操作系統(tǒng)的焦點(diǎn)在這個(gè)窗口上。如果為1則把操作系統(tǒng)的焦點(diǎn)改變?yōu)閏onsole窗口上,也就是把console窗口的標(biāo)題變?yōu)樗{(lán)色,task_a變?yōu)榛疑?。但是?dāng)輸入字符的時(shí)候,出問題了,因?yàn)閏onsole沒有顯示字符方面的功能,而且只有主程序能響應(yīng)鍵盤中斷,主程序是不知道console任務(wù)的消息隊(duì)列地址的。下面我們就要解決這個(gè)問題。
仔細(xì)一想,其實(shí)每個(gè)任務(wù)都需要接收數(shù)據(jù),也就是接收鍵盤、鼠標(biāo)或者其他信號(hào)的輸入,那么必然需要一個(gè)隊(duì)列。那我們就把隊(duì)列直接和TASK數(shù)據(jù)結(jié)構(gòu)綁在一起好了。每個(gè)任務(wù)一開始先申請(qǐng)隊(duì)列存儲(chǔ)空間,然后調(diào)用task_now函數(shù)可以獲取當(dāng)前任務(wù),也就能獲取這個(gè)任務(wù)所對(duì)應(yīng)的隊(duì)列了。task_a中取得鍵盤的中斷值,以前是直接顯示到task_a所對(duì)應(yīng)的窗口中,現(xiàn)在要先判斷key_to的值,如果為1則往console的消息隊(duì)列中寫入值,而不直接顯示?,F(xiàn)在我們可以輸入英文、數(shù)字和符號(hào)了,但還無法輸入"!"和“%”。接下來解決這個(gè)問題。
我們建立兩個(gè)keytable,keytable0和keytable1,這個(gè)兩個(gè)數(shù)組英文字母都差不多,主要的差別是一些符號(hào),比如@,!等需要按shift才能顯示的符號(hào)。我們增加一個(gè)變量key_shift,當(dāng)左加shift按下時(shí)為1,右邊shift按下時(shí)為2,左右兩邊都按下時(shí)為3,都不按時(shí)為0。然后處理鍵盤輸入的時(shí)候判斷key_shift的值,如果為不為0則查每2個(gè)表的值,如果為0則查第一個(gè)表的值。
更進(jìn)一步,如果要區(qū)分字母的大小寫,那么就需要同時(shí)判斷CapLock和Shift了。
- CapsLock : off && shift : off -> 小寫
- CapsLock : off && shift : on -> 大寫
- CapsLock : on && shift : off -> 大寫
- CapsLock : on && shift :on -> 小寫
我們已經(jīng)能取得shift的值了,如何取得CapsLock的值呢?在我們進(jìn)行32位模式之前,通過BIOS獲取的鍵盤的狀態(tài)值終于派上用場了。我們保存在binfo->leds中。這是一個(gè)字節(jié)變量,第4位為ScrollLock狀態(tài),第5位為NumLock狀態(tài),第6位為CapsLock狀態(tài)。
當(dāng)我們鍵盤按下CapsLock、NumLock、ScrollLock得到的掃描碼分別是0x3a,0x45,ox46,我們可以在接收到這三個(gè)值時(shí)根據(jù)情況改寫binfo->leds的值。
我們改寫binfo->leds中的值的時(shí)候也要對(duì)鍵盤上的指示燈作相應(yīng)的處理。處理的步驟如下:
- 讀取狀態(tài)寄存器,等待bit 1的值變?yōu)?
- 向數(shù)據(jù)輸出0x60寫入要發(fā)送的1字節(jié)數(shù)據(jù)
- 等待鍵盤返回1字節(jié)的信息
- 返回的信息如果為0xfa,表明1個(gè)字節(jié)的數(shù)據(jù)已成功發(fā)送,如果為0xfe則表明發(fā)送失敗。
要控制鍵盤LED的狀態(tài),需要按上述方法執(zhí)行兩次,向鍵盤發(fā)送EDXX數(shù)據(jù),其中XX bit 0 代表ScrollLock,bit 1代表NumLock, bit 2 代表CapsLock。0表示熄滅,1代表點(diǎn)亮。
第18天
今天我們先優(yōu)化一個(gè)光標(biāo)使之更符合我們的操作習(xí)慣。首先我們習(xí)慣如果操作系統(tǒng)的焦點(diǎn)在哪個(gè)窗口,那么哪個(gè)窗口的光標(biāo)就應(yīng)該閃爍,而其它窗口的光標(biāo)就消息了。首先根據(jù)這個(gè)我們先修改程序。
首先做簡單的,先考慮控制任務(wù)a的光標(biāo),定義一個(gè)變量cursor_c,如果為負(fù)值的話,光標(biāo)就不閃爍,如果為正值,那么光標(biāo)閃爍。對(duì)于任務(wù)b,我們?nèi)绾巫屓蝿?wù)a傳遞指令,讓任務(wù)b的光標(biāo)閃爍或者不閃爍呢?很簡單,跟傳遞鍵盤數(shù)據(jù)一樣,利用消息隊(duì)列。我們這么定義,如果讓任務(wù)b光標(biāo)閃爍,那么發(fā)送2,不閃爍那么就發(fā)送3。
現(xiàn)在讓console任務(wù)影應(yīng)回車鍵,當(dāng)按下回車的時(shí)候讓任務(wù)a向任務(wù)b發(fā)送10 + 256消息,console中已經(jīng)有cursor_x變量用于保存光標(biāo)橫座標(biāo)的值,我們再定義一個(gè)cursor_y變量用于保存光標(biāo)縱座標(biāo)的值。console接收到消息的時(shí)候?qū)⑦@一行的光標(biāo)擦除,然后cursor += 16就行了。
讓console任務(wù)窗口支持窗口滾動(dòng)也很簡單,其實(shí)就是將第2行開始上移一行,然后將最后一行畫黑。
突然我們發(fā)現(xiàn)對(duì)于console窗口,我們已經(jīng)可以輸入命令了:已經(jīng)支持回車,已經(jīng)支持滾動(dòng)。console窗口中每按下一個(gè)鍵盤,就保存在內(nèi)存中,然后按下回車的時(shí)候,讀取內(nèi)存中的字符串,如果這個(gè)字符串跟我們預(yù)期中的字符串相同則執(zhí)行一定的操作。如果不同,則在窗口中定入"Bad Command"字符串。
這一節(jié)我們努力實(shí)現(xiàn)三個(gè)命令:mem,cls,dir。
每次按下回車的時(shí)候我們用strcmp函數(shù)判斷所輸入的字符串是否符合我們預(yù)想的命令。比如,strcmp(cmd, "cls") == 0 則說明cmd中的字符串就是"cls"字符串。
- cls命令:將console窗口全部重新畫成黑色。
- mem命令:先跟之前一樣將任務(wù)a中的memtotal變量通過棧傳遵紀(jì)守法到console任務(wù)中然后在窗口中畫出來
接下來重點(diǎn)講dir命令。軟盤中保存文件名的地址為0x002600。文件名的保存格式如下:
struct FILEINFO {
unsigned char name[8];//文件名,不足8字節(jié)用空格補(bǔ)充,文件名都是大寫字母
unsigned char ext[3];//文件后綴,擴(kuò)展名
unsigned char type;//文件類型,0x01只讀文件,0x02隱藏文件,0x04系統(tǒng)文件,0x08非文件信息,0x10目錄
char reserve[10];//保留字節(jié),無用處
unsigned short time;//存放文件的時(shí)間
unsigned short date;//存放文件的日期
unsinged short clustno;//簇號(hào),表示文件從哪個(gè)扇區(qū)開始存放
unsigned int size;//文件的大小。
};
文件名的第一個(gè)字節(jié)為0xe5代表該文件已被刪除,第一個(gè)字節(jié)為0x00,代表這一段文件不包含文件名信息,文件信息最多可以存放224個(gè)。程序中我們先判斷文件名的首個(gè)字節(jié),如果為0x00則說名接下來沒有文件了。然后將0x00之前的FILEINFO結(jié)構(gòu)體中name,ext,size三個(gè)字段的信息打印到屏幕上這個(gè)命令就完成了。
第19天
今天來實(shí)現(xiàn)type命令,這個(gè)命令的功能是顯示文件的內(nèi)容。
我們要顯示文件的內(nèi)容首選要知道文件所在的位置。前一天的FILEINFO結(jié)構(gòu)體中有一個(gè)字段,clustno,這個(gè)字段表示文件從哪個(gè)扇區(qū)開始,那么就好解決了。
磁盤映像中的地址 = clustno * 512 + 0x003e00
程序的邏輯這個(gè)樣子。首先判斷前4個(gè)字符是不是"type"字符串,如果是的話再從第6個(gè)字符開始讀取與磁盤中的文件名比較,如果相等說明文件存在。然后讀取文件中的size字段的值,clustno字段的值。通過上面的公式可以知道所在的位置,然后循環(huán)size次,將文件起始地址的內(nèi)容讀畫在console窗口中。
雖然勉強(qiáng)算完成了這個(gè)命令但是還有不足之處,首先我們目前還不支持制表符,換行符,回車符;這3個(gè)字符編碼如下0x09,ox0a,ox0d。windows系統(tǒng)中的換行符編碼是0x0d,ox0a。而linux中的換行只有0x0d。我們的策略是這樣如果碰到0x0a就直接忽略,碰到0x0a就換行,碰到0x09就在當(dāng)前位置到下一個(gè)制表符之間填充空格,將制表位設(shè)定在第0,4,8,12個(gè)字符這樣4的倍數(shù)的位置。
解決了以上一個(gè)字符編碼之后,又發(fā)現(xiàn)一個(gè)不足之處。我們從clustno字段的值計(jì)算出文件中的首地址,但是按照windows管理磁盤方法,保存大于512字節(jié)的文件時(shí),有時(shí)候并不是存入連續(xù)的扇區(qū)中。
對(duì)于文件下一段存放在哪里,在磁盤中是有記錄的,我們只要分析這個(gè)記錄,就可以正確計(jì)算出所有內(nèi)容在磁盤中的地址了。這個(gè)記錄存放在位于0柱面,0磁頭,2扇區(qū)開始的9個(gè)扇區(qū)中,相當(dāng)于映像中的0x000200~0x0013ff中。這9個(gè)扇區(qū)的記錄被稱為FAT,file allocation table,也就是文件分配表。 FAT的大小為4608字節(jié),由于FAT是很重要的數(shù)據(jù),錯(cuò)誤的話直接會(huì)導(dǎo)致軟盤中的數(shù)據(jù)無法讀取,所有一個(gè)軟盤中一共有2個(gè)FAT,這兩個(gè)FAT是一模一樣的。第二份FAT 位于0x001400~0x0025ff。兩個(gè)FAT是連續(xù)存放的。
一個(gè)軟盤一共有2880個(gè)扇區(qū),F(xiàn)AT實(shí)際就是保存扇區(qū)號(hào),如果FAT每個(gè)扇區(qū)號(hào)用8位表示的話只能表示0255號(hào),不夠有。如果用16位表示的話,可以表示065535扇區(qū),又太浪費(fèi)。所以微軟選用了一個(gè)折中的方法,用12位表示一個(gè)扇區(qū),這就是所謂的FAT壓縮算法。用12位表示的話能表示0~4095扇區(qū),夠用且不會(huì)太浪費(fèi)。
FAT的解碼算法如下:以三個(gè)字節(jié)為一個(gè)單位,第二個(gè)字節(jié)的低4位與第一個(gè)字節(jié)組成一個(gè)12位的數(shù)字。第二個(gè)字節(jié)的高4位與第三個(gè)字節(jié)的組成一個(gè)12位的數(shù)。其中第二字節(jié)的低4位做為12位數(shù)的高4位;第二字節(jié)的高4位做為12位數(shù)的低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;
}
然后我們就可以根據(jù)解碼得到的fat數(shù)組讀取文件了。
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;
}
寫這段程序的想法很簡單,首先創(chuàng)建一個(gè)fat數(shù)組,然后把軟盤中的FAT解碼,解碼后的數(shù)據(jù)放到fat數(shù)組中。再根據(jù)fat數(shù)組把軟盤上的文件讀到內(nèi)存中。讀完之后再顯示到屏幕上。
接下來我們要寫每一個(gè)應(yīng)用程序了。
先寫一個(gè)簡單的程序,編譯好后放進(jìn)軟盤中。
fin:
HLT
JMP fin
然后運(yùn)行上面讀取文件的方法這個(gè)HLT.HRB文件。再為以這個(gè)文件為內(nèi)容空間做為程序的入口地址,設(shè)定好GDT,再jmp到這個(gè)地址,那個(gè)這個(gè)程序就運(yùn)行了。