輸入/輸出系統(包含鍵盤和顯示器與用戶交互)
經過這一章,操作系統才擁有和用戶交互的接口,用戶才能通過鍵盤操作它,並在顯示器獲取結果。
先是鍵盤
鍵盤中斷對應的是8259A的IRQ1,外部硬件中斷處理的框架已經搭好,現在需要做的只是寫好中斷處理程序並把它的地址填進函數指針數組即可。
鍵盤初體驗
先寫鍵盤中斷處理函數(新建keyboard.c)
PUBLIC void keyboard_handler(int irq)
{
disp_str("*");
}
然後設置函數指針數組(keyboard.c)
PUBLIC void init_keyboard()
{
put_irq_handler(KEYBOARD_IRD,keyboard_handler);/*設定鍵盤中斷程序*/
enable_irq(KEYBOARD_IRQ);/*開鍵盤中斷*/
}
就是簡單的接收鍵盤有輸入就顯示一個*.而且你會發現只能顯示一次,後面就會顯示鍵盤緩衝器滿。
鍵盤敲擊過程
現在主流的鍵盤都是USB的了,曾經還有AT和PS/2鍵盤。
graph LR
8048-->8042
8042-->8259A
- 8048鍵盤編碼器:獲取鍵盤輸入,並傳適當的數據給計算機
- 8042鍵盤控制器:接收和解碼來自鍵盤的數據,並與8259A和軟件通信
鍵盤敲擊:動作+內容,所以8048不僅要反映是哪個鍵,還要反映是什麼動作
- 動作:按下、保持按下、放開
- 內容:即鍵盤上不同的鍵
敲擊鍵盤產生的編碼:掃描碼Scan code,分成兩類
- Make Code:按下或者保持住時發送
- Break Code:彈起時發送
有三套:Scan code set 1、set 2、set 3
現在主流的是set2,比AT更老的XT鍵盤則是用set1
整個過程:
8048檢測到一個鍵的動作->把相應掃描碼發給8042->8042把它轉成相應的Scan code set1掃描碼->將其放到輸入緩衝期中->8042告訴8259A產生中斷(IRQ1)
此時,如果有新的按鍵被按下,8042將不再接收,直到緩衝區被清空(所以這個需要我們在中斷處理程序中讀取清空)
- 那麼如何從緩衝區讀取掃描碼?看8042
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-pHsPnFDP-1575882874068)(http://yeholdon.top/wp-content/uploads/2019/12/IMG_20191206_165240-scaled.jpg)]
也就是說也是用in指令來讀取。in_byte(0x60)
-
值得注意的是:在keyboard_handler裏in_byte後,可以持續讀取鍵盤輸入了,而且按一次出現兩個星號(keyboard_handler被調用兩次,兩次中斷)因爲一次敲擊包括按下Make Code和彈起Break Code兩個動作
-
打印讀取的掃描碼:獲取in_byte(0x60)的返回值(u8)
-
然後將得到的掃描碼對照scan code set 1就能得到按鍵內容。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-w2HbRHEA-1575882874071)(http://yeholdon.top/wp-content/uploads/2019/12/IMG_20191206_170835-scaled.jpg)]
值得注意的是,雖然鍵盤支持的是Scan code set2,但是最後傳入計算機的又是scan code set1.這時基於爲XT鍵盤寫的程序兼容性而考慮的。
用數組來存scan code set 1表
簡單來說就是掃描碼和具體鍵盤字符的映射表。
寫在keymap.h頭文件中。其中每3個值是一組(MAP_COLS=3),分別爲單獨按、Shift+某鍵和有0xE0前綴的掃描碼對應的字符。Esc,Enter等被定義成了宏,宏的具體數值無所謂,只要不造成衝突混淆即可,讓OS認識即可。注意0xE0和0xE1開頭的掃描碼要區別對待。
- 這樣問題就出現了,用戶輸入一個想要的字符,可能會是2個掃描碼,也可能是4個掃描碼,而8042的緩衝區大小隻有一個字節,所以實際輸入一個字符可能會產生2次4次等不同的中斷次數,另外,還有可能用戶先按了shift又鬆開了,我們必須將整個連續合法的碼序列作爲整體來處理。
- 爲此,需要將收到的掃描碼保存起來,然後結合後面的輸入一起解析用戶的意圖。
- 因爲處理起來比較複雜,所以如果我們把處理掃描碼的部分都放在keyboard_handler裏的話,這個函數會變得很大,結構不清晰。爲此,參考Minix,建立一個緩衝區,讓keyboard_handler每次收到的掃描碼都放入這個緩衝區,然後建立一個新的任務專門用來解析它們並做相應處理。
鍵盤輸入緩衝區
緩衝區用結構體s_kb來定義:
/* Keyboard structure, 1 per console. */
typedef struct s_kb {
char* p_head; /* 指向緩衝區中下一個空閒位置 */
char* p_tail; /* 指向鍵盤任務應處理的字節 */
int count; /* 緩衝區中共有多少字節 */
char buf[KB_IN_BYTES]; /* 緩衝區 */
}KB_INPUT;
結構如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-4HPKpKjP-1575882874072)(http://yeholdon.top/wp-content/uploads/2019/12/IMG_20191206_190617-scaled.jpg)]
注意這個是循環的,p_tail到達末尾後指針移到開頭
- 有了緩衝區,那就是將傳入的掃描碼添加到隊列中。這部分是在keyboard_handler裏實現的。如果緩衝區已滿,則直接丟棄新的碼。
用新加的任務處理鍵盤操作
前面設立了鍵盤緩衝區,就是爲了方便後面針對一串掃描碼串來統一處理。而且,這樣就可以使鍵盤中斷和碼處理不是嚴格的同時進行的,這樣做的好處就是可以用一個單獨的任務(進程)來處理鍵盤操作,而無需全部都寫在鍵盤中斷處理程序裏。而且,雖然不是有中斷馬上就處理,但是因爲進程的切換很快,這個時間差可以忽略。
- 新的任務task_tty()就是循環地調用keyboard.c裏定義的keyboard_read()函數。
- keyboard_read()函數裏要注意讀取的整個過程是要連貫的,不應該受到打擾,所以在讀取前後分別打開和關閉中斷。
解析掃描碼
前面只是搭好了框架,可以正常獲取掃描碼了,下面開始真正的解析算法。較爲複雜。。。具體工作就是擴充keyboard_read()
顯示簡單的字符(無shift等)
由簡到難,先處理小寫字母,再處理組合輸入等情況
break code 是make code和0x80 或or的結果;make Code 是break code 和0x7F and與的結果
處理shift、alt、ctrl等組合鍵
這三個每個都在鍵盤上有兩個鍵,而且有的時候左右shift是不同的,所以用6個int來表示它們的狀態。再加上caps lock ,num lo還有scroll lock等表示狀態的鍵。
- 如果一個完整的操作還沒結束(比如一個2字節的掃描碼還未完全讀入,則key賦值爲0,等到下一次keyboard_read()被執行時再繼續處理。也就是說,目前的情況是,一個完整的操作需要在keyboard_read()多次調用時完成。
處理剩下的所有按鍵
目前已經可以處理大部分按鍵了,但是還存在兩個問題:
- 更復雜的掃描碼,比如超過3個字符
- F1等功能鍵,系統把它們當成可打印字符處理,打印的還是奇怪的符號。
-
先解決第一個問題
- 目前的實現是,一個完整的讀取操作要調用多次keyboard_read()完成,因爲它一次只讀取一個字符。這樣還得加全局變量,記錄上一次讀取的結果,邏輯上比較難以理解。
- 符合邏輯的做法是,既然按下一個鍵會產生一個或者多個自己的掃描碼,那就應該在一個處理過程中把它們都讀出來。
- 實現也不難,只要把從kb_in讀取一個字符的代碼單獨用一個函數實現即可。get_byte_from_kbbuf()。
- 另外,以0xE1開頭的PAUSE鍵和以0xE0開頭的PrintScreen鍵都和其他普通的0xE1或者0xE0開頭的鍵不同,因爲它們一個make code有6個字符,另一個有4個字符,而普通的只有2個字符。所以需要特殊處理。
-
下面解決第二個問題,不可打印的控制字符怎麼處理
- 爲了增加程序的通用性,keyboard_read()就只負責讀取和匹配得到標準掃描碼,至於如何處理,應該交給上層軟件來處理。
- 所以,我們將前面打印得到的掃描碼的部分也去掉。然後,新建一個in_process函數來處理。
- 在定義各個非可打印字符的宏的時候,就在第9位增加了一個FLAG_EXT標誌位了,所以在in_process中,只要將key和FLAG_EXT&一下就能夠區分是可打印字符還是普通的非可打印字符。
- 對於這些非可打印字符,暫時還不處理,也就是沒有反應,後面可以在in_process中輕鬆添加。
另外,很值得注意的一點就是,對於shift這些按鍵,它們的作用主要和和別的按鍵組合使用,所以很多時候只需要將它們是否按下的狀態添加到正常的按鍵碼上即可。而且這裏無論單鍵還是組合鍵都是用的是32位的key表示,除去表示可打印字符和非可打印字符的第9位,還剩下32-9=23位來表示shift、ctrl、alt等鍵的狀態,這足夠鏈路。
接下來就是顯示器(終端)
熟悉的終端終於要出場了。因爲隨着鍵盤模塊的完善,需要考慮它和屏幕輸出的關係了。IO是密不可分的。
先了解一下終端和顯示器的驅動方式
TTY終端
tty也稱爲終端,直觀的認識是,通過按alt+f1等f功能鍵,可以切換到不同的屏幕界面,不同屏幕中分別有各自的IO,相互不受影響。但這只是表象,實際並沒有那麼簡單
TTY的結構
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-VlhhpIVd-1575882874073)(http://yeholdon.top/wp-content/uploads/2019/12/IMG_20191207_145538-scaled.jpg)]
可以看到,三個tty共用一個keyboard,而且實際也公用了同一塊顯存,這就 需要在切換tty的時候讓屏幕顯示顯存中某個位置的內容。這可以通過端口操作簡單地做到。
顯示器的基本概念
實際,對於顯示器的操作,我們可能操作的是顯卡,也可能只是顯存。顯示器實際有很多種模式,現在我們接觸到的只是默認的80*25文本模式,另外還有很多更復雜和強大的模式,用來顯示更強大的色彩等。
- 80*25文本模式下,顯存大小爲32KB,地址範圍爲0xB8000~0xBFFFF.每兩個字節代表一個字符,低字節是ASCII碼,高字節是屬性。
- 一個屏幕映射所佔空間就好計算了,80252=4000 Byte,而顯存爲32K,可見,顯存最多可以存放8個屏幕映射的數據。如果這時只用3個,那每個就可以用10多KB空間,這時還可以實現簡單的滾屏功能。
- 那麼,在切換tty的時候,如何讓系統界面(顯示器顯示指定位置的內容呢?其實和簡單,通過端口操作設置相應寄存器即可
VGA視頻子系統寄存器操作
- 對寄存器的訪問通過讀寫端口實現
- 但是如果多個寄存器只有一個端口,就要通過地址寄存器,先向地址寄存器寫需要的寄存器的索引號,然後再通過那個唯一的端口號讀寫。
- 讓光標隨着輸入字符位置移動:通過設置Cursor Location High/Low Register兩個寄存器來實現,將光標位置設爲disp_pos/2即可。
- 實現滾屏功能:設置Start Address High/Low Register,設置當前屏幕開始顯示的位置,實現整體滾屏。
TTY 任務
TTY任務,也就是前面的task_tty(),在其中執行了一個循環,這個循環將輪詢每一個TTY,然後處理輪詢到的TTY的事件,包括從鍵盤緩衝區讀取數據、顯示字符等。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-7ZJoV1Fn-1575882874074)(http://yeholdon.top/wp-content/uploads/2019/12/IMG_20191207_162330-scaled.jpg)]
- 並非每次輪詢到某個TTY時,箭頭所對應的全部事件都會發生,只有當某個TTY對應的控制檯是當前控制檯時,它才能夠讀取鍵盤緩衝區(虛線表示)。
- 這裏多出了一個控制檯的概念。從前面可以看到,控制檯是包含在TTY裏的,或者說每個TTY對應有一個console控制檯。
- 簡單來說,TTY裏設置了一個緩衝區,用來存放當前TTY從鍵盤緩衝區讀入並處理後待顯示的字符。而這個字符的顯示此時不是直接由in_process這個處理模塊來直接顯示了,而是建立一個新的模塊console來處理TTY的待顯示內容的顯示。
- 這樣高度模塊化是有好處的,可以將讀取、處理和顯示三個步驟分開來,一定程度上可以不用完全同步,這和緩衝區的設置是一致的,因爲要實現這樣的模塊化就需要緩衝區來暫存。
TTY任務框架
新建兩個結構體,分別爲s_tty和s_console,定義在tty.h和console.h中。
typedef struct s_console
{
unsigned int current_start_addr; /* 當前顯示到了什麼位置 */
unsigned int original_addr; /* 當前控制檯對應顯存位置 */
unsigned int v_mem_limit; /* 當前控制檯佔的顯存大小 */
unsigned int cursor; /* 當前光標位置 */
} CONSOLE;
可以看到,其實控制檯就是TTY中,負責操作顯存顯示的部分。規定了該TTY當前顯示的情況,比如佔據的顯存的位置,大小,以及當前顯示到的位置。控制檯是直接與用戶接觸的,當我們通過alt+fn切換終端時,其實就是在切換控制檯。而控制檯對應的TTY則是由TTY任務按照一定的頻率輪詢的。可以看成是一個程序的界面和後臺的關係,後臺進程被不斷地調度,而前端的用戶接口則保持同一個,除非用戶自己切換。
typedef struct s_tty
{
u32 in_buf[TTY_IN_BYTES]; /* TTY 輸入緩衝區 */
u32* p_inbuf_head; /* 指向緩衝區中下一個空閒位置 */
u32* p_inbuf_tail; /* 指向鍵盤任務應處理的鍵值 */
int inbuf_count; /* 緩衝區中已經填充了多少 */
struct s_console * p_console; // 指向對應console的指針
}TTY;
- 只有當某個tty對應的console是當前console的時候,它纔可以讀取鍵盤緩衝區。 這個理解起來也簡單,就像現在我們在OS中打開多個程序窗口,但是我們開始打字的話,字也只會被“當前”窗口獲取,如果當前窗口不是我們想要輸入的窗口,我們就需要先通過點擊或者鍵盤快捷鍵切換到目標窗口再輸入。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-uMYQpTP2-1575882874075)(http://yeholdon.top/wp-content/uploads/2019/12/IMG_20191207_171439-scaled.jpg)]
幾點開發和調試的總結教訓:
- 添加新的源文件就馬上在makefile中加上
- 寫一個新的函數的時候,就先考慮它是否要在別的源文件裏調用,以此來判斷是否加static也就是PUBLIC和PRIVATE
- 當寫的函數裏要調用別的函數而別的函數又還沒實現的時候,就會先去寫別的函數,但這時候最好標記一下前面的那個函數,不然很可能寫完就忘了原來在哪裏調用它了。
- PRIVATE的函數就在本文件前面聲明即可,PUBLIC的則要在proto.h這個專門放函數聲明的頭文件裏聲明。別的頭文件就可以不放函數聲明瞭,也儘量不包含別的頭文件,除非裏面的某些結構體定義中用到了對應的結構,這樣才包含對應頭文件。
總結:TTY任務開始運行時,所有TTY被初始化,全局變量nr_current_console被賦默認初值0.然後輪詢開始並一直進行下去。對於每一個TTY,先執行tty_do_read(),它將調用keyboard_read()並將讀入的字符交給函數in_process()處理,如果是需要輸出的字符,會被in_process放入當前TTY的緩衝區。然後tty_do_write()接着執行,如果緩衝區有數據,就會被送到out_char顯示出來。
目前因爲沒有切換過current console所以,其他tty輪詢到時,都被is_current_console()忽略掉了。
現在框架已經搭好,下面進行多個console的切換。
多控制檯
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-a3L3Ufl4-1575882874076)(http://yeholdon.top/wp-content/uploads/2019/12/IMG_20191207_205530-scaled.jpg)]
可以看到,一個console的總大小是大於80* 25 的,所以可以做到滾屏,相當於將80 *25的窗口整體下移(或者上移)直到到達console的邊界,就做到了滾屏。
圖上標註了s_console結構裏的成員,之前其實都還沒有用到。
- original_addr和v_mem_limit用作定義控制檯所佔顯存總體情況,它們是靜態的,一經初始化就不再改變
- current_start_addr將隨着屏幕捲動而變化
- cursor每輸出一次字符就更新一次
先初始化控制檯
init_screen()函數主要是初始化CONSOLE結構的那4個成員,並顯示標識console的序號和命令提示符$,並在init_tty()裏調用
需要注意的是,結構CONSOLE的成員都是以Word計的,這符合VGA寄存器操作的使用習慣。
修改out_char以適應多控制檯情況
主要是傳入CONSOLE,,然後根據該CONSOLE來顯示,而非disp_pos
切換控制檯
-
這一步是很關鍵的,其實說到底切換控制檯就是通過設置VGA系統的Start Address High/Low Register來讓當前這個只有80*25的顯示窗口移到目的控制檯所對應的那段顯存空間。
-
所以切換控制檯的操作是通過前面用過的滾屏的方法操作對應的CRT寄存器來實現的。
這裏要注意!
我的電腦當前是Ubuntu18.04系統,第一次測試的時候,發現只有F3能夠正確切換到console2,F1和F2貌似都不行,而且alt+F2貌似被系統佔用了,按下觸發的是系統的功能。因此我懷疑這裏的很多組合鍵是被系統一直佔用的,並沒有映射到bochs虛擬機裏。
經過測試,F5也是可以用的,但是F6和F7又不行。爲了能夠三個連續,最後找到了F9~F11都是可以的,爲此,這個問題算是解決了。
-
然後是添加滾屏代碼scroll_screen.
簡單起見,當屏幕滾到最下端或者最上端後,再按shift+down和shift+up就不再響應。 -
但是這樣還有一個問題,就是當寫滿當前屏幕後再往下寫,按照習慣應該自動往下滾動一行,現在來自行添加這個功能。主要是在out_char裏添加判斷和滾屏。
-
再進一步,如果寫滿屏幕後手動向上滾動了幾行,這時候又繼續寫,按照常規用戶習慣,應該自動滾動到當前光標所在行繼續寫,這個其實也簡單,就是在前面自動向下滾動一行的基礎上先計算出當前屏幕顯示的結尾和當前光標所在行差的行數,然後循環向下滾動這個差的行數即可。
完善鍵盤處理
到目前爲止,只剩下鍵盤上的某些鍵的處理程序沒有寫了,現在補上。
- 回車和退格:通過向tty緩衝區添加\n和\b實現,同時記得修改out_char
- Caps Lock\Num Lock\Scroll Lock:鍵盤上這三個鍵都有相應的狀態指示燈,可以通過寫入8042的輸入緩衝區來控制它們。輸入緩衝區和控制寄存器都是可寫的,只不過寫入緩衝區是用來給8048(在鍵盤上編碼器)發送命令。而寫入控制寄存器是往8042本身發送命令。
- 我們要操作的顯然是鍵盤本身,所以應該往8048發命令,使用0x60端口。設置LED的命令爲0xED,鍵盤收到後回一個ACK(0xFA),然後等待從0x60寫入的LED參數字節,表示LED狀態,收到後再回一個ACK。
- 另外往8042緩衝區寫數據前要通過讀0x64端口判斷是否緩衝區爲空,爲空才能寫數據。
TTY任務總結
運行在ring0的keyboard_handler主要是負責獲得讀入掃描碼,存入kb_in緩衝區。而終端任務task_tty()則是運行在ring1下的一個任務(進程,至少目前這倆是等價的),它負責讀取kb_in裏的掃描碼,處理得到具體的key後寫入當前tty的緩衝區,再由console輸出到屏幕顯示。所以,比較特殊的是,這裏的kb_in是在ring0下寫,ring1下讀的。這也是Minix的做法。
區分任務和用戶進程
現在,我們有了四個進程(任務)分別是TTY、A、B、C。ABC對於我們的OS來說是可有可無的,不運行也不會有什麼影響,而TTY則是必須的,沒有它,我們無法使用鍵盤進行用戶交互。因此,TTY應該是操作系統的一部分,我們有必要區分這兩種進程。TTY稱爲任務,A、B、C則稱爲用戶進程。實現上也做一些改變,讓用戶進程運行在ring3上,任務繼續是ring1,總結來說,就是將系統任務和用戶進程區分開來。這樣將特權級進一步層次化,有利於管理和保護操作系統。
具體來講就是將原來的NR_TASKS分成NR_TASKS和NR_PRCS用到的地方和描述符特權級也做相應修改,另外還有進程調度應該還是一起調,沒有區別。
自己的printf
現在我們的TTY已經有了雛形,可以寫一個用來在控制檯輸出的C庫函數printf()了。這裏要注意printf()這種函數並不是系統調用,而是C語言提供的庫函數,但是printf也是通過調用系統提供的系統調用實現的,比如write()這個系統調用,因此,我們在實現自己的printf的時候還要先實現write系統調用。
爲進程指定TTY
調用printf的是進程,而輸出的對象的TTY的控制檯,printf調用系統調用的時候,從ring3轉到了ring0,系統只能知道當前系統調用是由哪個進程觸發的,因此爲了讓printf知道要輸出到哪個控制檯,就要讓用戶進程有從屬於的TTY.具體實現可以在進程表中增加一個nr_tty成員。
printf()的實現
printf的實現其實並不簡單,因爲它的參數個數和類型都是可變的,而且其中表示格式的參數形式很多樣(%d,%x等等)。所以,由淺入深,先實現一個簡單的——只支持"%x"一種格式的printf()
我們知道,printf的結構是這樣的
printf("以%開頭的格式串",和格式串裏的格式對應的數量不定的輸出對象);
所以除了第一個格式字符串,後面的參數個數是不定的,爲此,C語言給出的是用…表示的可變參數。可變參數實現的原理也值得一說:
-
調用函數的過程是調用者將參數傳入堆棧,被調用的函數獲取堆棧裏的參數,最後完成調用過程後需要有人清理堆棧,恢復調用前的狀態。
-
這裏就涉及兩個問題:
- 一是參數壓棧的順序如何
- 二是由誰來清理堆棧,是調用者還是被調用者
-
規定這兩個問題的叫做調用約定calling conventions
-
對於C語言來說,使用的是C調用約定:後面的參數先入棧,由調用者清理堆棧。
-
這種方式的好處:在支持可變參數時得到了充分體現,因爲只有調用者才知道這次調用包含了幾個參數,清理堆棧的時候就很方便。而被調用者雖然通過一定的方式(比如用一定有的第一個參數來間接地告知被調用者後面參數的情況)得知參數的情況,但是這顯然有一定限制,也不夠方便。
-
但是可變參數的情況下,被調用者也需要知道參數的具體情況才能區分和使用這些參數。所以,printf()的實現還是用到了第一個一定有的參數作爲參考,傳達給被調用者傳入參數的個數和類型格式等。
-
具體實現:
- 定義一個buf緩衝區,用來暫存按照第一個參數指向的格式符來轉換輸出格式後的待輸出內容,由於第一個參數是char*也就是一個指針變量,佔4個字節,至於如何獲取第一個參數這個指針fmt指向的格式字符串的邊界,可以通過循環的終止條件來解決,不斷地讓指針自增,直到它指向的內容爲空。
- 後面的參數就可以根據緩衝區裏分離 出來的格式符的個數以及類型來獲取後面的可變參數
- 至於後面可變參數列表的首地址,則可以通過第一個參數的地址加上4的偏移得到。
- 得到了待輸出的格式化字符流buf後,就要調用系統調用來輸出到顯示屏了。
- 這個系統調用,就是write(),用在很多地方,比如網絡編程裏也有使用。前面已經實現過一個系統調用,所以這裏也比較容易實現了。
- 系統調用名稱爲write(),對應的內核部分爲sys_write()
-
增加一個系統調用的過程
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-uVx6IIK1-1575882874077)(http://yeholdon.top/wp-content/uploads/2019/12/IMG_20191208_211643-scaled.jpg)] -
一個使用了系統調用的C庫函數的調用過程(printf):
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-aiU9aMv7-1575882874078)(http://yeholdon.top/wp-content/uploads/2019/12/IMG_20191209_103726-scaled-e1575859118288.jpg )]
最後再記錄一下這次開發調試時遇到的問題和教訓
這一節在完成開發後運行,發現雖然各個console裏能夠正確輸出綁定在其上的用戶進程的printf()輸出。但是控制檯卻也隨着輸出在兩個輸出的console之間快速跳動。其實通過上面的分析很容易知道問題出在調用過程的最後一環也就是tty_write()裏的out_char()函數,因爲只有這個函數纔會直接操作顯存切換。
但是讓我惱火的是,我第一次逐字覈對了out_char函數,卻沒有問題,然後我就懵逼了。開始回溯整個過程,雖然發現了幾個遺漏的小問題,但是都不是造成這個console快速切換跳動的原,有些強迫症的我還爲此糾結了一晚上。結果第二天早上,我又從理性出發,覺得問題一定出在out_char,就又再覈對了一遍,原來並不是out_char本身有問題,而是裏面調用的flush()這個用來更新重新設置的當前console到顯示器的函數的問題,因爲flush應該是要判斷當前TTY是對應當前console後才能刷新的,而我遺漏掉了這個,直接導致只要輸出字符調用out_char就切換到了調用out_char的進程對應的console。
- 最後總結一下經驗教訓:
對於問題要理性分析,縮小排查範圍,然後再仔仔細細地在出問題的範圍內排查,就像這次,排查到out_char函數,不應該只看它的函數體有沒有錯誤,還要查看衍生的函數調用鏈上的每一個函數直到調用鏈的最後一個函數爲止。也就是縮小範圍,從“深度”着手 ,否則盲目地亂查效率很低…真的…很低…
結語
到這裏,一個OS的雛形已經基本實現,我們可以在多個控制檯之間切換,各個控制檯下運行着各自的用戶進程,彼此互不干擾,呈現出了一個多任務多控制檯操作系統雛形。接下來將進入一個操作系統的其他一些基本組成模塊的編寫,包括進程間通信、文件系統、內存管理。