注 : 文中講述的原理是推理和探討 , 和現實中的實現不必定徹底相同 。html
操做系統 , 主要分爲 8 個部分 :算法
1 引導程序數據庫
2 設備驅動數組
3 控制檯網絡
4 進程調度多線程
5 虛擬內存架構
6 文件系統函數
7 網絡通訊spa
8 編譯器操作系統
引導程序, 按照如今的 業界標準, 大概是 接通電源 -> BIOS 啓動 -> 引導程序 。 引導程序 是 磁盤開頭的 一段 字節 存儲的 代碼 。 BIOS 啓動後 就將 控制權交給這段代碼, 或者說加載這段代碼進入內存, 並執行這段代碼 。 引導程序 加載到內存裏應該也是存儲在 內存的 低位地址 附近, 好比 從 地址爲 0 , 或者 1 的 內存單元 開始 存儲 。 不過我到如今都有一個疑問, 內存地址 可使用 「0」 這個地址嗎 ? C / C++ / C# 好像都是用 0 來表示 空指針(null) 的 。
引導完了, 要顯示一個 界面 給 用戶看, 最基本的就是 控制檯 。 要顯示控制檯, 須要操做顯示器, 因此 這就是 須要 設備驅動 。
固然, 還須要 鍵盤 鼠標 的 輸入, 最起碼要有 鍵盤 的 輸入, 這也是 設備驅動 。
因此 控制檯 就是 設備驅動 加上一點小小的 控制程序 就能夠啦 。
這就是一個 簡單的 小小 操做系統 了 。
看起來跟 Dos 很像 ?
咱們再來看看 設備驅動 如何實現,
設備驅動 就是 給 設備發送 指令, 以及 和 設備 的 數據傳輸 。
CPU 應該有給 設備 發送指令 的 指令 。 聽說 設備 會被映射成 寄存器 ? 或者一個 內存地址 ?
CPU 和 設備 之間的 通訊 分爲 CPU 操做設備 和 設備通知 CPU 。
CPU 操做設備 很簡單, 就向 設備 發出指令, 該寫數據寫數據, 該讀數據讀數據 就行 。
設備通知 CPU 這個 有點複雜,
好比 鼠標 鍵盤 網卡 , 這些 交互式 的設備, 以及 和外部通訊 的 設備 都會 通知 CPU 。
好比 用戶對 鼠標 移動 按鍵 等, 就會通知 CPU, 網卡 接收到 網絡 傳輸過來的數據,也會通知 CPU ,
由 CPU 對數據作出 處理(響應) 。
這個通知的方式 是 中斷, 即 設備 須要 通知 CPU 時, 發起一個 中斷, CPU 接收到 中斷 會轉入 中斷處理程序, 接下來就能夠對 設備 的數據進行處理 。
中斷 是 CPU 硬件實現的一個機制, 因此效率很高 。
我前段時間看過一篇文章, 說 早期 的 CPU 也是沒有 中斷 的, 那時的 操做系統 是 經過 輪詢 的 方式 來 檢查 設備 是否有數據(通知 (Announce)) ,
看到這裏, 我笑了 。
嚴格來說, 轉入 中斷處理程序 時要保存 當前程序 的 上下文, 因此, 中斷處理程序 是一個 進程, 或者說, 轉入 中斷處理程序 是 跨進程 的 。
而 在 執行 中斷處理程序 的 過程當中, 若是 又發生了中斷 , 怎麼辦 ?
好像能夠 嵌套 執行 中斷, 就好像 函數嵌套 同樣, 新的 中斷 發生, 就 轉入 新中斷 的 處理程序, 處理完之後, 再回到 原來的 中斷 處理程序 繼續執行 。
還有就是忽略 中斷 中的 中斷, 這大概是 級別 比較高的 系統核心 中斷 會這麼作。
也許還能夠有 中斷 排隊 。
固然 這些 就是 操做系統 要處理 的 邏輯 。
進程調度,
現代操做系統 都是 多進程 多線程 的 架構 。
有的文章說 Linux 裏的 線程 是 小進程, 有的文章說 Windows 裏是以 線程 爲 調度單位 。
無論 小進程 仍是 線程, 咱們 以 線程 來看好了 。
咱們這樣來設計 :
系統的 調度 單元 是 線程, 一個進程能夠包含多個線程, 最少會有一個線程 。
進程的 動態性 由 線程 來表現, 進程 做爲一個 靜態的 資源邊界 。
這跟 Windows 比較像吧 ?
由於 各個廠商 各個型號 的 設備 的 操做方式 不一樣, 因此 操做系統 定義一個規範, 能夠由 廠商 和 開發者 本身編寫 設備驅動程序, 來支持 設備 。
操做系統 只要 和 設備驅動程序 交互就行 。
而 設備驅動程序 的 規範中 一個 重要的部分 就是 上述 的 中斷原理 。
當 CPU 接收到 設備發出的 中斷後, 轉入 中斷處理程序, 但並不須要在 中斷處理程序 中 進行 具體的 處理邏輯, 中斷處理程序 只須要將 負責具體處理邏輯 的 驅動程序 線程 加入 就緒隊列 就能夠, 這樣 驅動程序線程 很快就能夠執行, 進行具體的處理了 。 驅動程序線程 平時 是 掛起(Suspend) 的 狀態 。
進程做爲一個 靜態邊界, 主要就是內存裏的 數據段 代碼段 , 廣義的說, 還有 線程池 等等 資源 。
線程共用 的 堆 和 每一個線程各自 的 棧, 應該都是在 數據段 裏吧 ~~ ?
那麼 如何來 調度 進程(線程) 呢 ?
我以爲 平均主義 最簡單,
對於 就緒隊列 裏的 線程, 每一個分配 1000 納秒的 時間片, 這樣 輪流執行, 這樣, 1 秒鐘 能夠 執行 100萬 個 線程 , 固然 每一個線程 只能 分到 1 個 時間片 。
若是是 1萬 個線程, 那麼 每一個線程 能夠分到 100 個 時間片, 累計時間是 100 微秒 = 0.1 毫秒 。
若是是 1千 個線程, 那麼 每一個線程 能夠分到 1000 個 時間片, 累計時間是 1000 微秒 = 1 毫秒 。
若是是 100 個線程, 那麼 每一個線程 能夠分到 1 萬 個 時間片,累計時間是 1萬 微秒 = 10 毫秒 。
固然 這是 理論上的, 並無 把 線程切換 等的 時間花費 算進去 。
你們會問, 對於 不怎麼運行的 線程, 平均分配 會不會 被 不怎麼運行 的 線程 佔用比較多的 時間片, 形成浪費 ?
這是由於 Windows (Linux ?) 有一個 「搶佔式多任務」 的 概念 吧, 意思就是 對於 使用時間片 越多 的 線程 就 分配 更多 的 時間片 給它 。
但我以爲這個問題不存在,
不運行的 線程 就 掛起嘛 , 無論 是 Sleep, 仍是 掛起, Sleep 也是一種 掛起 。
掛起了 就 不佔用 時間片 了 ,因此不存在 浪費 一說 。
對於在 就緒隊列 中的 線程, 均等的給予時間片, 保證 實時響應性 。
有一個基本的問題是, 應用程序進程 在 運行時是 佔用了 CPU 的, 那麼, 由誰來調度進程 ? 應用程序進程 怎麼 切換到 其它進程 ?
仍是用上面說的 中斷 的 方法 。
操做系統 會 在 CPU 裏 設置一個 中斷, 咱們能夠稱之爲 「系統中斷」, 能夠設定爲每隔一個時間片(好比 1000 納秒) 發起一次 中斷,
這是 CPU 本身發出的 中斷,
中斷後, 轉入 系統中斷處理 程序, 即 系統中斷 進程,
在 系統中斷進程 裏, 能夠進行 進程調度, 根據 調度算法, 系統中斷進程 將 CPU 交給 下一個 等待執行的 進程 。
在 Windows 的 任務管理器 裏, 能夠看到一個 「系統中斷」 的 進程, 也許就是咱們上面說的 系統中斷進程 吧 ~ !
在 系統比較繁忙, 好比 開了比較多的 程序 時, 會看到 任務管理器 裏的 「系統中斷」 進程 會佔用 比較多的 CPU, 多是 忙於 虛擬內存 的 頁 載入載出,
若是是這樣的話, Windows 裏的 「系統中斷」 還包含了 虛擬內存 的 功能 。
接下來講說 虛擬內存,
虛擬內存 裏, 頁 的大小(Size) 是一個 關鍵 的 參數 。
頁 太大了 很差, 頁 過小了 也很差 。
我提議用 線性表 做爲頁表, 假設有 1M 個 頁表項, 每一個 頁 的大小(Size)是 1M , 這樣 虛擬內存 空間能夠達到 1M * 1M = 1T ,
如何 ?
頁表項 的 內容是 1 當前頁 是在 物理內存 仍是 在 磁盤頁文件, 2 若是在 物理內存, 頁 的 物理內存地址, 若是在 磁盤頁文件, 頁 在 頁文件 裏的 地址(Position) 。
1T 的 地址空間 大概是用 40位 的 地址 能夠表示, 再加上用 一個位 表示 在 物理內存 仍是 磁盤頁文件, 頁表項 能夠用 41 位 來表示,
咱們能夠放寬一點,用 64 位(8 個字節) 來表示,
這樣, 1M 個 頁表項 就佔用 1M * 8 = 8M 的空間, 或者說, 頁表 須要佔用 8M 的空間 。
也就是說, 8M 的 頁表 能夠管理 1T 的 虛擬內存 空間 。
線性表 的 優勢 是 查找快 。
實際上 頁表項 還能夠再小一點, 由於 頁的大小 是固定的, 因此咱們能夠用 編號 來表示 頁在 物理內存 和 磁盤頁文件 中的位置 。
好比
編號 * 1M = 頁在 磁盤頁文件 中的 位置,
編號 * 1M + 起始地址 = 頁在 物理內存 中的位置, 起始地址 是 物理內存 開始用來存儲 頁 的 地址
這樣 頁表項 只要有 21 位 就能夠了, 20 位 表示 1M 範圍內的 編號, 1 位 表示 頁 在 物理內存 仍是 磁盤頁文件 。
可是這樣 須要多一個計算的過程,就是 上面說的,
編號 * 1M = 頁在 磁盤頁文件 中的 位置,
編號 * 1M + 起始地址 = 頁在 物理內存 中的位置, 起始地址 是 物理內存 開始用來存儲 頁 的 地址
要 多一次 計算 才能知道 頁在 磁盤頁文件 或者 頁在 物理內存 中的位置 。
虛擬內存地址 換算成 物理內存地址 的 算法 是, 虛擬地址 / 除以 頁的大小(Size) , 商 = 頁的序號, 餘數 = 地址在 頁 裏的 偏移量 。
根據 頁的序號 在 頁表 中 查找 頁表項,
由於 頁表 是 線性表, 因此根據 頁的序號 在 頁表中 查找 頁表項 至關於 查找 數組 。
找到 頁表項 後, 能夠知道 頁 在 物理內存 仍是 磁盤頁文件,
若是在 物理內存, 則能夠知道 頁的 物理地址, 頁的物理地址 + 地址在 頁 裏的 偏移量 = 虛擬地址的換算結果
虛擬地址的換算結果 就是 虛擬地址 對應的 物理地址 。
若是 頁 在 磁盤頁文件, 則須要 將 頁 加載 到 物理內存, 再根據上述算法 將 虛擬地址 轉換成 物理地址 。
由於 物理內存 空間有限, 因此 將 頁 從 磁盤頁文件 載入 物理內存 的 同時, 也會將 頁 從 物理內存 移除, 載入 磁盤頁文件 。
因此 就存在一個 「命中算法」, 優先載入哪些頁, 優先載出哪些頁, 使得效率更高 。
固然 經常使用的留下, 不經常使用的載出, 這大概是大原則 。
命中算法 其實 隨便怎麼玩均可以, 不是大問題 。
如今的 虛擬內存 的 地址轉換 是在 CPU 的 存儲管理部件 中完成的, 也就是硬件完成的, 操做系統 只要設置好 頁表 就好 。
我想, 早期 的 虛擬內存 應該是由 操做系統 提供一個 地址轉換 的 原語,
編譯器 在編譯的時候, 對 每次 尋址操做 , 都編譯成 先調用 地址轉換原語, 將 虛擬地址 轉換成 物理地址, 再 用 物理地址 執行 具體操做 。
這是 軟件方式 實現的 虛擬地址 轉換, 固然 比起 硬件實現 的 方式, 效率比較低 。
這種方式 可能主要 存在於 早期 的 實驗室 裏 。
文件系統 是 線性表 + 鏈表 的 經典案例 。
文件 是 連續的 順序的, 因此, 在 磁盤上,咱們也會 連續的 順序的 來 存儲文件 。
但若是 1M 的文件, 磁盤上有 500K 和 600K 這樣 2 個不連續 的 空閒空間, 那要怎麼存儲 ?
固然是把 文件 分爲 2 部分, 每部分 500K, 部分 1 存 500K 的 空閒空間, 部分 2 存 600K 的 空閒空間 。
在 部分 1 的 末尾, 會保存一個指針, 指向 部分 2 的 起始地址 。
以此類推, 文件 在 磁盤上的 物理拓撲 是, 一個用 鏈表 方式 鏈接起來的 多個線性表 。
這也是 磁盤使用 一段時間後 「磁盤碎片增多, 讀寫效率變低」 的 緣由 。
這一點在 機械硬盤 上 尤其明顯 。
這是 文件 的 存儲 。
文件系統 還包含 文件 和 目錄 表, 用於(根據名字)檢索 文件 和 目錄 。
文件目錄表 一般會在 磁盤 的 開頭 劃定一塊 固定區域 來 保存 。
文件目錄表 的 格式 和 這塊 固定區域 的 大小 決定了 文件目錄表 最多能 管理 多少個 文件 。
這也是一般咱們會看到 「xx 文件系統 最多 支持 yy 個文件 , zz 個 目錄」 的緣由 吧 !
文件目錄表 會採用 索引 來 檢索 文件 和 目錄 。
索引 的 特色 是 檢索 的 時間花費 與 文件(目錄) 數量無關, 只與 文件(目錄) 名字長度 有關 。
這也是 Dos 只支持 8個英文字符 的 文件(目錄)名, 而 Windows 支持 很長的 文件(目錄)名 的 緣由吧 。
有關 索引, 我在 《我發起了一個 .Net 開源數據庫項目 SqlNet》 http://www.javashuo.com/article/p-gpuysmcv-q.html 一文中有論述 。
網絡通訊 的 基礎 是 網卡驅動, 網卡驅動 也是 設備驅動, 設備驅動 的 部分在上文 簡單的 說了 。
網卡驅動 解決了, 網絡通訊 就簡單了,
只要按照 協議 格式 分析數據, 拆包, 將數據 轉發 給應用程序 就能夠了 。
操做系統 應該提供至少一個 編譯器, 好比 C 語言編譯器, 這樣 開發者 能夠在 操做系統 上 編寫程序 。
有關 編譯器, 請參考我寫的另外一篇文章 《漫談編譯原理》 http://www.javashuo.com/article/p-fhvkvvhp-hk.html
計算機技術發展到如今, 也是 卷帙浩繁, 是個 大工程 。
不過 從 工程學 的角度來看, 也不復雜,
咱們能夠蓋一座大樓, 就能蓋兩座大樓, 能蓋兩座大樓, 就能蓋三座大樓, 能蓋三座大樓, 就能蓋四座大樓, ……
蓋十座大樓也是能夠的嘛 。