跳到主要內容

發表文章

目前顯示的是有「原始碼(Source Code)」標籤的文章

尾聲-從硬碟開機

我們要將 Orange'S 安裝到硬碟上,並實現硬碟啟動。 回憶軟碟啟動的過程: BIOS 將開機磁區讀入記憶體 0000:7C00 處 跳轉到 0000:7C00 處開始執行開機程式碼 開機程式碼從軟碟中找到 loader.bin,並將其讀入記憶體 跳轉到 loader.bin 開始執行 loader.bin 從軟碟中找到 kernel.bin,並將其讀入記憶體 跳轉到 kernel.bin 開始執行,到此可認為啟動過程結束 系統執行中 第 1 步中,係由 CMOS 來決定。第 3 步和第 5 步中, 軟碟啟動:程式碼將在軟碟中尋找 loader.bin 和 kernel.bin 硬碟啟動:需要讓開機磁區程式碼從硬碟中尋找 loader.bin 並讓 loader 從硬碟中尋找 kernel.bin 故我們必須重寫 boot.asm 和 loader.asm,讓它們讀取硬碟而不是軟碟。新的檔我們取名為 hdboot.asm 和 hdldr.asm。

尾聲-一些瑣碎的事兒

問題一:我們的系統每次啟動都是「全新」的,上次建立的文件到下一次啟動就不見了。 問題二:每次都會執行一次解開 cmd.tar 的操作。 問題三:再一次解壓縮時,可能已有包含的檔案了

記憶體管理-簡單的 shell

有了 fork() 和 exec(),我們可以實現簡單的 shell 了。目前只實現一種功能:讀取命令並執行之(如果命令存在的話)。

記憶體管理-exec

exec 係用以將現在的處理序映射替換成另一個。亦即我們可從硬碟讀取另一個可執行檔,用它替換掉剛剛被 fork 出來的子處理序。 為測試 exec,勢必先有一個簡單的可執行檔(echo)。因此,我們將製作一個類似於 C 的 Run-time:把過去可供給應用程式使用的函數單獨連結成一個檔,以後直接連結即可。 目前包括: 兩個真正的系統使用 sendrec 和 printx lib/syscall.asm 字串操作 memcopy、memset、strcpy、strlen lib/string.asm FS 的介面 lib/open.c lib/read.c lib/write.c lib/close.c lib/unlink.c MM 的介面 lib/fork.c lib/exit.c lib/wait.c SYS 的介面 lib/getpid.c 其他 lib/misc.c lib/vsprintf.c lib/printf.c 將以上檔案單獨連結成一個檔:oragescrt.a ar rcs lib/orangescrt.a lib/syscall.asm lib/string.asm \ lib/open.c lib/read.c lib/write.c lib/close.c lib/unlink.c \ lib/fork.c lib/exit.c lib/wait.c \ lib/getpid.c \ lib/misc.c lib/vsprintf.c lib/printf.c 並寫一個最簡單的 echo(與 pwd)。 最後,想「安裝」一些應用程式到我們的檔案系統中,有如下工作: 編寫應用程式,並編譯連結 將連結好的應用程式打成一個 tar 包:inst.tar 將 inst.tar 用工具 dd 寫入磁片(映射)的某段特定磁區(假設這一段的第一個扇區的磁區號為 X) 啟動系統,這時 mkfs() 會在檔案系統中建立一個新檔 cmd.tar,它的 inode 中的 i_start_sect 成員會被設為 X 在某個處理序中-比如 Init-將 cmd.tar 解包,將其中包含的檔存入文件系統

用 C# 批次控制 Word 合併列印

前由 我有全區的電話資料,問題在於我要依不同里別來製作出電話簿。結果如下圖: 單純採用合併列印無法達成我的需求。解決方法係用「功能變數」儲存上一個里別,與現在里別進行比較:若不同,則換頁。不過,這樣功能變數還蠻長的。最後,我還是採用 C# 來解決。 解決方案 用 C# 控制 WORD 中合併列印的「資料來源 Data Source」,給予不同里別的「sqlstatement」。迴圈處理不同的里別即可。但可預見其處理過程會很慢,不過還好,我可以不用在意它,有跑出結果即可。 程式碼 IList<string> areas = new List<string>() { "後壁", "侯伯", "嘉苳", "土溝", "嘉田", "嘉民", "菁豊", "崁頂", "後廍", "墨林", "菁寮", "新嘉", "頂長", "平安", "仕安", "竹新", "新東", "長安", "頂安", "福安", "烏樹" }; string root = @"D:\"; // 根目錄 string data = root + @"\data.docm"; // 資料檔(即資料來源) string template = root + @"\template.docx"; // 已設定好格式與合併欄位的 Word 檔 string output = @"d:\Final"; // 輸出之資料夾 object oMissing = System.Reflection.Missing.Va...

記憶體管理-exit 和 wait

假設處理序 P 有子處理序 A。而 A 使用 exit(),那麼 MM 將會: 告訴 FS:A 退出,請作相應處理 釋放 A 佔用的記憶體 判斷 P 是否正在 WAITING 如果是 清除 P 的 WAITING 位 向 P 發送訊息以解除阻塞(到此 P 的 wait() 函數結束) 釋放 A 的處理序表項(到此 A 的 exit() 函數結束) 如果否 設定 A 的 HANGING 位 瀏覽 proc_table[],如果發現 A 有子處理序 B,那麼 將 Init 處理序設定為 B 的父處理序(換言之,將 B 過繼給 Init) 判斷是否滿足 Init 正在 WAITING 且 B 正在 HANGING 如果是 清除 Init 的 WAITING 位 向 Init 發送訊息以解除阻塞(到此 Init 的 wait() 函數結束) 釋放 B 的處理序表項(到此 B 的 exit() 函數結束) 如果否 如果 Init 正在 WAITING 但 B 並沒有 HANGING,那麼「握手」會在將來 B 使用 exit() 時發生 如果 B 正在 HANGING 但 Init 並沒有 WAITING,那麼「握手」會在將來 Init 使用 wait() 時發生 如果 P 使用 wait(),那麼 MM 將會: 瀏覽 proc_table[],如果發現 A 是 P 的子處理序,並且它正在 HANGING,那麼 向 P 發送訊息以解除阻塞(到此 P 的 wait() 函數結束) 釋放 A 的處理序表項(到此 A 的 exit() 函數結束) 如果 P 的子處理序沒有一個再 HANGING,則 設 P 的 WAITING 位 如果 P 壓根兒沒有子處理序,則 向 P 發送訊息,訊息攜帶一個表示出錯的返回值(到此 P 的 wait() 函數結束)

記憶體管理-fork

一個新的處理序需要: 自己的程式碼、資料、堆疊 在 proc_table[] 中佔用一個位置 在 GDT 中佔用一個位置,用以存放處理序對應的 LDT 描述符號 問題在於,第一項的程式碼、資料、堆疊從何而來?傳統上,生成一個新處理序時,係直接由某個已執行的處理序處來繼承或複製。 生成子處理序的系統使用及稱之為 fork()。

檔案系統-將 TTY 納入檔案系統

寫入 TTY 跟寫入普通檔是很類似的,不同之處在於 TTY 不需要進行任何埠操作,只是寫入顯示卡記憶體即可。而對於讀出操作,則有很大的不同。TTY 收到處理序讀出請求的訊息,並不能馬上返回資料,因為此時並沒有任何輸入,而需要使用者輸入字元,等輸入結束後,TTY才可以返回給處理序。問題在於: 怎樣是「輸入結束」:是美一次鍵盤敲擊後算結束(面向字元,Raw mode)?還是等敲 Enter 才算結果(面向行,Cooked mode)?或者其它? 是否要讓檔案系統等待輸入過程結束? TTY 需要馬上向檔案系統發送訊息以示返回。而檔案系統則完全應該阻塞想要得到輸入的處理序,直到輸入結束。 讀 TTY 的過程: 假設處理序 P 要求讀取 TTY,它會發送訊息給檔案系統 檔案系統將訊息傳遞給 TTY TTY 記下發出請求的處理序號等資訊後立即返回 檔案系統此時並不對 P 接觸阻塞,因為結果尚未準備好 檔案系統一如往常等待任何處理序之請求 TTY 將鍵盤輸入複製進 P 傳入的記憶體位址 一旦遇到 Enter,TTY 就告訴檔案系統,P 的請求已被滿足 檔案系統會解除對 P 的阻塞 讀取工作結束 寫 TTY 的過程: P 發訊息給檔案系統 檔案系統傳遞給 TTY TTY 受到訊息候立即將字元寫入顯示卡記憶體(保持 P 和 FS 處理序的阻塞) 完成後發訊息給檔案系統 檔案系統在發訊息給 P 整個過程結束

檔案系統-建立檔案、與其它相關函數

file descriptor 一個檔案在檔案系統中涉及五個要素: 檔案內容(資料)所佔用的磁區 i-node i-node 在 inode-map 中佔用的一位 資料磁區在 sector-map 中佔用的一位或多位 檔案在目錄中佔有的目錄項(direntry) 建立檔案 為檔案內容(資料)分配磁區 在 inode_array 中分配一個 i-node 在 inode-map 中分配一位 在 sector-map 中分配一位或多位 在相應目錄中寫入一個目錄項(direntry) 刪除檔案 釋放 inode-map 中的相應位 釋放 sector-map 中的相應位 刪除根目錄中的目錄項

檔案系統-完善硬碟驅動並製作一個檔案系統

前篇只是完成 DEV_OPEN,必須完成後續的 DEV_READ 和 DEV_WRITE,以利檔案的操作。 「檔案系統」需要: 有地方存放 Metadata-super block 有地方紀錄磁區的使用情況-sector map 有地方來紀錄任一檔的資訊,比如佔用哪些磁區等-i-node map、inode_array (存放真正的 i-node) 有地方存放檔的索引-root 資料區 檔案系統

檔案系統-硬碟驅動程式

請先建立一顆硬碟(80MB),分為 1 個 primary partition, 1 個 extended partitions, 4 個 logical partitions。 而我在此處採用 VirtualBox,故需要將 raw file 轉成 vdi 格式。 $ bximage -hd -mode=flat -size=80 -q 80m.img $ fdisk 80m.img $ VBoxManage convertfromraw -format VDI 80m.img 80m.vdi 硬碟分割結果(自由切割)

處理序間通訊(IPC, Inter-Process Communication)

採用微核心。 IPC 分為兩種: 非同步 IPC:送完訊息就不管了 同步 IPC:發送者一直等到接收者收到訊息才放手,接收者接不到訊息就一直等著。優勢如下: 作業系統無須另外維護緩衝區來存放正在傳遞的訊息 作業系統無須保留一份訊息副本 作業系統無須維護接收佇列(發送佇列還是需要的) 發送者和接收者都可在任何時刻清晰且容易第知道訊息是否送達。 假設有處理序 A 想要向 B 發送訊息 M,其過程如下: A 首先準備好 M A 透過系統使用 sendrec,最終使用 msg_send 簡單判斷是否發生鎖死 判斷目標處理序 B 是否正在等待來自 A 的訊息 如果是:訊息被複製給 B,B被解除阻塞,繼續執行 如果否:A 被阻塞,並被加入到 B 的發送佇列中 假設有處理序 B 想要接收訊息(來自特定處理序、中斷或者任意處理序),其過程如下: B 準備一個空的訊息結構體 M,用於接收訊息 B 通過系統使用 sendrec,最終使用 msg_receive 判斷 B 是否有個來自硬體的訊息(通過 has_int_msg),如果是,且 B 準備接收來自中斷的訊息或準備接收任意訊息,則馬上準備一個訊息給 B,並返回。 如果 B 想接收來自任意處理序的訊息,則從自己的發送佇列中選取締一個(如果佇列非空的話),將其訊息複製給 M 如果 B 想接收來自特定處理序 A 的訊息,則先判斷 A 是否正在等待向 B 發送訊息,若是的話,將其訊息複製給 M 如果此時沒有任何處理序發訊息給 B,B 會被阻塞

輸入/輸出系統-printf

printf 使用過程示意圖 程式碼 include/const.h (修改) #define NR_SYS_CALL 2 include/global.h (新增) extern TASK user_proc_table[]; include/proc.h (修改) typedef struct s_proc { STACK_FRAME regs; /* process registers saved in stack frame */ u16 ldt_sel; /* gdt selector giving ldt base and limit */ DESCRIPTOR ldts[LDT_SIZE]; /* local descriptors for code and data */ int ticks; /* remained ticks */ int priority; u32 pid; /* process id passed in from MM */ char p_name[16]; /* name of the process */ int nr_tty; /* 用以指定此處理序的 TTY */ } PROCESS; /* Number of tasks & procs */ #define NR_TASKS 1 #define NR_PROCS 3 include/string.h (新增) PUBLIC int strlen(char* p_str); include/type.h (新增) typedef char * va_list; kernel/proc.c (修改) for (p = proc_table; p < proc_table+NR_TASKS+NR_PROCS; p++) { for(p=proc_table;p < proc_table+NR_TASKS+...

輸入/輸出系統-TTY

TTY任務 在 TTY 任務中執行一個迴圈,此迴圈將輪詢每一個 TTY,處理它的事件,包括從鍵盤緩衝區讀取資料、顯示字元等內容。 並非每輪詢到某個 TTY 時,箭頭所對應的全部事件都會發生,只有當某個 TTY 對應的控制台是現在控制台時,它才可以讀取鍵盤緩衝區(虛線) TTY 可以對輸入的資料作更多的處理,但在這裡,我們只把它簡化為「顯示」一項。 雖然圖中鍵盤和顯示器的畫在 TTY 的外面,但應把鍵盤和顯示器算做每一個 TTY 的一部分,它們是公用的。 執行的過程如上,則輪詢到每個 TTY 不外乎作兩件事: 處理輸入:查看是不是現在 TTY,如果是則從鍵盤緩衝區讀取資料 處理輸出:如果有要顯示的內容則要顯示它 TTY 任務程式碼示意 與先前程式實現的區別在於: 每個 TTY 都應該有自己的讀和寫的動作。所以在 keyboard_read() 內部,函數需要了解自己是被哪一個 TTY 使用。我們透過為函數傳入一個參數來作到這一點,此參數指向當前 TTY 的指針。 為了讓輸入和輸出分離,被 keyboard_read() 使用的 in_process() 不應該在直接回顯字元,而應該將回顯的任務交給 TTY 來完成,這樣,我們就需要為每個 TTY 建立一塊緩衝區,用以放置將被回顯的字元。 每個 TTY 回顯字元時操作的 CONSOLE 是不同的,故每個 TTY 都應該有一個成員來記載其對應的 CONSOLE 資訊。

輸入/輸出系統-鍵盤

敲擊鍵盤有兩個方面的含義:動作和內容。 動作可分為三類:按下、保持按住的狀態、放開 內容則是鍵盤上不同的鍵。 敲擊鍵盤所產生的編碼被稱作掃描碼(Scan Code),它分為 Make Code 和 Break Code 兩類。 當一個鍵被按下或者保持住按下時,將會產生 Make Code,當鍵盤彈起時,產生 Break Code。 除了 Pause 鍵之外,每個按鍵都對應一個 Make Code 和一個 Break Code。 當 8048 檢測到一個鍵的動作後,會把相應的掃描碼發送給 8042,8042會把它轉換成相應的 Scan code set1 掃描碼,並將其放置在輸入緩衝區中,然後 8042 告訴 8259A 產生中斷(IRQ1)。如果此時鍵盤又有新的鍵被按下,8042 將不再接收,一直到緩衝區被清空,8042 才會收到更多的掃描碼。

遲到的處理序-處理序調度

程式碼 include/proc.h /*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ proc.h ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Forrest Yu, 2005 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ typedef struct s_stackframe { /* proc_ptr points here ↑ Low */ u32 gs; /* ┓ │ */ u32 fs; /* ┃ │ */ u32 es; /* ┃ │ */ u32 ds; /* ┃ │ */ u32 edi; /* ┃ │ */ u32 esi; /* ┣ pushed by save() │ */ u32 ebp; /* ┃ │ */ u32 kernel_esp; /* <- 'popad' will ignore it │ */ u32 ebx; /* ┃ ↑棧從高地址往低地址增長*/ u32 edx; /* ┃ │ */ u32 ecx; /* ┃ │ */ u32 eax; /* ┛ │ */ u32 retaddr; /* return address for assembly code save() │ */ u32 eip; /* ┓ │ */ u32 cs; /* ┃ │ */ u32 eflags; /* ┣ these are pushed by CPU during interrupt │ */ u32 ...

遲到的處理序-里程碑式的成果

Orange'S 的運轉過程

遲到的處理序-第一個處理序

處理序 宏觀:有自己的目標或功能,且又受控於處理序調度模組(就是老闆啦!) 微觀:利用系統資源、有自己的程式碼和資料,與自己的堆疊;需要被調度,就如一個人輪換著做不同工作 處理序示意圖 未雨綢繆 未來會有多個處理序,但 CPU 只有一個,亦即 CPU 通常小於處理序個數:同一時刻,總有「正在執行的」與「正在休息的」處理序。對於「正在休息的」處理序,我們必須讓它在重新醒來時記住自己掛起前的狀態,以便讓原來的任務繼續執行下去。 因此,我們需要一個資料結構紀錄一個處理序的狀態:在處理序要被掛起的時候,處理序資料被寫入這個資料結構,等到處理序重新開機時,這個資訊重新被讀出來。 資料結構,用以紀錄處理序的狀態 最簡單的處理序 處理序A執行中 時鐘中斷發生,ring1 -> ring0,時鐘中斷處理常式啟動 處理序調度,下一個應執行的處理序(假設為處理序B)被指定 處理序B被恢復,ring1 -> ring0 處理序B執行中

為了處理序,幫核心新增中斷處理吧!

作為一個作業系統,處理序是最為基本且重要的東西。不僅是一個處理序,應該是多個處理序。 然而,若要達到可控制處理序,就涉及到處理序和作業系統之間執行的轉換。 因為 CPU 只有一個,同一時刻要麼「客戶處理序」在執行,不然就是「作業系統」在執行。 因此,為達到處理序之目的,勢必要有一種控制權轉換機制,亦即「中斷」。 主要工作:設定 8259A 和建立 IDT

擴充核心-切換堆疊和GDT

接續「 重新放置核心-整理記憶體中的核心並將控制權交給它 」。 esp, GDT 等內容目前還在 Loader中,為了方便控制,我們預計將它們放進核心中。 再者,目前已經可以採用 C 語言了,因此我們將盡量避免用編譯。在此,我們透過 C 語言將 Loader 中的原 GDT 全部複製給新的 GDT,並將 gdt_ptr 中的內容換成新的 GDT 的基底位址和界限。