首發於個人博客網站(prajna.top) 歡迎你們前去交流,有pdf版本。node
本文主要是從應用的角度出發,分別闡述操做系統接口,計算機語言,文件系統等背後的一些知識,規範,原理,設計思想,應用法門,讓初學者對編碼有一個總體的,全局的認識,有一個物理的視角,找到本身的起點。python
寫這篇文章主要是基於本身大學的經歷,當時抱着一腔熱血去學計算機編程,但是當把c/c++語言,數據結構,操做系統,計算機組成原理等課程都學完後,卻發現本身彷佛什麼也不會,只會printf打印一些字符串。那段時間真的好苦惱,特別想作軟件,殊不知從何開始,也不知道該如何去使力,蹉跎了很久,浪費了大量的時間。linux
形成這種現象的主要緣由,一是本身缺乏那種天賦,二是教學過於側重基礎和理論,每門課程只涉及到一個局部,沒有一門課程把這些串起來。我不瞭解語言的基礎庫,除了printf後,其它API都不會用;也不瞭解具體的操做系統平臺的API;雖然學了TCP/IP,socket的具體使用卻又不清楚; 至於像fat,ext等磁盤文件系統的格式,那就更遙遠了;我甚至還不清楚計算機語言和編譯工具的關係;更要命的是,我還不知道本身不知道這些。說白了就是理論同應用脫節,雖然大學也安排了課程設計,實驗和實習,但都只是走了過場,也沒有人來指點一下,該看些什麼書籍。大學讀完,就知道拖拉幾個控件作一個窗口,鏈接一下數據庫。c++
windows + VC屏蔽了太多的技術細節,惋惜大學期間接觸的恰恰就是它,對用戶這個是好事越傻瓜越好,但是對計算機的學生就要了命了。自從我轉投到linux開源世界後,終於才發現了什麼是自由,什麼是編程。當閱讀linux源碼碰到不理解的地方,能夠直接修改源碼加上打印,分析kernel流程。對比着minix的源碼來學習操做系統結構原理,那些概念就變成實實在在的數據結構和算法,動手寫一個minix的驅動,微內核和宏內核區別就一目瞭然了。強烈建議,想學習編程的同窗都去擁抱開源世界,而後,再回到本身感興趣的或工做相關的領域。程序員
計算機語言說白了就是工具,關鍵仍是你要作什麼,這樣就涉及到了應用,以及專業背景知識。如想作驅動編程,離不開對操做系統驅動架構的瞭解;想作一個磁盤分區合併,那須要瞭解文件系統的格式;作個播放器吧,那對視頻文件格式,編碼格式,編解碼API的瞭解必不可少。 隨着你對軟件系統瞭解的深刻,會發現其實一切都是協議。 http 是一套web 通信的協議;計算機語言是開發工具提供的協議; 操做系統是內核空間與應用空間的協議..., 這些協議被各類規範約束--並造成了各類技術。 因此,每種技術的背後都一套協議,規則和思想。瞭解這些才能算真正瞭解了相關技術。web
在這篇文章裏面,我以GNU/Linux做爲平臺,從應用的角度出發來把相關的課程來串一串提供一個「物理視圖」,讓初學者有個全局的認識,可以有一個方向和切入角度,至少知道該找些什麼資料來看。算法
它是內核對應用空間提供的一套協議,主要包括:數據庫
ELF是編譯, 連接生成的,執行的時候,由ld 解析,加載在到內存,最後控制權交給程序入口代碼,程序開始執行。所以,它提供了2類視圖:連接視圖和執行視圖。 編程
從連接視圖上看,ELF由衆多的 Section組成,編譯器先把源碼編譯成.o文件,主要是提取函數,全局變量等生成符號表,把它們填充到相應的 Section裏面去。 在這個階段,全部的符號都是無法定位到地址的。windows
Link的時候,對.o文件進行合併,對各個文件內的符號進行重定位,安排它們的地址,以下圖所示, link完成後,g_u8 和 g_flag2都有地址了。
對於動態連接的函數,在link階段無法安排地址,須要放到 dynsym Section裏面去,在 ld的時候,來進行定位 -- 這就是所謂的 "函數重定位"。
linux系統提供了可執行程序readelf來解析 ELF文件格式,咱們可使用它來了解一下ELF文件的一些通用的Section。
'offset Align' 是各個Section在ELF文件內的偏移地址,咱們以二進制的方式打開ELF文件,根據偏移地址,就能夠查看相應Section的二進制內容。
從下圖中能夠看到 .interp的內容是 "/lib64/ld-linux-x86-64.so.2",
上面這些就是編譯,連接生成ELF文件的過程:編譯器以源文件做爲輸入,先提取各個文件的全局變量和函數,生成符號表,再把它們連接到一塊兒,連接的時候對各個符號進行定位,分配地址。對於動態連接庫的函數,則推遲到'加載'程序到內存的時候進行定位。編譯連接後,代碼和數據分散到了相應的section裏面,程序加載的時候,須要把Section 合併成Secgment,而後,以Secgement爲單位加載到內存頁面裏面去,咱們來看一下Segment的結構。
ELF有9種Segment,其中比較核心的是 --
Segment同Section 是有對應關係的,如:
程序執行的時候,先從INTERP 段找到對應的執行程序--可執行程序通常是ld.so, 首先加載ELF文件,根據Prgrame Header 數據結構,把section加載到各個程序段裏面, 而後,遞歸重定位動態連接符號,加載這些符合依賴的動態連接庫,處理ELF文件中的重定位, 而後,把控制權交給代碼入口,程序開始運行。在這個過程當中,ld 最重要的一個事情就是'重定位', 修改ELF裏面的動態連接函數的符號表。
系統調用是kernel提供給應用層的API,經過軟中斷來調用。調用的形式是這樣
對ENTER_KERNEL宏的定義,傳統(i386)的調用方式是 'int $80', ia64是 'syscall'指令來進入kernel。 軟中斷的流程基本上都是這麼幾個步驟
咱們能夠在'arch/x86/syscalls/'目錄下找到 syscall的列表,
最前面的數字就是系統調用號, 如: 'syscall 5'調用的是'open'。 總共大約是400個左右,涵蓋了最基本的應用:如上圖中文件相關的操做, 進程類的(fork, execve)等。 這些系統調用都被libc庫作了封裝, 一些簡單的底層函數(如:mount, mkdir, stat)則只是簡單地被包裹了一下,直接軟中斷到kernel了。因此,你們若是想了解文件系統相關API的實現,必定要看kernel的源碼,看 libc庫的源碼是沒用的,它都是簡單地作了一個系統調用的轉換。
前面提到過一切都是協議,POSIX是一個普遍被支持的協議(規範),Linux和各種unix都對它提供了支持,只要操做系統申明支持POSIX接口,它就得實現POSIX定義的系統調用。對linux/unix而言, POSIX只是它們的一個子集,它會還會支持UNIX世界的一些系統調用規範。總之,有了這些規範,libc就能夠在各個系統間無縫移植。
sysfs是linux kernel以文件系統的方式提供給應用層的接口,在linux的世界裏,驅動模塊都被抽象爲文件系統節點,所以,咱們對 /sys/文件系統進行讀寫操做,能夠與內核裏面的驅動層進行交流。 具體的接口內容,請查詢 '/Documentation/ABI/'目錄下sysfs-module文件
計算機語言就是一套人機交互的協議,比如咱們學了英語,就能夠同「支持」英語的人交流,來達到咱們的一些目的。計算機語言的本質也是同樣的,程序員經過某語言來調配它的資源,完成目標任務。不管是什麼語言,語法方面都大同小異,無非就是變量定義,表達式加幾個循環而已--它的理論源頭就是大名鼎鼎的圖靈完備的編程語言。圖靈從理論上論證了,只要符合圖靈完備規則,就能夠知足全部的自動化計算的須要。
所以,各類語言的語法都大同小異,那麼,爲何咱們還須要那麼多的語言呢?
這就回到了「什麼是計算機語言的本質「這個問題了,語言的核心究竟是什麼呢? 很顯然不是語法,而是它編程思想和資源(功能)。 每種語言都是爲了解決某些問題而存在,都會提供一套語法和平臺資源--包括標準庫和第三方庫。
彙編語言主要是爲了方便記憶,對機器指令上作了一個替換,沒有彙編語言,咱們還須要一邊查着芯片手冊,一邊手敲着 "6F 01 20" 之類的代碼。因爲它沒有提供內存管理和系統結構化的手段,本質上仍是機器指令級別的編程。使用匯編語言,咱們還得規劃內存,挑選寄存器,完成堆棧操做,所以,彙編只適合代碼量少的系統。如很小的單片機或者系統的引導代碼。高級語言則不一樣,編譯器幫忙搞掂了內存佈局,進棧出棧等這些煩瑣的事情,直接面嚮應用。
C語言提供了面向模塊的結構化思想,經過模塊化機制,咱們能夠分工合做,構建起應用。學習C語言,就是要了解它的模塊化思想,學會如何利用數據結構和函數指針封裝模塊,提供統一對外接口。另外,還得了解它的庫,這個決定了咱們能得到多大的資源支持。C的基本庫對系統調用進行了封裝, 提供了一些跟操做系統相關的函數(文件操做,進程管理,內存相關,socket等),它沒有提供經常使用的數據結構和算法(好比:鏈表的構建和排序,二叉樹),須要本身處理。 在應用層面,它則提供了字符和字符串處理,數學函數(sin,cos,tan,...),日期和時間等等,可見C語言的標準庫提供的都是底層的函數,固然了也有一些上層的GUI框架利用C語言提供接口的,他們在C的基礎上提供大量的擴展,一步步地架構了本身的系統。
C++雖然是C的擴展,但它是全新的語言,提供的是面向對象的架構思想。只不是兼容C語言規範,它能夠利用C語言的全部資源。它利用模板來提供標準庫(STL)封裝了經常使用的數據結構和算法。 官方又提供的 Boost模板庫,擁有了更豐富更強大的功能。 利用STL/Boost和基礎C庫,都足以開發一些底層軟件。 C和C++的基礎庫都沒有提供圖形,多媒體,GUI框架,用戶須要使用第三方的資源,如:SDL, opengl,Qt等。
像JAVA這類帶虛擬機的語言最厲害的地方,除了跨平臺性,就是它強大的類庫。不但封裝了經常使用的數據結構和算法,還集成了GUI框架,圖形庫,多媒體處理,也有很強的web處理能力。
python則提出不要重複造"輪子",由於它提供了大量可用的輪子,從基礎的數據結構到大型的應用模塊,應有盡有。幾句代碼就能夠完成一個 http服務器。 並且,python把變量定義這個環節都省了,直接面對要解決的問題,效率極高,特別適合教學,科研,作算法分析和原型驗證,工具軟件。 你想一想,如今你忽然有了一個算法構想,立刻想驗證一下,使用的python的話,你直接就能夠寫算法代碼了,想當於把算法的僞碼拿過來直接就用了。
用C語言的話,你得先定義各類數據結構,變量,再編譯,排錯,真的是很會很急人的,若是涉及到字符串的處理,心早就拔涼拔涼了。
軟件編程就是利用語言來調配各類資源來實現目標。學習一門新語言,除了學習語法,更多的關注點仍是它提供的編程思想和它的平臺資源。你得了解目標語言提供的各類資源,瞭解它的適用場景,它是爲解決什麼問題而存在。最好的學習方法是看一些經典的源碼,如:mplayer的C語言代碼,很好地詮釋了什麼是模塊化設計,真的使人歎爲觀止,會發現原來本身根本就不會寫代碼。
學彙編的比C語言厲害些嗎?
常常會聽到這樣的爭論,有一次地鐵上,我在用C++編碼,旁邊一個」哥們「問我學習C++是否是賺錢些,我唯苦笑不已。語言自己沒有高下之分,會用這個語言來完成任務纔算厲害,最厲害的就是作出產品,像 freebsd, linux 這種劃時代的產品,纔是真的厲害。對程序員而已,厲害的是你的編程思想和算法能力,行業的專業知識。語言這種東西,只是是信手拈來,實現你的想法而已。
編譯器就是語言的某一個具體實現,廠家不一樣,產品不一樣,像經典的 TC 和 VC。 編譯器最基礎的功能就是把所支持的源語言,編譯成操做系統支持的可執行文件格式。若是,它再提供一個GUI界面,增長了源碼編輯,斷點調試,GUI框架,那麼,它就成了一個開發平臺,好比經典的VC。 目前,咱們使用的集成開發環境都是融合了編輯,編譯,項目管理,資源編輯,GUI控件佈局,代碼生成等一系列工具。
對於初學者仍是建議你們本身使用GCC來做編譯器, GDB來調試, 本身寫Makefile來定義編譯規則,再找一個源碼編輯工具像Emacs或者Eclipse之類,這樣你們能夠很清晰地知道本身在幹什麼,須要作什麼。再使用集成開發工具的時候,知道該怎麼去處理各類問題,否則一直稀裏糊塗地不不知道一個'Run'點下去,到底發生了什麼,一出問題就傻眼。
若是須要重量級GUI框架的,能夠考慮Qt平臺,初學者也不要使用Qt Creator,能夠本身使用Qt的工具來作預處理,如:
能夠定義相應的規則,把它放到Makefile裏面,這樣就可使用make來進行管理。
話說學好了數據結構和算法,就基本解決了編程問題。但是當咱們拖拉幾個控件,寫幾個事件,就能夠完成工做的時候,不由會有點疑惑,說好的數據結構和算法呢。--其實,它無處不在,在應用框架裏,在中間件裏,在API裏面,在 Kernel裏。 C++的STL和JAVA都把一些經常使用的數據結構封裝成了「集合」對象。
數據結構就是對客觀對象的抽象,算法則是如何來組織和調用這些數據。在kernel裏面,咱們耳熟能詳的概念都是一個個具體的數據結構如:進程,內存頁面等等。下面是linux的進程數據結構'struct task_struct'部分片段,能夠看到對進程的描述信息(屬性)都定義在該結構裏面了。
這個就是咱們經過ps命令看到的進程信息描述,其實在linux kernel都是按照線程來管理的,因此,task_struct 也是線程的描述。 linux 源碼裏面就是大量的這種數據結構定義,以及把它們連接起來進行管理的各種鏈表,二分查找樹。
下面簡單說下數據結構和算法在STL裏面的應用。
STL做爲C++的標準庫,它封裝了經常使用的數據結構和算法。<vector> 的數據結構本質上是動態數組,當空間不夠的時候,它從新分配空間,並拷貝舊元素到的數組。
<list> 是一個鏈表,「讀」和「 插入」的時間複雜度是O(n)--n是元素的位置, 「插入」數據操做的時間消耗是常量級的。
雖然<vector>的'插入'時間複雜度也是O(n),可是,<list>不須要拷貝後面的元素,只須要移動到相應的位置,它的'插入'性能更好,而<vector> 提供隨機訪問能力,適合經過'數組下標'直接訪問場合。
有沒有像<vector>那樣提供隨機訪問能力,可是又能提供 <list> 那樣的插入性能的數據結構呢?
有,那就是 <deque>,它至關因而 <vector> 和 <list> 的一個結合,至關於 <list> 來鏈接固定大小的<vector>。
那是否是可使用<deque>來代替<vector>和<list>呢? 固然不能夠:
<set>和 <map> 一般用一棵紅黑二叉樹來作爲數據結構的實現,根據關鍵字來排序,「查找」,「插入」和」「刪除」的時間開銷都是 O(lg(n)),它裏面的記錄是有序排列的,咱們根據關鍵字把它導出來就是一個有序列表,它們的綜合性能比較好。
若是須要更快速的訪問能力,能夠考慮使用帶hash結構的 <unordered_set> 和 <unordered_map> 「查找」,「插入」和」「刪除」的平均開銷是「常量級」的,性能很好,可是它裏面的數據是「無序」的。
文件系統就是用來對文件進行增刪改查的控制系統,不一樣類型的文件系統對應着不一樣的管理策略,可是,不管何種策略都須要解決兩個最基本的問題:
所以文件系統須要在磁盤記錄一些額外的信息,因此格式化之後,磁盤容量會小於實際的容量。格式化的過程就是在磁盤上「安裝」文件系統的過程,下面以EXT2和FAT32爲例來講說文件系統是如何工做的。
ext系列是linux下面主打的文件系統,它以Block爲容量單位,以Inode來抽象文件(目錄也是文件)。一個硬盤分紅多個Block Group,每一個Group裏面分別存儲Inode和具體的文件數據,以下圖所示:
從上面圖中能夠看到,每一個Group Block的結構都是如出一轍,除了Data Blocks外,內容也都是同樣的嗎?
由於Super Block和 GDT是很是重要的,因此,每一個Block Group都有一份備份,所以他們的數據是同樣的,因爲備份會浪費空間,新版本的EXT系統再也不要求每一個Group都進行備份了,格式化的時候,能夠選擇備份策略。除了這兩項外,其它的內容就跟該Group的存儲的數據有關了。
那麼劃分Block Group的好處有哪些呢?
主要是爲了防止文件存儲碎片化。在存儲的時候,預先保留多餘空間,儘可能把文件放到一個Group裏面,所以文件不是互相連續存放的:好比前後建立2個文件,第一個文件放在213 Block,第二個文件可能就510 Block,預留一部分空間給文件擴展用。固然了,對超大的文件,是會跨Group存放的。 經過精心的Block Group劃分,再加上Linux靈活的文件分配策略,EXT文件系統在正常使用狀況下「碎片率」是比較低的。
目錄表沒有放到Inode Table裏面,而是放到Data Blocks上了,每一個目錄節點都對應一個Inode,它的數據區域(Data Blocks)就是它的目錄表。每一個目錄表項包括:文件名,類型和Inode號--經過Inode號又能找到下一級目錄表,這樣就構成了一個目錄鏈表結構。
類Unix系統都有一個「根目錄」,是路徑的起始點,系統就是從根目錄開始來遍歷目錄鏈表的,它也至關於鏈表的「根節點」,所以它的位置必需是固定的:Linux系統的「根目錄」Inode節點號是2。
下面以一個小容量的ext2系統爲例來遍歷下目錄。
首先查找「根節點」,它的內容以下:
BLOCKS: (0):204 表示:該Inode只佔用了一個Block(編號從0開始起),內容在204號Block上,咱們如今讀取Inode 2 裏面的內容(也就是204號Block):
根據目錄表項的定義解析二進制數據,就能夠獲得根目錄的目錄表:
Name | Inode | Type |
---|---|---|
. | 0x02 | 目錄 |
lost+found | 0x0b | 目錄 |
home | 0x501 | 目錄 |
large.img | 0x0c | 文件 |
接下來,遍歷下一級目錄。
若是咱們要訪問"/home"目錄,經過查找表,會發現它的Inode號是1281, 而後,讀取Inode 1281的內容,再根據BLOCKS項的信息,讀取它的數據塊,又能夠獲得'/home'的目錄表--就如同前面讀取「根節點」同樣,只是Inode 2變成了Inode 1281。這樣反覆遞歸,就能夠一級一級地查詢到最終的目錄或者文件。
以上就是目錄存儲和查找的過程,目錄表存儲在Inode的數據區域,查找就是從根節點開始,反覆遞歸,直到目的路徑。
爲何「根目錄」的Inode號是2,而不是1或者0呢?
Inode 0 表示該Inode 不存在,相似C語言的空指針,這個是爲了編碼方便。 Inode 1是用存儲壞塊的, 因此,「根目錄」就是Inode 2了,固然了,不一樣操做系統的具體實現估計會有差別。
咱們經過目錄結構找到文件對應的Inode節點後,就能夠讀取它的內容了。Inode節點使用一個數組來存儲文件所佔用的Block號(BLOCKS項的內容), 數組的長度爲15,Block號用4個字節來表示。數組的前12位是當即尋址,第13,14和15位則是間接尋址。
下面以Block大小爲1024個字節爲例,來講明這個尋址過程。
如上圖所示:
分析一下:文件最多可佔用(12+256+256256+256256*256 =16843020)個Block,若是Block的大小是1K,那麼文件大小的上限約是 16G。對於小於13個Block的文件是直接尋址,訪問速度快。間接尋址,主要是爲了增大文件的容量,同時也能快速讀取前12個Block的內容。
FAT是Windows下的一款經典文件系統,當時Windows系統下必定要作兩件事情是:碎片整理和殺毒,而這些都同FAT文件系統有關。下面先了解下FAT系統的基本內容,下圖是FAT的「物理視圖」。
BPB (啓動引導區域)-- 至關於FAT文件系統的「頭部」,定以了FAT文件系統的「元數據」:扇區大小,簇的大小,FAT表的位置和大小,根目錄的位置,基本上FAT文件系統的物理佈局,都定義在這個裏面,在系統格式化的時候生成。
FAT--全稱是File Allocation Table(文件分配表),實質上就是一個大數組,以「簇」爲單位 ,來記錄硬盤空間的使用狀況:
目錄結構--目錄也被看成(抽象)成一種文件,它的內容就是一個目錄表。目錄項的定義以下:
根據上面信息,能夠畫出FAT16物理佈局圖,
下面咱們以一個只有10M的FAT16文件系統爲例,來實際說明一下FAT文件系統的工做過程。首先,解析BPB區域, 獲得FAT16的「元數據」,部分重要信息以下:
根據上面信息,能夠畫出FAT16物理佈局圖,
由於根目錄是特殊的數據,也能夠說數據區是從0x9200開始的。
接下來找到根目錄,讀取它的目錄表:
根據目錄項的定義,獲得根目錄內容以下:
文件地址的物理簇號是從2開始算起的,所以,ROOT_F~1.IMG的起始物理地址(以字節爲單位)計算以下:
文件起始地址: 0x9200 + (3 - 2) * 2048 = 0x9A00。
0x9A00就是文件的起始地址,當咱們讀完該簇後,該如何去尋找下一個簇呢?
由於FAT16是的FAT表項是2個字節,所以 FAT[3]是0x04, 也就是說下一個文件簇的位置是4, 完整的查找過程以下:
能夠看出 FAT就是一個連接,當前的值指向下一個簇號,直到以0xFFFF做爲結束。
同理讀取目錄也是同樣的,本例中,HOME目錄的起始地址是: 0x9200 + (0x0d - 2) * 2048 = 0xEA00
0xea00的內容同根目錄同樣,就是該目錄下的目錄表,若是該目錄的內容超過一個簇,也一樣去經過FAT表去查找其它的內容。
這就是FAT文件系統的基本概念和工做流程。不難看出FAT採用的這種鏈表結構,會致使碎片化會很嚴重,使用時間長了之後,一個文件的簇鏈獲得處都是。另外,FAT尚未權限管理,病毒程序就如入無人之境,能夠隨意複製和破壞。FAT的好處是簡單靈活,文件的個數不固定,且佔用磁盤空間少。可是,畢竟適應不了目前大容量高性能的要求,微軟從FAT12打補丁到FAT32後,就推出了NTFS文件系統。
到這裏,文件系統的一些基礎知識就介紹完了,那麼瞭解它有哪些實際的意義呢?
首先,像電影裏面的黑客,能夠直接面對文件系統的原始數據,好比:從文件系統損壞的硬盤裏面恢復一些關鍵文件--經過讀取磁盤文件系統的元數據,好比說直接找目錄表,看看哪些文件還能夠被識別,再查找相應的Inode節點(或FAT表),讀取它的BLOCKS(或簇),只要物理上還沒損壞,就能恢復出來。
其次,能夠作一些磁盤類的工具,像文件搜索,恢復刪除文件之類--文件被刪除後,文件系統通常都是修改了些標誌位,如:bitmap的空閒標誌置1,表示空間被釋放等,其實文件的內容還在硬盤上,只要及時地找到文件的位置,就有恢復的可能。我曾經用python作過一個小工具,能夠瀏覽,提取iso 9600文件系統(光盤)裏的文件,這樣能夠在不使用虛擬光驅的狀況下,把iso鏡像文件的內容都提取出來。
歡迎你們來個人網站交流:般若程序蟬