一、內(nèi)核
- 操作系統(tǒng)是一個用來和硬件打交道并為用戶程序提供一個有限服務(wù)集的低級支撐軟件。一個計算機(jī)系統(tǒng)是一個硬件和軟件的共生體,它們互相依賴,不可分割。計算機(jī)的硬件,含有外圍設(shè)備、處理器、內(nèi)存、硬盤和其他的電子設(shè)備組成計算機(jī)的發(fā)動機(jī)。但是沒有軟件來操作和控制它,自身是不能工作的。
完成這個控制工作的軟件就稱為操作系統(tǒng),在Linux的術(shù)語中被稱為“內(nèi)核”,也可以稱為“核心”。Linux內(nèi)核的主要模塊(或組件)分以下幾個部分:存儲管理、CPU和進(jìn)程管理、文件系統(tǒng)、設(shè)備管理和驅(qū)動、網(wǎng)絡(luò)通信,以及系統(tǒng)的初始化(引導(dǎo))、系統(tǒng)調(diào)用等。
計算機(jī)組成:
image.png
計算機(jī)內(nèi)核:
ima、ge.png
二、4G地址空間解析
- 4G的進(jìn)程地址空間被人為的分為兩個部分--用戶空間與內(nèi)核空間。用戶空間從0到3G(0xc0000000),內(nèi)核空間占據(jù)3G到4G。用戶進(jìn)程通常情況下只能訪問用戶空間的虛擬地址,不能訪問內(nèi)核空間的虛擬地址。例外情況只有用戶進(jìn)程進(jìn)行系統(tǒng)調(diào)用(代表用戶進(jìn)程在內(nèi)核態(tài)執(zhí)行)等時刻可以訪問到內(nèi)核空間。
- 用戶空間對應(yīng)進(jìn)程,所以每當(dāng)進(jìn)程切換,用戶空間就會跟著變化;而內(nèi)核空間是由內(nèi)核負(fù)責(zé)映射,它并不會跟著進(jìn)程變化,是固定的。內(nèi)核空間地址有自己對應(yīng)的頁表,用戶進(jìn)程各自有不同的頁表。
-
每個進(jìn)程的用戶空間都是完全獨(dú)立、互不相干的。
image.png
三、Java IO類庫的基本分類
1. Java 的 I/O分類**
IO操作類在包 java.io 下,大概有將近 80 個類,但是這些類大概可以分成四組,分別是:
基于字節(jié)操作的 I/O 接口:InputStream 和 OutputStream
基于字符操作的 I/O 接口:Writer 和 Reader
基于磁盤操作的 I/O 接口:File
基于網(wǎng)絡(luò)操作的 I/O 接口:Socket
前兩組主要是根據(jù)傳輸數(shù)據(jù)的數(shù)據(jù)格式,后兩組主要是根據(jù)傳輸數(shù)據(jù)的方式
2.基于字節(jié)的 I/O 操作接口
基于字節(jié)的 I/O 操作接口輸入和輸出分別是:InputStream 和 OutputStream,InputStream 輸入流的類繼承層次如下圖所示:
圖 1. InputStream 相關(guān)類層次結(jié)構(gòu)(查看大圖)

輸入流根據(jù)數(shù)據(jù)類型和操作方式又被劃分成若干個子類,每個子類分別處理不同操作類型,OutputStream 輸出流的類層次結(jié)構(gòu)也是類似,如下圖所示:
圖 2. OutputStream 相關(guān)類層次結(jié)構(gòu)(查看大圖)

這里就不詳細(xì)解釋每個子類如何使用了,如果不清楚的話可以參考一下 JDK 的 API 說明文檔,這里只想說明兩點(diǎn),一個是操作數(shù)據(jù)的方式是可以組合使用的,如這樣組合使用
OutputStream out = new BufferedOutputStream(new ObjectOutputStream(new FileOutputStream("fileName"));
還有一點(diǎn)是流最終寫到什么地方必須要指定,要么是寫到磁盤要么是寫到網(wǎng)絡(luò)中,其實(shí)從上面的類圖中我們發(fā)現(xiàn),寫網(wǎng)絡(luò)實(shí)際上也是寫文件,只不過寫網(wǎng)絡(luò)還有一步需要處理就是底層操作系統(tǒng)再將數(shù)據(jù)傳送到其它地方而不是本地磁盤。關(guān)于網(wǎng)絡(luò) I/O 和磁盤 I/O 我們將在后面詳細(xì)介紹。
3.基于字符的 I/O 操作接口
不管是磁盤還是網(wǎng)絡(luò)傳輸,最小的存儲單元都是字節(jié),而不是字符,所以 I/O 操作的都是字節(jié)而不是字符,但是為啥有操作字符的 I/O 接口呢?這是因?yàn)槲覀兊某绦蛑型ǔ2僮鞯臄?shù)據(jù)都是以字符形式,為了操作方便當(dāng)然要提供一個直接寫字符的 I/O 接口,如此而已。我們知道字符到字節(jié)必須要經(jīng)過編碼轉(zhuǎn)換,而這個編碼又非常耗時,而且還會經(jīng)常出現(xiàn)亂碼問題,所以 I/O 的編碼問題經(jīng)常是讓人頭疼的問題。關(guān)于 I/O 編碼問題請參考另一篇文章 《深入分析Java中的中文編碼問題》。
下圖是寫字符的 I/O 操作接口涉及到的類,Writer 類提供了一個抽象方法 write(char cbuf[], int off, int len) 由子類去實(shí)現(xiàn)。
圖 3. Writer 相關(guān)類層次結(jié)構(gòu)(查看大圖)

讀字符的操作接口也有類似的類結(jié)構(gòu),如下圖所示:
圖 4.Reader 類層次結(jié)構(gòu)(查看大圖)

讀字符的操作接口中也是 int read(char cbuf[], int off, int len),返回讀到的 n 個字節(jié)數(shù),不管是 Writer 還是 Reader 類它們都只定義了讀取或?qū)懭氲臄?shù)據(jù)字符的方式,也就是怎么寫或讀,但是并沒有規(guī)定數(shù)據(jù)要寫到哪去,寫到哪去就是我們后面要討論的基于磁盤和網(wǎng)絡(luò)的工作機(jī)制。
4.字節(jié)與字符的轉(zhuǎn)化接口
另外數(shù)據(jù)持久化或網(wǎng)絡(luò)傳輸都是以字節(jié)進(jìn)行的,所以必須要有字符到字節(jié)或字節(jié)到字符的轉(zhuǎn)化。字符到字節(jié)需要轉(zhuǎn)化,其中讀的轉(zhuǎn)化過程如下圖所示:
圖 5. 字符解碼相關(guān)類結(jié)構(gòu)

InputStreamReader 類是字節(jié)到字符的轉(zhuǎn)化橋梁,InputStream 到 Reader 的過程要指定編碼字符集,否則將采用操作系統(tǒng)默認(rèn)字符集,很可能會出現(xiàn)亂碼問題。StreamDecoder 正是完成字節(jié)到字符的解碼的實(shí)現(xiàn)類。也就是當(dāng)你用如下方式讀取一個文件時:
清單 1.讀取文件
try {
StringBuffer str = new StringBuffer();
char[] buf = new char[1024];
FileReader f = new FileReader("file");
while(f.read(buf)>0){
str.append(buf);
}
str.toString();
} catch (IOException e) {}
FileReader 類就是按照上面的工作方式讀取文件的,F(xiàn)ileReader 是繼承了 InputStreamReader 類,實(shí)際上是讀取文件流,然后通過 StreamDecoder 解碼成 char,只不過這里的解碼字符集是默認(rèn)字符集。
寫入也是類似的過程如下圖所示:
圖 6. 字符編碼相關(guān)類結(jié)構(gòu)

通過 OutputStreamWriter 類完成,字符到字節(jié)的編碼過程,由 StreamEncoder 完成編碼過程。
5.磁盤 I/O 工作機(jī)制
前面介紹了基本的 Java I/O 的操作接口,這些接口主要定義了如何操作數(shù)據(jù),以及介紹了操作兩種數(shù)據(jù)結(jié)構(gòu):字節(jié)和字符的方式。還有一個關(guān)鍵問題就是數(shù)據(jù)寫到何處,其中一個主要方式就是將數(shù)據(jù)持久化到物理磁盤,下面將介紹如何將數(shù)據(jù)持久化到物理磁盤的過程。
我們知道數(shù)據(jù)在磁盤的唯一最小描述就是文件,也就是說上層應(yīng)用程序只能通過文件來操作磁盤上的數(shù)據(jù),文件也是操作系統(tǒng)和磁盤驅(qū)動器交互的一個最小單元。值得注意的是 Java 中通常的 File 并不代表一個真實(shí)存在的文件對象,當(dāng)你通過指定一個路徑描述符時,它就會返回一個代表這個路徑相關(guān)聯(lián)的一個虛擬對象,這個可能是一個真實(shí)存在的文件或者是一個包含多個文件的目錄。為何要這樣設(shè)計?因?yàn)榇蟛糠智闆r下,我們并不關(guān)心這個文件是否真的存在,而是關(guān)心這個文件到底如何操作。例如我們手機(jī)里通常存了幾百個朋友的電話號碼,但是我們通常關(guān)心的是我有沒有這個朋友的電話號碼,或者這個電話號碼是什么,但是這個電話號碼到底能不能打通,我們并不是時時刻刻都去檢查,而只有在真正要給他打電話時才會看這個電話能不能用。也就是使用這個電話記錄要比打這個電話的次數(shù)多很多。
何時真正會要檢查一個文件存不存?就是在真正要讀取這個文件時,例如 FileInputStream 類都是操作一個文件的接口,注意到在創(chuàng)建一個 FileInputStream 對象時,會創(chuàng)建一個 FileDescriptor 對象,其實(shí)這個對象就是真正代表一個存在的文件對象的描述,當(dāng)我們在操作一個文件對象時可以通過 getFD() 方法獲取真正操作的與底層操作系統(tǒng)關(guān)聯(lián)的文件描述。例如可以調(diào)用 FileDescriptor.sync() 方法將操作系統(tǒng)緩存中的數(shù)據(jù)強(qiáng)制刷新到物理磁盤中。
下面以清單 1 的程序?yàn)槔?,介紹下如何從磁盤讀取一段文本字符。如下圖所示:
圖 7. 從磁盤讀取文件

當(dāng)傳入一個文件路徑,將會根據(jù)這個路徑創(chuàng)建一個 File 對象來標(biāo)識這個文件,然后將會根據(jù)這個 File 對象創(chuàng)建真正讀取文件的操作對象,這時將會真正創(chuàng)建一個關(guān)聯(lián)真實(shí)存在的磁盤文件的文件描述符 FileDescriptor,通過這個對象可以直接控制這個磁盤文件。由于我們需要讀取的是字符格式,所以需要 StreamDecoder 類將 byte 解碼為 char 格式,至于如何從磁盤驅(qū)動器上讀取一段數(shù)據(jù),由操作系統(tǒng)幫我們完成。至于操作系統(tǒng)是如何將數(shù)據(jù)持久化到磁盤以及如何建立數(shù)據(jù)結(jié)構(gòu)需要根據(jù)當(dāng)前操作系統(tǒng)使用何種文件系統(tǒng)來回答,至于文件系統(tǒng)的相關(guān)細(xì)節(jié)可以參考另外的文章。
6.Java Socket 的工作機(jī)制
Socket 這個概念沒有對應(yīng)到一個具體的實(shí)體,它是描述計算機(jī)之間完成相互通信一種抽象功能。打個比方,可以把 Socket 比作為兩個城市之間的交通工具,有了它,就可以在城市之間來回穿梭了。交通工具有多種,每種交通工具也有相應(yīng)的交通規(guī)則。Socket 也一樣,也有多種。大部分情況下我們使用的都是基于 TCP/IP 的流套接字,它是一種穩(wěn)定的通信協(xié)議。
下圖是典型的基于 Socket 的通信的場景:
圖 8.Socket 通信示例

主機(jī) A 的應(yīng)用程序要能和主機(jī) B 的應(yīng)用程序通信,必須通過 Socket 建立連接,而建立 Socket 連接必須需要底層 TCP/IP 協(xié)議來建立 TCP 連接。建立 TCP 連接需要底層 IP 協(xié)議來尋址網(wǎng)絡(luò)中的主機(jī)。我們知道網(wǎng)絡(luò)層使用的 IP 協(xié)議可以幫助我們根據(jù) IP 地址來找到目標(biāo)主機(jī),但是一臺主機(jī)上可能運(yùn)行著多個應(yīng)用程序,如何才能與指定的應(yīng)用程序通信就要通過 TCP 或 UPD 的地址也就是端口號來指定。這樣就可以通過一個 Socket 實(shí)例唯一代表一個主機(jī)上的一個應(yīng)用程序的通信鏈路了。
6.建立通信鏈路
當(dāng)客戶端要與服務(wù)端通信,客戶端首先要創(chuàng)建一個 Socket 實(shí)例,操作系統(tǒng)將為這個 Socket 實(shí)例分配一個沒有被使用的本地端口號,并創(chuàng)建一個包含本地和遠(yuǎn)程地址和端口號的套接字?jǐn)?shù)據(jù)結(jié)構(gòu),這個數(shù)據(jù)結(jié)構(gòu)將一直保存在系統(tǒng)中直到這個連接關(guān)閉。在創(chuàng)建 Socket 實(shí)例的構(gòu)造函數(shù)正確返回之前,將要進(jìn)行 TCP 的三次握手協(xié)議,TCP 握手協(xié)議完成后,Socket 實(shí)例對象將創(chuàng)建完成,否則將拋出 IOException 錯誤。
與之對應(yīng)的服務(wù)端將創(chuàng)建一個 ServerSocket 實(shí)例,ServerSocket 創(chuàng)建比較簡單只要指定的端口號沒有被占用,一般實(shí)例創(chuàng)建都會成功,同時操作系統(tǒng)也會為 ServerSocket 實(shí)例創(chuàng)建一個底層數(shù)據(jù)結(jié)構(gòu),這個數(shù)據(jù)結(jié)構(gòu)中包含指定監(jiān)聽的端口號和包含監(jiān)聽地址的通配符,通常情況下都是“*”即監(jiān)聽所有地址。之后當(dāng)調(diào)用 accept() 方法時,將進(jìn)入阻塞狀態(tài),等待客戶端的請求。當(dāng)一個新的請求到來時,將為這個連接創(chuàng)建一個新的套接字?jǐn)?shù)據(jù)結(jié)構(gòu),該套接字?jǐn)?shù)據(jù)的信息包含的地址和端口信息正是請求源地址和端口。這個新創(chuàng)建的數(shù)據(jù)結(jié)構(gòu)將會關(guān)聯(lián)到 ServerSocket 實(shí)例的一個未完成的連接數(shù)據(jù)結(jié)構(gòu)列表中,注意這時服務(wù)端與之對應(yīng)的 Socket 實(shí)例并沒有完成創(chuàng)建,而要等到與客戶端的三次握手完成后,這個服務(wù)端的 Socket 實(shí)例才會返回,并將這個 Socket 實(shí)例對應(yīng)的數(shù)據(jù)結(jié)構(gòu)從未完成列表中移到已完成列表中。所以 ServerSocket 所關(guān)聯(lián)的列表中每個數(shù)據(jù)結(jié)構(gòu),都代表與一個客戶端的建立的 TCP 連接。
7.數(shù)據(jù)傳輸
傳輸數(shù)據(jù)是我們建立連接的主要目的,如何通過 Socket 傳輸數(shù)據(jù),下面將詳細(xì)介紹。
當(dāng)連接已經(jīng)建立成功,服務(wù)端和客戶端都會擁有一個 Socket 實(shí)例,每個 Socket 實(shí)例都有一個 InputStream 和 OutputStream,正是通過這兩個對象來交換數(shù)據(jù)。同時我們也知道網(wǎng)絡(luò) I/O 都是以字節(jié)流傳輸?shù)?。?dāng) Socket 對象創(chuàng)建時,操作系統(tǒng)將會為 InputStream 和 OutputStream 分別分配一定大小的緩沖區(qū),數(shù)據(jù)的寫入和讀取都是通過這個緩存區(qū)完成的。寫入端將數(shù)據(jù)寫到 OutputStream 對應(yīng)的 SendQ 隊(duì)列中,當(dāng)隊(duì)列填滿時,數(shù)據(jù)將被發(fā)送到另一端 InputStream 的 RecvQ 隊(duì)列中,如果這時 RecvQ 已經(jīng)滿了,那么 OutputStream 的 write 方法將會阻塞直到 RecvQ 隊(duì)列有足夠的空間容納 SendQ 發(fā)送的數(shù)據(jù)。值得特別注意的是,這個緩存區(qū)的大小以及寫入端的速度和讀取端的速度非常影響這個連接的數(shù)據(jù)傳輸效率,由于可能會發(fā)生阻塞,所以網(wǎng)絡(luò) I/O 與磁盤 I/O 在數(shù)據(jù)的寫入和讀取還要有一個協(xié)調(diào)的過程,如果兩邊同時傳送數(shù)據(jù)時可能會產(chǎn)生死鎖,在后面 NIO 部分將介紹避免這種情況。


