按下鍵盤后為什么屏幕上就會(huì)有輸出
書接上回,上回書咱們說到,繼內(nèi)存管理結(jié)構(gòu) mem_map 和中斷描述符表 idt 建立好之后,我們又在內(nèi)存中倒騰出一個(gè)新的數(shù)據(jù)結(jié)構(gòu) request。
并且把它們都放在了一個(gè)數(shù)組中。
這是塊設(shè)備驅(qū)動(dòng)程序與內(nèi)存緩沖區(qū)的橋梁,通過它可以完整地表示一個(gè)塊設(shè)備讀寫操作要做的事。
我們繼續(xù)往下看,tty_init。
- void main(void) {
- ...
- mem_init(main_memory_start,memory_end);
- trap_init();
- blk_dev_init();
- chr_dev_init();
- tty_init();
- time_init();
- sched_init();
- buffer_init(buffer_memory_end);
- hd_init();
- floppy_init();
- sti();
- move_to_user_mode();
- if (!fork()) {init();}
- for(;;) pause();
- }
這個(gè)方法執(zhí)行完成之后,我們將會(huì)具備鍵盤輸入到顯示器輸出字符這個(gè)最常用的功能。
打開這個(gè)函數(shù)后我有點(diǎn)慌。
- void tty_init(void)
- {
- rs_init();
- con_init();
- }
看來這個(gè)方法已經(jīng)多到需要拆成兩個(gè)子方法了。
打開第一個(gè)方法,還好。
- void rs_init(void)
- {
- set_intr_gate(0x24,rs1_interrupt);
- set_intr_gate(0x23,rs2_interrupt);
- init(tty_table[1].read_q.data);
- init(tty_table[2].read_q.data);
- outb(inb_p(0x21)&0xE7,0x21);
- }
這個(gè)方法是串口中斷的開啟,以及設(shè)置對(duì)應(yīng)的中斷處理程序,串口在我們現(xiàn)在的 PC 機(jī)上已經(jīng)很少用到了,所以這個(gè)直接忽略,要講我也不懂。
看第二個(gè)方法,這是重點(diǎn)。代碼非常長(zhǎng),有點(diǎn)嚇人,我先把大體框架寫出。
- void con_init(void) {
- ...
- if (ORIG_VIDEO_MODE == 7) {
- ...
- if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10) {...}
- else {...}
- } else {
- ...
- if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10) {...}
- else {...}
- }
- ...
- }
可以看出,非常多的 if else。
這是為了應(yīng)對(duì)不同的顯示模式,來分配不同的變量值,那如果我們僅僅找出一個(gè)顯示模式,這些分支就可以只看一個(gè)了。 啥是顯示模式呢?那我們得簡(jiǎn)單說說顯示,一個(gè)字符是如何顯示在屏幕上的呢?換句話說,如果你可以隨意操作內(nèi)存和 CPU 等設(shè)備,你如何操作才能使得你的顯示器上,顯示一個(gè)字符‘a’呢?
我們先看一張圖。
內(nèi)存中有這樣一部分區(qū)域,是和顯存映射的。啥意思,就是你往上圖的這些內(nèi)存區(qū)域中寫數(shù)據(jù),相當(dāng)于寫在了顯存中。而往顯存中寫數(shù)據(jù),就相當(dāng)于在屏幕上輸出文本了。
沒錯(cuò),就是這么簡(jiǎn)單。 如果我們寫這一行匯編語(yǔ)句。
- mov [0xB8000],'h'
后面那個(gè) h 相當(dāng)于匯編編輯器幫我們轉(zhuǎn)換成 ASCII 碼的二進(jìn)制數(shù)值,當(dāng)然我們也可以直接寫。
- mov [0xB8000],0x68
其實(shí)就是往內(nèi)存中 0xB8000 這個(gè)位置寫了一個(gè)值,只要一寫,屏幕上就會(huì)是這樣。
簡(jiǎn)單吧,具體說來,這片內(nèi)存是每?jī)蓚€(gè)字節(jié)表示一個(gè)顯示在屏幕上的字符,第一個(gè)是字符的編碼,第二個(gè)是字符的顏色,那我們先不管顏色,如果多寫幾個(gè)字符就像這樣。
- mov [0xB8000],'h'
- mov [0xB8002],'e'
- mov [0xB8004],'l'
- mov [0xB8006],'l'
- mov [0xB8008],'o'
此時(shí)屏幕上就會(huì)是這樣。
是不是賊簡(jiǎn)單?那我們回過頭看剛剛的代碼,我們就假設(shè)顯示模式是我們現(xiàn)在的這種文本模式,那條件分支就可以去掉好多。 代碼可以簡(jiǎn)化成這個(gè)樣子。
- #define ORIG_X (*(unsigned char *)0x90000)
- #define ORIG_Y (*(unsigned char *)0x90001)
- void con_init(void) {
- register unsigned char a;
- // 第一部分 獲取顯示模式相關(guān)信息
- video_num_columns = (((*(unsigned short *)0x90006) & 0xff00) >> 8);
- video_size_row = video_num_columns * 2;
- video_num_lines = 25;
- video_page = (*(unsigned short *)0x90004);
- video_erase_char = 0x0720;
- // 第二部分 顯存映射的內(nèi)存區(qū)域
- video_mem_start = 0xb8000;
- video_port_reg = 0x3d4;
- video_port_val = 0x3d5;
- video_mem_end = 0xba000;
- // 第三部分 滾動(dòng)屏幕操作時(shí)的信息
- origin = video_mem_start;
- scr_end = video_mem_start + video_num_lines * video_size_row;
- top = 0;
- bottom = video_num_lines;
- // 第四部分 定位光標(biāo)并開啟鍵盤中斷
- gotoxy(ORIG_X, ORIG_Y);
- set_trap_gate(0x21,&keyboard_interrupt);
- outb_p(inb_p(0x21)&0xfd,0x21);
- a=inb_p(0x61);
- outb_p(a|0x80,0x61);
- outb(a,0x61);
- }
別看這么多,一點(diǎn)都不難。
首先還記不記得之前匯編語(yǔ)言的時(shí)候做的工作,存了好多以后要用的數(shù)據(jù)在內(nèi)存中。
內(nèi)存地址 | 長(zhǎng)度(字節(jié)) | 名稱 |
---|---|---|
0x90000 | 2 | 光標(biāo)位置 |
0x90002 | 2 |
擴(kuò)展內(nèi)存數(shù) |
0x90004 | 2 | 顯示頁(yè)面 |
0x90006 | 1 |
顯示模式 |
0x90007 | 1 | 字符列數(shù) |
0x90008 | 2 | 未知 |
0x9000A | 1 |
顯示內(nèi)存 |
0x9000B | 1 |
顯示狀態(tài) |
0x9000C | 2 | 顯卡特性參數(shù) |
0x9000E | 1 |
屏幕行數(shù) |
0x9000F | 1 | 屏幕列數(shù) |
0x90080 | 16 |
硬盤1參數(shù)表 |
0x90090 | 16 | 硬盤2參數(shù)表 |
0x901FC | 2 |
根設(shè)備號(hào) |
所以,第一部分獲取 0x90006 地址處的數(shù)據(jù),就是獲取顯示模式等相關(guān)信息。
第二部分就是顯存映射的內(nèi)存地址范圍,我們現(xiàn)在假設(shè)是 CGA 類型的文本模式,所以映射的內(nèi)存是從 0xB8000 到 0xBA000。
第三部分是設(shè)置一些滾動(dòng)屏幕時(shí)需要的參數(shù),定義頂行和底行是哪里,這里頂行就是第一行,底行就是最后一行,很合理。
第四部分是把光標(biāo)定位到之前保存的光標(biāo)位置處(取內(nèi)存地址 0x90000 處的數(shù)據(jù)),然后設(shè)置并開啟鍵盤中斷。
開啟鍵盤中斷后,鍵盤上敲擊一個(gè)按鍵后就會(huì)觸發(fā)中斷,中斷程序就會(huì)讀鍵盤碼轉(zhuǎn)換成 ASCII 碼,然后寫到光標(biāo)處的內(nèi)存地址,也就相當(dāng)于往顯存寫,于是這個(gè)鍵盤敲擊的字符就顯示在了屏幕上。
這一切具體是怎么做到的呢?我們先看看我們干了什么。
1. 我們現(xiàn)在根據(jù)已有信息已經(jīng)可以實(shí)現(xiàn)往屏幕上的任意位置寫字符了,而且還能指定顏色。
2. 并且,我們也能接受鍵盤中斷,根據(jù)鍵盤碼中斷處理程序就可以得知哪個(gè)鍵按下了。
有了這倆功能,那我們想干嘛還不是為所欲為?
好,接下來我們看看代碼是怎么處理的,很簡(jiǎn)單。一切的起點(diǎn),就是第四步的 gotoxy 函數(shù),定位當(dāng)前光標(biāo)。
- #define ORIG_X (*(unsigned char *)0x90000)
- #define ORIG_Y (*(unsigned char *)0x90001)
- void con_init(void) {
- ...
- // 第四部分 定位光標(biāo)并開啟鍵盤中斷
- gotoxy(ORIG_X, ORIG_Y);
- ...
- }
這里面干嘛了呢?
- static inline void gotoxy(unsigned int new_x,unsigned int new_y) {
- ...
- x = new_x;
- y = new_y;
- pos = origin + y*video_size_row + (x<<1);
- }
就是給 x y pos 這三個(gè)參數(shù)附上了值。
其中 x 表示光標(biāo)在哪一列,y 表示光標(biāo)在哪一行,pos 表示根據(jù)列號(hào)和行號(hào)計(jì)算出來的內(nèi)存指針,也就是往這個(gè) pos 指向的地址處寫數(shù)據(jù),就相當(dāng)于往控制臺(tái)的 x 列 y 行處寫入字符了,簡(jiǎn)單吧?
然后,當(dāng)你按下鍵盤后,觸發(fā)鍵盤中斷,之后的程序調(diào)用鏈?zhǔn)沁@樣的。
- _keyboard_interrupt:
- ...
- call _do_tty_interrupt
- ...
- void do_tty_interrupt(int tty) {
- copy_to_cooked(tty_table+tty);
- }
- void copy_to_cooked(struct tty_struct * tty) {
- ...
- tty->write(tty);
- ...
- }
- // 控制臺(tái)時(shí) tty 的 write 為 con_write 函數(shù)
- void con_write(struct tty_struct * tty) {
- ...
- __asm__("movb _attr,%%ah\n\t"
- "movw %%ax,%1\n\t"
- ::"a" (c),"m" (*(short *)pos)
- :"ax");
- pos += 2;
- x++;
- ...
- }
前面的過程不用管,我們看最后一個(gè)函數(shù) con_write 中的關(guān)鍵代碼。
__asm__ 內(nèi)聯(lián)匯編,就是把鍵盤輸入的字符 c 寫入pos 指針指向的內(nèi)存,相當(dāng)于往屏幕輸出了。
之后兩行 pos+=2 和 x++,就是調(diào)整所謂的光標(biāo)。
你看,寫入一個(gè)字符,最底層,其實(shí)就是往內(nèi)存的某處寫個(gè)數(shù)據(jù),然后順便調(diào)整一下光標(biāo)。
由此我們也可以看出,光標(biāo)的本質(zhì),其實(shí)就是這里的 x y pos 這仨變量而已。
我們還可以做換行效果,當(dāng)發(fā)現(xiàn)光標(biāo)位置處于某一行的結(jié)尾時(shí)(這個(gè)應(yīng)該很好算吧,我們都知道屏幕上一共有幾行幾列了),就把光標(biāo)計(jì)算出一個(gè)新值,讓其處于下一行的開頭。
就一個(gè)小計(jì)算公式即可搞定,仍然在 con_write 源碼處有體現(xiàn),就是判斷列號(hào) x 是否大于了總列數(shù)。
- void con_write(struct tty_struct * tty) {
- ...
- if (x>=video_num_columns) {
- x -= video_num_columns;
- pos -= video_size_row;
- lf();
- }
- ...
- }
- static void lf(void) {
- if (y+1<bottom) {
- y++;
- pos += video_size_row;
- return;
- }
- ...
- }
相似的,我們還可以實(shí)現(xiàn)滾屏的效果,無非就是當(dāng)檢測(cè)到光標(biāo)已經(jīng)出現(xiàn)在最后一行最后一列了,那就把每一行的字符,都復(fù)制到它上一行,其實(shí)就是算好哪些內(nèi)存地址上的值,拷貝到哪些內(nèi)存地址,就好了。
這里大家自己看源碼尋找。 所以,有了這個(gè)初始化工作,我們就可以利用這些信息,弄幾個(gè)小算法,實(shí)現(xiàn)各種我們常見控制臺(tái)的操作。
或者換句話說,我們見慣不怪的控制臺(tái),回車、換行、刪除、滾屏、清屏等操作,其實(shí)底層都要實(shí)現(xiàn)相應(yīng)的代碼的。 所以 console.c 中的其他方法就是做這個(gè)事的,我們就不展開每一個(gè)功能的方法體了,簡(jiǎn)單看看有哪些方法。
- // 定位光標(biāo)的
- static inline void gotoxy(unsigned int new_x, unsigned int new_y){}
- // 滾屏,即內(nèi)容向上滾動(dòng)一行
- static void scrup(void){}
- // 光標(biāo)同列位置下移一行
- static void lf(int currcons){}
- // 光標(biāo)回到第一列
- static void cr(void){}
- ...
- // 刪除一行
- static void delete_line(void){}
內(nèi)容繁多,但沒什么難度,只要理解了基本原理即可了。
OK,整個(gè) console.c 就講完了,要知道這個(gè)文件可是整個(gè)內(nèi)核中代碼量最大的文件,可是功能特別單一,也都很簡(jiǎn)單,主要是處理鍵盤各種不同的按鍵,需要寫好多 switch case 等語(yǔ)句,十分麻煩,我們這里就完全沒必要去展開了,就是個(gè)苦力活。 到這里,我們就正式講完了 tty_init 的作用。
在此之后,內(nèi)核代碼就可以用它來方便地在控制臺(tái)輸出字符啦!這在之后內(nèi)核想要在啟動(dòng)過程中告訴用戶一些信息,以及后面內(nèi)核完全建立起來之后,由用戶用 shell 進(jìn)行操作時(shí)手動(dòng)輸入命令,都是可以用到這里的代碼的! 讓我們繼續(xù)向前進(jìn)發(fā),看下一個(gè)被初始化的倒霉鬼是什么東東。 欲知后事如何,且聽下回分解。
本文轉(zhuǎn)載自微信公眾號(hào)「低并發(fā)編程」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系低并發(fā)編程公眾號(hào)。本網(wǎng)站已獲得低并發(fā)編程的授權(quán)。