作者簡介:
程磊,某手機大廠系統開發工程師,閱碼場榮譽總編輯,最大的愛好是鉆研Linux內核基本原理。
目錄:
一、計算機簡介
1.1 什么是計算機
1.2 計算機發展史
1.3 計算機的二元結構
二、計算機硬件體系結構
2.1 圖靈機模型
2.2 馮諾依曼結構
2.3 現代計算機結構
三、計算機軟件體系結構
3.1 系統軟件
3.2 應用軟件
四、操作系統組成結構
4.1 內核
4.2 OS庫
4.3 OS進程
五、操作系統本質解析
5.1 操作系統的目的
5.2 操作系統的作用之一
5.3 操作系統的作用之二
六、計算機運行模型
6.1 CPU運行模型
6.2 進程調度模型
6.3 軟件體系結構
一、計算機簡介
在講解操作系統之前,我們先從整體上講一下計算機,再從硬件講到軟件,最后再講操作系統。
1.1 什么是計算機
我們生活中幾乎到處都能接觸到計算機,從我們日常使用的手機、平板,到辦公使用的筆記本、臺式機,到銀行的ATM機,到各處可見的監控設備,還有我們平時看不見但是我們瀏覽的網頁其所在的服務器,還有微信、抖音等我們日常所用的APP它們所在的服務器,等等,這些都是計算機。如果沒有了計算機,我們的生活將難以想象。那么究竟什么是計算機呢,這個還真不好下定義的,那我們就來看一下百度百科對計算機的定義:計算機俗稱電腦,是現代一種用于高速計算的電子計算機器,可以進行數值計算,又可以進行邏輯計算,還具有存儲記憶功能。是能夠按照程序運行,自動、高速處理海量數據的現代化智能電子設備。計算機的應用非常廣泛,從我們日常最常見的臺式機、筆記本到手機平板都是計算機,而且大到服務器、超級計算機,小到各種嵌入式設備也都是計算機。現在我們對計算機既有了感性的認識,又知道了的它的權威定義,那么計算機是怎么產生的呢,下面我們來看一看計算機的發展史。
1.2 計算機發展史
計算機的發展史從概念上最早可以追溯到結繩計數、手指頭計數,有了計數然后我們就有了加減乘除各種計算需求。但是人類天然的計算能力不太行,于是便開始創造工具、使用工具進行計算。最古老的計算工具當數中國的算盤了,算盤運用熟練的話可以使加減運算的速度大大加快。后來歐洲人發明了計算尺,通過對數方式把乘除運算轉換為加減運算,使得乘除的計算速度有了很大的飛躍。歐洲在文藝復興和科學革命之后,很多科學家都想發明一種通過齒輪和力學原理來驅動的機械計算機,最出名的當數帕斯卡發明的加法器了,可以進行6位數的加減運算。與牛頓同時期的著名數學家、哲學家萊布尼茨在帕斯卡之后發明了乘法器,可以進行加減乘除開方運算。萊布尼茨的理想遠不止于此,他一直想把邏輯思維轉化為數學運算,并且用機器實現邏輯運算。雖然在當時沒有實現,但是為后來的計算機發展提供了很大的啟發。據說萊布尼茨看了中國的易經八卦之后從中發現了二進制,并認為以后的計算機可能會采取二進制來實現。
19世紀早期的英國科學家巴貝奇發明了差分機,差分機可以進行一些非常復雜的數學運算,如多項式計算、求解三角函數等。后來他又設計出了分析機,但是由于分析機過于復雜,在當時并沒有制造出來。雖然分析機沒有造出來,但是它的意義卻是非常巨大的。它的內部有存儲部分,有碾磨(相當于現在的CPU),還有輸入輸出部分,已經可以看到現代計算機的影子了。而且巴貝奇是第一個意識到計算機是需要編程的人,在此之前人們一直覺得計算機如果造出來了,它就可以做任何它能做的事情,并不需要人類為它編程,當然之前的人也意識不到編程是什么意思。英國著名詩人拜倫的女兒Ada,自幼對數學興趣濃厚,與科學家交往甚密。后來在其它人的介紹下認識了巴貝奇,兩人很快就成為了亦師亦友的關系。雖然分析機并沒有造出來,但是Ada還是為分析機寫了很多程序,所以被后世稱為歷史上第一個程序員。估計絕大多數人都想不到世界上第一個程序員竟然是位女程序員。后來美國國防部設計了一種計算機語言,用的就是Ada的名字來命名的。
然后計算機從機械時代到了機電時代,很快又到了電子時代。1946年2月14日,被認為是世界上第一臺電子計算機的ENIAC誕生了,之后計算機的發展就走上了快車道,從電子管到晶體管到集成電路到大規模集成電路,一直發展至今。這一段歷史比較清晰,網上的資料也很多,而且這段歷史中計算機的邏輯結構并沒有發生多大的變化,所以我們就不展開細說了。
1.3 計算機的二元結構
計算機是由硬件和軟件組成的,就像人是由肉體和靈魂組成的一樣。計算機的各種硬件就像是人的大腦、心肝脾肺腎、骨骼、皮膚一樣,支撐著人活著。但是人要是只有肉體沒有靈魂,那就是植物人,電腦要是只有硬件沒有軟件,那就是一堆金屬和塑料,沒有用處。下面兩章我們分別講一下計算機的硬件體系結構和軟件體系結構。
二、計算機硬件體系結構
我們按照從簡單到復雜、從抽象到具體的過程來講一講計算機的硬件體系結構。
2.1 圖靈機模型
什么是圖靈機,我們為什么要講圖靈機?圖靈機是計算機的數學模型,我們在這里講圖靈機并不是為了學習圖靈機本身,圖靈機的理論還是很復雜的。我們這里講圖靈機就是為了理解一下計算機的運行模型,明白CPU的運行是線性的。我們先來看一下圖靈機的定義,圖靈機是英國數學家阿蘭·圖靈于1936年提出的一種抽象的計算機模型,即將人們使用紙筆進行數學運算的過程進行抽象,由一個虛擬的機器替代人類進行數學運算。它有一條無限長的紙帶,紙帶分成了一個一個的小方格,每個方格有不同的內容。有一個機器頭在紙帶上移來移去。機器頭有一組內部狀態,還有一些固定的程序。在每個時刻,機器頭都要從當前紙帶上讀入一個方格信息,然后結合自己的內部狀態查找程序表,根據程序把信息輸出到紙帶方格上,并轉換自己的內部狀態,然后進行移動。我們可以看出圖靈機的定義還是很簡單的,圖靈機的運行模式和我們現在的CPU運行模式還是很像的,我們下面畫一個圖靈機的運行模型。
從圖中我們可以看到圖靈機模型確實非常簡單,有一個讀寫磁頭,磁頭有自己的內部狀態,磁頭根據自己的內部狀態會讀或者寫當前方格里面的內容,然后相應地改變自己的狀態,然后向左或者向右移動一格或者若干格繼續執行。圖靈機本身的理論非常復雜,想學習的同學可以去搜索一下或者查閱相關的書籍。我們在這里想要說的就一點,圖靈機的運行是線性的,線性的意思不是說它是單向直線性的,而是說它只能在一條線上運行,而且所有運行的點按時間記錄排序是線性的。
2.2 馮諾依曼結構
馮諾依曼結構是計算機的物理模型,是計算機能夠制造出來的關鍵。馮諾依曼結構由三部分組成:一是計算機硬件實現上采用二進制;二是存儲程序設計,就是把計算機程序放到存儲器中,關于這一部分的論述就參看《深入理解編譯體系》的第一章;三是計算機由控制器、運算器、存儲器、輸入設備、輸出設備五個部分組成。控制器是整個計算機的神經中樞,控制著整個計算機的運轉。控制器從存儲器中取指令,解析指令,然后根據指令的內容讓運算器進行相應的運算或者向其它部件發出一些命令。運算器是負責進行數學運算和邏輯運算的部件,數學運算包括加減乘除移位比較等運算,邏輯運算包括與、或、非、異或等運算。存儲器是存儲程序和數據的地方。輸入設備和輸出設備是負責和人交互的設備,人們通過輸入設備向計算機輸入命令和數據,通過輸出設備得到計算機運算出來的結果。下面我們畫一張馮諾依曼結構的圖。
可以看到馮諾依曼結構比圖靈機模型復雜了不少,但是總體上還是很簡單的。有了馮諾依曼結構,計算機就有了被造出來的物理模型,我們也可以從馮諾依曼結構中更好地去理解計算機的邏輯結構。
2.3 現代計算機結構
其實并沒有現代計算機結構這個術語,現代計算機結構是我把馮諾依曼結構再具體化一下,把具體計算機結構再抽象化一下總結出來的,可以讓大家更方便更深入地理解計算機硬件體系結構。現代計算機和馮諾依曼結構最主要的區別是把控制器和運算器合二為一稱作CPU,把存儲器一分為二,分為內存和外存。我們來畫一下現代計算機結構的圖。
現代計算機的具體體系結構非常復雜,我只是撿其中比較關鍵的畫了出來,很多細節沒有畫,如總線、南橋、北橋、各種控制器等,上圖中也有很多連線沒有畫。計算機運行的時候先從外存中加載程序到內存,然后CPU不斷地執行內存中的指令、讀寫內存數據,CPU也會使內存繼續從外存中讀取數據或者把內存的數據更新到外存。CPU通過IO端口或者內存映射IO來控制外設獲取外設的狀態或者數據,外設通過中斷控制器(APIC)向CPU通知報告一些事情。計算機就是這樣不斷運行的。
推薦閱讀:
想要深入學習計算機體系結構的話,推薦閱讀《Structured Computer Organization》《Computer Architecture A Quantitative Approach》《Computer Organization and Design》《Computer Systems - A Programmer’s Perspective》《Code The Hidden Language of Computer Hardware and Software》《Computer Organization and Architecture Designing for Performance》
想要學習CPU設計運行原理的話,推薦閱讀《Modern Processor Design》《Microprocessor Architecture》《Processor Microarchitecture An Implementation Perspective》《Chip Multiprocessor Architecture Techniques to Improve Throughput and Latency》《CPU自制入門》《自己動手寫CPU》《大話處理器》。
想要了解具體CPU的運行原理的話可以閱讀CPU廠商的CPU手冊,x86 CPU:《Intel 64 and IA-32 Architectures Software Developer’s Manual》,一共4卷10冊,總共4800多頁,下載地址:https://software.intel.com/en-us/articles/intel-sdm。ARM:《Arm Architecture Reference Manual for A-profile architecture》,只有一個整體本,不分卷分冊,總共11000多頁,下載地址:https://developer.arm.com/Architectures/A-Profile%20Architecture。RISC-V:《The RISC-V Instruction Set Manual Volume I User-Level ISA》《The RISC-V Instruction Set Manual Volume I I Priv ileged Architecture》,分別230,250多頁,下載地址:https://riscv.org/technical/specifications。
三、計算機軟件體系結構
從上面我們知道了硬件的整體結構和運行情況,下面我們來說一說計算機的軟件體系結構。計算機軟件整體上分為系統軟件和應用軟件,系統軟件是面向硬件和面向程序員的軟件,應用軟件是面向普通用戶的軟件。
3.1 系統軟件
最重要的系統軟件是操作系統,操作系統是管理所有硬件并為進程提供運行環境的軟件,所有的應用程序都要運行在操作系統之上,操作系統為應用程序提供服務,并管理所有的應用程序。關于操作系統的具體情況,請看第四章和第五章。
在操作系統啟動之前還有兩個軟件,它們是Boot Firmware 和 BootLoader,Boot Firmware的存在有一個原因、兩個作用:一個原因是為了克服計算機啟動時雞生蛋、蛋生雞的矛盾,按照馮諾依曼模型,計算機啟動時要從內存讀取程序運行程序,而計算機啟動時內存是空的啥都沒有,要想讓內存有程序就必須要從外存中加載程序,而從外存中加載程序的指令也是程序,這個程序也要先加載到內存,怎么辦呢,我們提前寫好一段程序,把它固化到ROM中去,ROM是掉電不會丟失內容的,再把這個ROM配置到特定的物理內存地址空間中,具體地址是什么要看CPU廠商了。CPU啟動時會從特定的內存地址運行,也就是這個ROM所在地址,這樣CPU就有程序可以執行了。Boot Firmware的兩個作用分別是初始化硬件檢測硬件和加載操作系統為操作系統提供一些基本的硬件信息。后來為了靈活性,Boot Firmware并不是直接加載的操作系統,而是先加載BootLoader再由BooLoader加載的操作系統。BootLoader的存在一是可以很方便配置一些信息如OS啟動參數,二是支持多操作系統啟動。桌面計算機常見的Boot Firmware是BIOS和UEFI,BIOS是PC最初的Boot Firmware,由于推出的時間比較早,所以存在很多問題,后來Intel又重新設計開發了UEFI,UEFI相比BIOS由很多的優點。現在基本上大部分桌面計算機都已經轉向了UEFI。比較常見的Boot Loader如LILO、GRUB,現在桌面版Linux用的基本都是GRUB。
嵌入式系統的概念和這個不太一樣,嵌入式系統最先啟動的兩個軟件叫做Boot ROM和BootLoader,Boot ROM存在的原因和Boot Firmware存在的原因是一樣的,但是Boot ROM并不具有Boot Firmware的功能,Boot ROM是比較輕的,相當于是UEFI/PI 中的SEC階段,Boot Firmware中的其它功能被轉移到BootLoader中來做,Boot ROM一般是由CPU廠商來開發的,不開源。BootLoader負責硬件初始化并直接加載啟動操作系統。造成桌面計算機和嵌入式系統的這種差異的原因是兩者的運行和使用環境大不相同造成的。
作為最基礎的三個系統軟件,Boot Firmware、BootLoader、操作系統,前兩者很少有人熟悉,甚至很多人根本就沒聽說過,是因為它們啟動過去之后基本就不再起作用了。除此之外還有一套很重要的系統軟件,它是編譯工具鏈,包括預處理器、編譯器、匯編器、鏈接器,所有的軟件要想從源碼變成二進制程序都需要它們來處理,想要具體了解它們的情況請參看《深入理解編譯系統》。除此之外還有一些系統查看、配置、維護工具也算是系統軟件,如ps、top、sysctl等。
推薦閱讀:
想要學習操作系統理論的話,推薦閱讀《Modern Operating Systems》《Operating System Concepts》《Operating Systems Design and Implementation》《Operating Systems Three Easy Pieces》《Operating Systems In Depth》《Operating Systems Internals and Design Principles》《Operating Systems A Concept-Based Approach》《現代操作系統》這本是陳海波/夏虞斌著,和第一本的漢譯版重名,并不是同一本書。
想要學習Windows操作系統的話,推薦閱讀《Windows Internals 7th Part 1》和《Windows Internals 7th Part 2》
想要學習Mac OS操作系統的話,推薦閱讀《Mac OS X and iOS Internals》和《Mac OS X Internals A Systems Approach》
3.2 應用軟件
應用軟件就比較好理解了,就是那些面向普通用戶實現某些特定需求的軟件,如 上網-瀏覽器,聽歌-千千靜聽,看視頻-暴風影音,追劇-愛奇藝,社交-微信,購物-淘寶,刷視頻-抖音。所有應用軟件都運行在操作系統之上,享受操作系統的服務,并接受操作系統的管理。
四、操作系統組成結構
我們經常說操作系統,那么究竟什么是操作系統呢,我們先來從操作系統的構成成分的角度來講一講。操作系統是由三部分構成的,其中最重要的是內核部分,但是內核并不是操作系統的全部,除了內核外,OS庫和OS進程也是操作系統的一部分。OS庫是運行在用戶空間進程中的共享庫,雖然它是運行在用戶空間,雖然它只是共享庫,但是它是操作系統的一部分,因為它參與實現了操作系統的一部分功能。OS進程雖然是運行在用戶空間的進程,但是它并不屬于應用程序,因為它是實現操作系統功能必不可少的一部分。
4.1 內核
內核是操作系統最重要的部分,是操作系統的核心,它運行在內核空間,運行在CPU特權模式,直接與硬件打交道,并直接管理著進程的運行。內核可以分為宏內核與微內核,宏內核就是傳統的內核方式,所有的內核功能都放在內核空間里。微內核是把一些最基本的無法移出內核的功能才放在內核里,其它能放到用戶空間的功能全都放到用戶空間進程里,叫做微服務,應用進程通過IPC與微服務溝通,微服務與微內核一起構成一個完成的內核。
微內核辯論:
我對微內核的態度是和Linus是一樣的,下面的話是節選自Linus寫的書《Just For Fun》。
微內核的理論依據是,操作系統是非常復雜的,所以要通過模式化來減少復雜性。微內核方法的原則,即核心的核心,是盡量減少功能。它的主要功能是傳播。電腦所提供的一系列不同的服務都是通過微內核的傳播渠道實現的。因此,應盡量分割問題的空間,使其不再復雜。我認為這種做法很愚蠢。是的,每一個單獨的部分是簡單的,但是相互作用的多種功能如果放在一起就要復雜得多 ,而 Linux 就是后者的情況。想一想自己的大腦。每一個單獨的部分都很簡單,但是各部分的相互作用構成了一個復雜的系統。這是一個整體比個別更大的問題。拿一個問題來說,如果你簡單地將問題一分為二,說半個問題要容易一半,那么你就忽略了一個事實,即:你必須要考慮到兩個半個之間的聯系所帶來的復雜性。微內核的理論是,如果把核分為五十份,那么每一份都只有五十分之一的復雜性。但是每個人都忽視了一個事實,即各部分之間的聯系事實上比源系統更加復雜,而且那些個別部分也不是那么簡單。這是我對微內核最重要的反駁:你想實現的簡單化是錯誤的簡單化。
上面是Linus的分析,我是非常贊同的,內核的復雜是內核的固有屬性,是它內在邏輯的復雜,你把它人為的劃分到不同的用戶空間進程,并沒有減少它內在的邏輯復雜性。把一個事物模塊化能減少它的復雜性,但是模塊化不是說非得要在形式上把它們分割到不同的進程,在邏輯上把它們分割開來就可以了。比如說有一個進程非常復雜,你把它弄成一坨不分模塊是不對的,但是模塊化的形式可以是把它分割成一個exe和n個so就可以了,不一定非要把它做成n+1個進程。n+1個模塊無論是在進程內交互,還是在進程間交互,其本身的復雜性并沒有減少,進程間通信還降低了效率。
微內核還有一個經常宣傳的優點是如果某個微服務出了問題,只重啟這個微服務就可以了,不用重啟整個內核。但是這個優點是禁不起推敲的,以我在安卓系統上的工作經驗為例,如果某個重要的JAVA進程崩潰重啟了比如SystemServer,就會導致很多Java進程包括所有的APK進程都會重啟,雖然內核沒有重啟,但是在用戶看來還是整個系統都重啟了。所以如果某個微服務重啟了,一定會導致所有的應用進程重啟的,在用戶看來還是整個系統重啟了,也許你會說微內核本身并沒有重啟,所以整個重啟時間減少了,但是這點時間減少并沒有多少意義。而微內核帶來的整個系統性能的大幅度下降這個問題,則是無法容忍和無法克服的。
我發現很多嵌入式系統使用的都是微內核。因為嵌入式系統是一個相對封閉的系統,不會有經常下載新程序、突然運行很多進程的情況,所以嵌入式系統對性能的需求是穩定的。所以微內核只要在測試中能滿足嵌入式系統的性能需求,問題就不大。通用操作系統包括手機系統、桌面系統、服務器系統,都有面臨負載突然暴增的情況,所以對性能的要求非常高。
目前所有流行的通用操作系統內核都是宏內核,有些宣稱自己是微內核或者混合內核的,本質上還是宏內核。《Mac OS X Internals A Systems Approach》6.2章節明確說XNU不是微內核,是宏內核,《Mac OS X and iOS Internals》第8章Hybrid Kernels這一節說,Windows、MacOS雖然是混合內核,但是實際上還是宏內核。
4.2 OS庫
有些運行在用戶空間的動態庫,是屬于操作系統的一部分,這些庫雖然是庫,但是并不是由應用程序開發者開發,而是由操作系統廠商開發,因為它們是構成操作系統功能的一部分。在Linux上常見的OS庫有libc.so、ld-linux-x86-64.so、libm.so、libpthread.so、libdl.so、librt.so、libcrypt.so、libpam.so、libX11.so等。Linux上的OS庫是非常多的,上面只是隨便舉幾個例子,下面我們對其中最重要的幾個進行一下分析。
libc.so,libc(C Library)的二進制動態庫形式,它的源碼實現有很多,Linux發行版一般用的是glibc,安卓上的libc的實現叫做bionic。libc不僅生成libc.so,還有libpthread.so、ld-linux-x86-64.so等很多庫都是libc生成的。libc.so里面不僅有C標準庫的實現,還有對系統調用的封裝,所以libc.so不是一個普通的庫,它是內核的對外接口庫,是非常重要的,是進程運行必不可少的。你cat 任何一個進程的 /proc/pid/maps 幾乎一定會看到libc.so,如果看不到,那是因為有些進程是把libc靜態鏈接到自身了,進程里面還是包含libc的。
ld-linux-x86-64.so,是Linux系統的加載器,負載在進程啟動時把進程所依賴的所有動態庫都加載到內存并進行一些重定位工作。沒有了ld-linux-x86-64.so,幾乎所有的應用進程都無法啟動運行。
libpthread.so,Linux的多線程實現庫,想要使用系統的多線程功能的進程必須要調用這個so才行。
4.3 OS進程
并不是所有的操作系統功能都放進了內核里,還有很多功能是放在了用戶進程里來實現的,我們把這些進程叫做OS進程。我總結了一些常見的放在用戶空間的操作系統功能,它們是 init system、package system、window system。這些都被叫做某某系統,可見它們并不是簡單的一個進程而已,它們一般是由一個進程加上一些配置文件,有的還提供一些so,它們有著自己的邏輯體系,下面我們來一一講解一下。
Init system,是操作系統啟動的第一個用戶空間進程,它負責把整個用戶空間給啟動起來,啟動系統所有的守護進程,設置一些系統配置,在系統運行的過程中監管著整個系統,系統的關機也由它負責。最早的init system 叫 sysv init,它的實現比較簡單直接方便,大量使用腳本。后來sysv init 變成了系統啟動速度慢的最大瓶頸,一是因為腳本的運行比較慢,二是因為它是單線程的,沒能發揮出多核的優勢。后面就出現了兩個init system,分別是upstart 和 systemd 來解決sysv init 的問題,現在大部分linux發行版都把init system 換成了systemd。
Package system,一個系統上有那么多的軟件,該如何安裝、如何卸載、如何管理,安裝包該采取什么格式,這便是package system要做的工作。目前Linux上有兩個比較流行的package system,一個是redhat 推出的rpm,一個是debian推出的dpkg。
Window system 也叫做Graphics system,也就是一個操作系統的圖形界面該如何管理,這是一個非常復雜的系統,牽涉面非常廣,為什么是Android呢,因為我在手機廠商工作的時候做過這方面的工作,相對比較熟悉,對Linux的圖形系統不太熟悉。
除了上面三個很大的系統之外,還有一些GUI程序,看起來像是應用程序,但是實際上應該劃到操作系統的內容里面,比如桌面、文件管理器、設置等程序,因為它們是和操作系統緊密相關的,而且一般也是由操作系統廠商來開發和維護的。
五、操作系統本質解析
前面我們對操作系統的組成結構進行了分析,下面我們從操作系統的功能作用的角度再對操作系統進行具體的分析。我們先來思考一個問題,什么是操作系統,為什么要有操作系統?我們通過對操作系統發展歷史的研究以及對Linux內核實現的深入研究發現,操作系統的存在就是為了一個目的,就是為了運行程序,如果再加個形容詞的話,那就是多快好省地運行程序。為了實現這個目的,操作系統提供了兩個作用:1.操作系統是一個更加高級的抽象計算機,2.操作系統是計算機資源管理器。下面我們對一個目的、兩個作用展開討論。
5.1 操作系統的目的
操作系統存在的目的就是為了運行程序。我們還可以用反證法來論證這個結論,假設操作系統不能用來運行程序,那么操作系統要它還有什么用呢。所以操作系統存在的目的就是為了運行程序,為了多快好省地運行程序,多就是多任務,快就是效率高,好就是安全穩定,省就是節省資源,為了實現這個目的,操作系統做了非常大的努力。
既然是為了運行程序,那么我們要問的第一個問題就是程序是怎么來的呢。首先程序是用計算機編程語言編寫的。編寫程序的工具是什么呢,是文本編輯器。編寫的原則是什么呢,隨便怎么寫都可以嗎,不是的,首先要符合所使用語言的語法才行。其次我們對程序還有幾個要求,其中首先最重要的要求就是正確性,程序運行結果不正確可不行,除了編程時我們小心翼翼盡量保證程序邏輯正確外,我們在運行的時候發現程序不正確時還要用調試工具GDB來調試程序,找到程序的錯誤改正它。對于程序的內存錯誤,我們還可以使用asan、malloc-debug、valgrind等工具來排查錯誤。再者我們寫的程序還要簡潔清晰、可讀性強、可維護性強,為了達到這個目的,我們要學習很多編程的藝術,在此給大家推薦幾本書:《Agile Software Development》《Clean Code》《The Clean Coder》《Clean Architecture》《Code Complete》《Design Patterns》《Refactoring Improving the Design of Existing Code》。其中大家在很多博客上經常看到的一些編程原則如開閉原則、單一責任原則、依賴倒置原則等都是出自第一本書,漢譯書名叫《敏捷軟件開發》。再者我們寫的程序還要具有高效性,程序效率低運行慢可不行,那么怎么讓我們寫的程序比較高效呢,為此我們要學習數據結構與算法,只有把數據結構與算法學透了,我們才能把我們學到的方法和我們的需求結合起來,寫出來運行又快占用空間又小的程序。為此我向大家推薦幾本書:《Introduction to Algorithms》《Algorithm Design and Applications》《Algorithms in a Nutshell》《Data Structures and Algorithms in Java》《Algorithms》《An Introduction to the Analysis of Algorithms》。如果是已經寫好的程序嫌它運行慢應該怎么辦呢,我們有性能剖析工具gprof,它能幫你找到熱點代碼,然后你就可以進行定向優化,必要時還可以使用匯編語言實現一些函數來提高效率。
當我們把源代碼都寫好了之后,程序就可以運行了嗎,不行,CPU不認識源代碼,只認識二進制程序,所以我們還需要把源代碼編譯、鏈接成二進制程序才行。二進制程序有沒有格式呢,隨便怎么生成都行嗎,不行,不同的操作系統都有自己特定的二進制程序格式。Linux使用的二進制程序格式叫做ELF格式,ELF是一個總體格式,在不同的操作系統上、不同的CPU架構上又有一些具體細化的要求,如都有哪些段,函數怎么傳遞參數等,這些統稱為ABI,應用程序二進制接口。源碼生成的二進制程序可以分為exe主程序和so動態庫程序,主程序可以直接執行,庫程序不能直接執行,只能在別人的進程里被調用執行。程序運行起來就是進程,一個進程由一個exe主程序和n個so庫程序構成。
還有一個概念叫做IDE,集成開發環境,它是一個圖形界面程序,里面集成了文件編輯器、調試器、編譯器、鏈接器、Build工具等,可以在一個程序里完成一系列開發操作。
5.2 操作系統的作用之一
操作系統為了運行程序提供了哪些作用呢,第一個就是操作系統是一個更加高級的抽象計算機。怎么理解這句話呢,我們從計算機發展的歷史來講解。最開始的時候是沒有操作系統的,只有應用程序,應用程序的開發要直接面向硬件開發,因為沒有人給你提供API。比如說你要讀寫一個文件,你該怎么辦,要是擱現在那是很簡單的,你只需要open(file path),read(fd),write(fd)就可以了。但是在當時讀寫卻是非常復雜的,你需要了解磁盤的型號,知道磁盤的端口,知道磁盤的各個技術參數如磁盤有多少個盤面柱面扇區,你要自己編程管理每一個扇區,記錄哪一個扇區用過了哪一個扇區沒用過,想想都頭疼,都想放棄。現在好了,現在有操作系統幫你做這些事情,你只需要按照操作系統給你的API去做就行了,那是十分的方便啊。操作系統向應用程序提供的功能,從底層實現的角度來看叫做系統調用,從上層應用的角度來看叫做API,系統調用和API看起來很像又不太一樣,但是它倆又有很強的關聯。
5.3 操作系統的作用之二
操作系統的第一個作用是從單個進程的角度來看的,操作系統是一個更加高級的抽象計算機。現在我們從多個進程的角度來看,又發現操作系統其實是個計算機資源管理器。這和操作系統的標準定義又很契合,我們來看一下操作系統的標準定義:操作系統是管理計算機硬件與軟件資源的計算機程序。操作系統需要處理如管理與配置內存、決定系統資源供需的優先次序、控制輸入設備與輸出設備、操作網絡與管理文件系統等基本事務。操作系統也提供一個讓用戶與系統交互的操作界面。
既然操作系統是計算機資源管理器,那么我們先來看一看計算機都有哪些資源呢?從第二章我們可以看出,計算機最主要的資源有CPU、內存、外存、外設,因此我們可以很輕易地看出操作系統的四個必要組成部分分別是CPU管理、內存管理、外存管理、設備管理。CPU管理就是CPU時間管理就是進程調度。內存管理是對內存資源進行管理,包括物理內存的管理和虛擬內存的管理。外存管理包括文件系統和磁盤驅動兩部分。設備管理是最復雜的,一般都是由內核提供基礎代碼和驅動模型,由各個硬件廠商具體去寫自家設備的驅動程序,系統還會制定用戶空間接口,提供一些基礎操作庫。由于Linux支持的設備眾多,因此設備驅動代碼是Linux里面代碼最多的部分,占Linux總代碼的比例高達60%多。
操作系統管理計算機資源總體上有四種模式,分別是:1時間分割管理,2空間分割管理,3獨占性管理,4直接管理。我們把資源分為共享性資源和非共享性資源。共享不共享是相對于進程來說的,有些資源直接對整個系統起作用,對進程不可用,我們把系統對這種資源的管理方法叫做直接管理,例如中斷控制器。對于共享性資源,我們要根據資源自身的特點采取相應的管理方式。資源的特點包括時間分割相似性和空間分割相似性。時間分割相似性是指在一個較少的時間片上資源仍然是可用的。空間分割相似性是指把資源分割成若干份,每一個仍然能達到資源本身的作用。對于具有時間分割相似性的資源我們采取時間分割管理,對于具有空間分割相似性的資源我們采取空間分割管理,如果兩者都不具有我們采取獨占性管理。
CPU管理:
對CPU應該怎么管理呢,我們來思考一下。我們能不能采用空間分割管理呢?試一下,如果用空間分割管理,那么就是把一個CPU分割為n個部分,每個部分都是一個獨立的執行單元,n個進程各占一個各運行各的,好像也行,但是根據我們在第二章所學的知識,CPU做不到啊。如果用時間分割管理呢,把時間分成一片一片的,輪流分給每個進程來使用,由于CPU是一條指令一條指令地執行,不同進程之間的指令是沒有依賴關系的,所以是可以的。所以我們對CPU采取的管理方式是時間分割管理。對CPU進行時間分割管理就是進程調度。
進程調度需要統計進程的運行時間,被動調度需要有定時器的支持,文件系統需要給文件打上時間戳,用戶空間也有查看時間、修改時間、設置定時器的需求。
內存管理:
我們再來看一下對內存應該使用什么管理方式。內存能不能進行時間分割管理呢?能,把時間分成一片一片的,每個時間片就只有一個進程就能獨享整個內存。顯然這么做很不利于充分使用內存,太浪費了,違背了多快好省地運行進程的原則。那么能不能對內存進行空間分割管理呢?能,每個進程占用一小塊內存進行運行,這樣能充分利用整個內存。這么做有沒有什么缺點呢?有,內核和各個進程都在物理內存里,大家都可以相互訪問甚至破壞別人的內存,顯然是不安全的。對此有什么辦法呢,最先想到的也是最直觀的方法就是分段機制,修改CPU的運行原理,實現分段機制,內核、每個進程各自運行在各自的段里面。由于是在CPU硬件上實現的限制,誰也無法訪問自己段外面的內存,這樣就實現了進程隔離,非常安全了。但是這么做有沒有缺點呢?有,物理內存本來就少,分完段之后物理空間就更小了,而且還有一個缺點就是程序啟動的時候就要把程序全部加載到內存才行,不然運行到沒有加載的程序就崩潰了。對此有什么解決辦法嗎,最剛開始想的辦法是軟件的,每個程序要自己要寫個overlay manager,程序員要對自己程序的執行流程有清晰的認識,自己控制何時把哪部分即將會用到的程序加載到內存,何時將哪部分暫時不會使用的程序再放回外存,這樣程序就可以使用較少的內存也能順利運行了。但是這個做法非常麻煩,對程序員的考驗非常大,要解決這個問題還是得使用硬件機制才行。于是后來就產生了虛擬內存/分頁機制,每個進程都運行在虛擬內存上,并且獨占整個虛擬內存空間,這樣就實現了進程隔離,非常安全,同時進程并不立即分配物理內存,而是程序在運行的過程中產生page fault 按需調頁,用到的時候再去分配物理內存,這樣就可以節省物理內存。所以Linux對內存的管理方式是在形式上每個進程都獨占整個虛擬內存空間,在實際上進程之間對物理內存進行空間分割管理,而且是延遲分配,用到的才分配,用不到就不分配,又節省了物理內存,分頁機制真是太完美了。
外存管理:
顯然,外存也是采用空間分割管理的。
外設管理:
有些設備比如相機,既不具有時間分割相似性,也不具有空間分割相似性,只能對其進行獨占性管理。我們可以來假設一下,如果對相機進行時間分割管理的話,比如說A進程用了0.5秒拍照拍了一半,B進程又用了0.5秒拍照拍了一半,然后A進程又用了0.5秒把整個照片拍完了,接著B進程又用0.5秒把整個照片拍完了,顯然這是不可能的,因為相機做不到。我們再假設對相機進行空間分割管理,比如說這個相機拍照的分辨率是1000×800的,A、B兩個進程同時用相機,分別拍出了一個1000×400的照片,這也是不可能的,相機做不到。所以對相機只能采取獨占性管理,每個進程在使用相機前都要先申請,使用完后再釋放,使用期間進程對相機是獨占的。
計算機中除了CPU、內存、主板、總線等之外的設備都叫做外設,內核對外設的管理叫做設備管理,也是操作系統中非常重要的一塊。外設的運行需要驅動程序,內核一般會提供總線的驅動和一些設備類型的框架代碼,然后由各個硬件廠商去編寫具體的驅動程序。想學習Linux驅動編程的話,推薦閱讀《Linux Device Drivers》《Essential Linux Device Drivers》《Linux設備驅動開發詳解》。
有些設備,并不是單單有個驅動就能獨立運行了,而是與用戶空間庫和進程一起構成了一個新的系統,比如網卡和顯示器。網卡需要網卡驅動,但是只有網卡驅動,網卡還是沒有用,網卡與網卡驅動還有網絡協議還有一些相關的硬件軟件共同構成了網絡系統。再說顯示器,光有個顯示驅動也是沒有意義的,顯示器只是計算機圖形系統中的一個組成部分。計算機圖形系統還包括GPU、渲染庫、窗口系統等很多組件。
中斷機制:
計算機中外設與CPU的交互方式,除了CPU可以控制外設之外,外設也可以主動向CPU報告事情,避免了CPU輪詢外設的低效行為,這種方式就叫做中斷。中斷除了這個作用之外還有其它一些作用,比如可以用來處理CPU異常、用來實現系統調用,定時器中斷還可以用來實現搶占式多任務。所以中斷機制對操作系統來說是非常重要的,重要性就相當于人的神經系統加呼吸系統一樣重要。
編程設施:
計算機除了管理硬件之外,還為軟件提供了兩個編程設施,分別是線程同步和進程間通信。線程同步是因為多個執行流(線程)同時訪問公共數據引起的,如果不采取一些措施的話就會產生程序錯誤,因此操作系統提供了線程同步這個編程設施來解決這個問題。線程同步中有一個非常常用的方法叫做自旋鎖。
進程間通信是因為有了虛擬內存/分頁機制之后產生了進程隔離,進程之間無法直接訪問對方,所以需要通過內核提供的進程間通信機制來進行溝通,具體內容請參看《深入理解Linux進程間通信》。Linux還有一個重要的機制叫做信號機制(signal),它和IPC很像,但又不是典型的IPC,也不僅僅是IPC,它還是系統處理CPU異常的方法。
其它模塊:
操作系統還有兩個重要的模塊:一個是和社會因素有關的安全防護,一個是和物理因素有關的電源管理。電源管理包括基本電源管理和高級電源管理,基本電源管理包括:睡眠、休眠、關機、重啟,高級電源管理也就是各種省電機制,如CPUIdle、CPUFrequ、DEVFreq,另外溫控也可以算到電源管理里面。
推薦閱讀:
想要學習Linux內核的話,推薦以下書籍《Linux Kernel Development》《Understanding the Linux Kernel》《Professional Linux Kernel Architecture》《Mastering Linux Kernel Development》《Understanding the Linux Virtual Memory Manager》《Linux內核深度解析》《Linux操作系統原理與應用》《深度探索Linux操作系統》《ARM Linux內核源碼剖析》《奔跑吧Linux內核》《Linux內核源代碼情景分析》。
想要學習早期版本Linux的,如0.11、0.12,推薦以下書籍《Linux內核設計的藝術》《Linux內核完全注釋》。
還有一些學習Linux的網絡資源:
LWN:https://lwn.net/Kernel/Index/
linux-insides:https://github.com/0xAX/linux-insides
宋寶華:https://blog.csdn.net/21cnbao
蝸窩科技:http://www.wowotech.net
CHENG Jian:https://kernel.blog.csdn.net/
內核工匠:https://blog.csdn.net/feelabclihu
DroidPhone:https://blog.csdn.net/DroidPhone
Bystander_J:https://blog.csdn.net/weixin_42092278
術道經緯:https://www.zhihu.com/column/c_1108400140804726784
Kernel Exploring:https://richardweiyang-2.gitbook.io/kernel-exploring/
Linux Performance:http://linuxperf.com/
Linux系統標準規范:
https://refspecs.linuxfoundation.org/
https://man7.org/linux/man-pages/man7/standards.7.html
六、計算機運行模型
學了上面這些知識之后,我們心里要有幾幅計算機的運行模型圖,達到計算機就在我心中運行的境界。一是CPU運行模型圖,二是進程調度圖,三是總體軟件體系結構圖。
6.1 CPU運行模型
最簡單的CPU模型是圖靈機模型,非常簡單,就是一個一直在線性運行的機器。其次是帶中斷的圖靈機模型,這個模型可以在正常的執行流之外插入一段額外的執行流。然后是具體的CPU運行模型,進程一般運行在用戶空間,借助中斷機制可以陷入內核執行一些系統調用,外設發生中斷會執行中斷處理函數。
實際的CPU運行模型在這個基礎之上又變得非常復雜了,如下圖所示。在學習了中斷機制、系統調用和進程調度之后,對下面這幾張圖應該能理解地比較透徹。
我們在這里說一下CPU執行場景,場景就是英文中的context,一般都翻譯成上下文,我看到有一本書上翻譯成場景,覺得這個翻譯非常信達雅,所以后面就用場景這個翻譯了。CPU的執行場景一共有兩種:一個是進程執行場景,一個是中斷執行場景。進程執行場景分為兩種情況:用戶空間進程執行場景和內核空間進程執行場景。進程一般在用戶空間運行,發生系統調用時會進入到內核空間運行。中斷執行場景只有一種情況,那就是內核空間中斷執行場景,因為中斷不能運行在用戶空間。中斷執行場景和進程執行場景還有一個很大的區別就是進程執行場景可阻塞可調度,中斷執行場景必須一次性執行完,不能阻塞,因為它不可調度。
6.2 進程調度模型
在CPU運行模型的基礎上,CPU在用戶空間執行進程,可以通過系統調用進入內核空間,也可能由于中斷或者異常發生而進入內核空間,在內核空間里進程可以由于阻塞而主動發生進程調度,或者在定時器中斷里面由于時間片耗盡而發生調度。
圖片展示的是UP(單處理器)調度的情況,每個時刻最多只有一個進程會被選中在CPU上運行。如果是SMP(多處理器)的話,就是有NR_CPU個這樣的圖同時運轉,互不干擾,這是從運行的角度看是這樣的,但是如果從空間的角度看的話,因為內核空間只有一個,所以圖片應該是中心捏合在一起,但是翅膀各是各的那種樣式的圖。
對于調度器是如何選擇進程進行調度的,我們心中要有下面這張圖。
6.3 軟件體系結構
下面我們再來看一下整個軟件體系放在一起是什么樣的:
我們可以看到內核運行在硬件之上,進程運行在內核之上,有init等進程是作為操作系統的一部分共同維護整個系統的運行。內核里面包括進程調度、內存管理、文件系統、BIO層、網絡模塊、其它驅動等模塊。進程是由一個exe主程序和n個so庫程序組成。每個進程中的libc.so,ld.so等庫程序都是屬于操作系統的一部分。所有進程通過進程調度共享CPU、輪流執行。
審核編輯:湯梓紅
評論
查看更多