一怀各、內(nèi)核
- 操作系統(tǒng)是一個(gè)用來和硬件打交道并為用戶程序提供一個(gè)有限服務(wù)集的低級(jí)支撐軟件。一個(gè)計(jì)算機(jī)系統(tǒng)是一個(gè)硬件和軟件的共生體术浪,它們互相依賴瓢对,不可分割。計(jì)算機(jī)的硬件胰苏,含有外圍設(shè)備硕蛹、處理器、內(nèi)存、硬盤和其他的電子設(shè)備組成計(jì)算機(jī)的發(fā)動(dòng)機(jī)法焰。但是沒有軟件來操作和控制它僵腺,自身是不能工作的。
完成這個(gè)控制工作的軟件就稱為操作系統(tǒng)壶栋,在Linux的術(shù)語中被稱為“內(nèi)核”辰如,也可以稱為“核心”。
Linux內(nèi)核的主要模塊(或組件)分以下幾個(gè)部分:存儲(chǔ)管理贵试、CPU和進(jìn)程管理琉兜、文件系統(tǒng)、設(shè)備管理和驅(qū)動(dòng)毙玻、網(wǎng)絡(luò)通信豌蟋,以及系統(tǒng)的初始化(引導(dǎo))、系統(tǒng)調(diào)用等桑滩。
計(jì)算機(jī)組成:
計(jì)算機(jī)內(nèi)核:
二、4G地址空間解析
- 4G的進(jìn)程地址空間被人為的分為兩個(gè)部分--用戶空間與內(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í)行)等時(shí)刻可以訪問到內(nèi)核空間该互。
- 用戶空間對(duì)應(yīng)進(jìn)程,所以每當(dāng)進(jìn)程切換韭畸,用戶空間就會(huì)跟著變化宇智;而內(nèi)核空間是由內(nèi)核負(fù)責(zé)映射,它并不會(huì)跟著進(jìn)程變化胰丁,是固定的随橘。內(nèi)核空間地址有自己對(duì)應(yīng)的頁表,用戶進(jìn)程各自有不同的頁表锦庸。
-
每個(gè)進(jìn)程的用戶空間都是完全獨(dú)立机蔗、互不相干的。
三酸员、Java IO類庫的基本分類
1. Java 的 I/O分類**
IO操作類在包 java.io 下蜒车,大概有將近 80 個(gè)類,但是這些類大概可以分成四組幔嗦,分別是:
基于字節(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ù)類型和操作方式又被劃分成若干個(gè)子類邀泉,每個(gè)子類分別處理不同操作類型嬉挡,OutputStream 輸出流的類層次結(jié)構(gòu)也是類似庞钢,如下圖所示:
圖 2. OutputStream 相關(guān)類層次結(jié)構(gòu)(查看大圖)
這里就不詳細(xì)解釋每個(gè)子類如何使用了颜懊,如果不清楚的話可以參考一下 JDK 的 API 說明文檔河爹,這里只想說明兩點(diǎn)咸这,一個(gè)是操作數(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ò)傳輸茬故,最小的存儲(chǔ)單元都是字節(jié)盖灸,而不是字符,所以 I/O 操作的都是字節(jié)而不是字符磺芭,但是為啥有操作字符的 I/O 接口呢?這是因?yàn)槲覀兊某绦蛑型ǔ2僮鞯臄?shù)據(jù)都是以字符形式钾腺,為了操作方便當(dāng)然要提供一個(gè)直接寫字符的 I/O 接口姻报,如此而已。我們知道字符到字節(jié)必須要經(jīng)過編碼轉(zhuǎn)換吴旋,而這個(gè)編碼又非常耗時(shí),而且還會(huì)經(jīng)常出現(xiàn)亂碼問題荣瑟,所以 I/O 的編碼問題經(jīng)常是讓人頭疼的問題治拿。關(guān)于 I/O 編碼問題請(qǐng)參考另一篇文章 《深入分析Java中的中文編碼問題》。
下圖是寫字符的 I/O 操作接口涉及到的類笆焰,Writer 類提供了一個(gè)抽象方法 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 個(gè)字節(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)字符集签杈,很可能會(huì)出現(xiàn)亂碼問題瘫镇。StreamDecoder 正是完成字節(jié)到字符的解碼的實(shí)現(xiàn)類。也就是當(dāng)你用如下方式讀取一個(gè)文件時(shí):
清單 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é)和字符的方式。還有一個(gè)關(guān)鍵問題就是數(shù)據(jù)寫到何處辑鲤,其中一個(gè)主要方式就是將數(shù)據(jù)持久化到物理磁盤盔腔,下面將介紹如何將數(shù)據(jù)持久化到物理磁盤的過程。
我們知道數(shù)據(jù)在磁盤的唯一最小描述就是文件月褥,也就是說上層應(yīng)用程序只能通過文件來操作磁盤上的數(shù)據(jù)弛随,文件也是操作系統(tǒng)和磁盤驅(qū)動(dòng)器交互的一個(gè)最小單元。值得注意的是 Java 中通常的 File 并不代表一個(gè)真實(shí)存在的文件對(duì)象吓坚,當(dāng)你通過指定一個(gè)路徑描述符時(shí)撵幽,它就會(huì)返回一個(gè)代表這個(gè)路徑相關(guān)聯(lián)的一個(gè)虛擬對(duì)象,這個(gè)可能是一個(gè)真實(shí)存在的文件或者是一個(gè)包含多個(gè)文件的目錄礁击。為何要這樣設(shè)計(jì)?因?yàn)榇蟛糠智闆r下逗载,我們并不關(guān)心這個(gè)文件是否真的存在哆窿,而是關(guān)心這個(gè)文件到底如何操作。例如我們手機(jī)里通常存了幾百個(gè)朋友的電話號(hào)碼厉斟,但是我們通常關(guān)心的是我有沒有這個(gè)朋友的電話號(hào)碼挚躯,或者這個(gè)電話號(hào)碼是什么,但是這個(gè)電話號(hào)碼到底能不能打通擦秽,我們并不是時(shí)時(shí)刻刻都去檢查码荔,而只有在真正要給他打電話時(shí)才會(huì)看這個(gè)電話能不能用漩勤。也就是使用這個(gè)電話記錄要比打這個(gè)電話的次數(shù)多很多。
何時(shí)真正會(huì)要檢查一個(gè)文件存不存缩搅?就是在真正要讀取這個(gè)文件時(shí)越败,例如 FileInputStream 類都是操作一個(gè)文件的接口,注意到在創(chuàng)建一個(gè) FileInputStream 對(duì)象時(shí)硼瓣,會(huì)創(chuàng)建一個(gè) FileDescriptor 對(duì)象究飞,其實(shí)這個(gè)對(duì)象就是真正代表一個(gè)存在的文件對(duì)象的描述,當(dāng)我們在操作一個(gè)文件對(duì)象時(shí)可以通過 getFD() 方法獲取真正操作的與底層操作系統(tǒng)關(guān)聯(lián)的文件描述堂鲤。例如可以調(diào)用 FileDescriptor.sync() 方法將操作系統(tǒng)緩存中的數(shù)據(jù)強(qiáng)制刷新到物理磁盤中亿傅。
下面以清單 1 的程序?yàn)槔榻B下如何從磁盤讀取一段文本字符瘟栖。如下圖所示:
圖 7. 從磁盤讀取文件
當(dāng)傳入一個(gè)文件路徑葵擎,將會(huì)根據(jù)這個(gè)路徑創(chuàng)建一個(gè) File 對(duì)象來標(biāo)識(shí)這個(gè)文件,然后將會(huì)根據(jù)這個(gè) File 對(duì)象創(chuàng)建真正讀取文件的操作對(duì)象半哟,這時(shí)將會(huì)真正創(chuàng)建一個(gè)關(guān)聯(lián)真實(shí)存在的磁盤文件的文件描述符 FileDescriptor坪蚁,通過這個(gè)對(duì)象可以直接控制這個(gè)磁盤文件。由于我們需要讀取的是字符格式镜沽,所以需要 StreamDecoder 類將 byte 解碼為 char 格式敏晤,至于如何從磁盤驅(qū)動(dòng)器上讀取一段數(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 這個(gè)概念沒有對(duì)應(yīng)到一個(gè)具體的實(shí)體蔬墩,它是描述計(jì)算機(jī)之間完成相互通信一種抽象功能译打。打個(gè)比方,可以把 Socket 比作為兩個(gè)城市之間的交通工具
拇颅,有了它奏司,就可以在城市之間來回穿梭了。交通工具有多種樟插,每種交通工具也有相應(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ī)痹届,但是一臺(tái)主機(jī)上可能運(yùn)行著多個(gè)應(yīng)用程序呻待,如何才能與指定的應(yīng)用程序通信就要通過 TCP 或 UPD 的地址也就是端口號(hào)來指定。這樣就可以通過一個(gè) Socket 實(shí)例唯一代表一個(gè)主機(jī)上的一個(gè)應(yīng)用程序的通信鏈路了队腐。
6.建立通信鏈路
當(dāng)客戶端要與服務(wù)端通信蚕捉,客戶端首先要?jiǎng)?chuàng)建一個(gè) Socket 實(shí)例,操作系統(tǒng)將為這個(gè) Socket 實(shí)例分配一個(gè)沒有被使用的本地端口號(hào)香到,并創(chuàng)建一個(gè)包含本地和遠(yuǎn)程地址和端口號(hào)的套接字?jǐn)?shù)據(jù)結(jié)構(gòu)鱼冀,這個(gè)數(shù)據(jù)結(jié)構(gòu)將一直保存在系統(tǒng)中直到這個(gè)連接關(guān)閉。在創(chuàng)建 Socket 實(shí)例的構(gòu)造函數(shù)正確返回之前悠就,將要進(jìn)行 TCP 的三次握手協(xié)議千绪,TCP 握手協(xié)議完成后,Socket 實(shí)例對(duì)象將創(chuàng)建完成梗脾,否則將拋出 IOException 錯(cuò)誤荸型。
與之對(duì)應(yīng)的服務(wù)端將創(chuàng)建一個(gè) ServerSocket 實(shí)例,ServerSocket 創(chuàng)建比較簡單只要指定的端口號(hào)沒有被占用炸茧,一般實(shí)例創(chuàng)建都會(huì)成功瑞妇,同時(shí)操作系統(tǒng)也會(huì)為 ServerSocket 實(shí)例創(chuàng)建一個(gè)底層數(shù)據(jù)結(jié)構(gòu),這個(gè)數(shù)據(jù)結(jié)構(gòu)中包含指定監(jiān)聽的端口號(hào)和包含監(jiān)聽地址的通配符梭冠,通常情況下都是“*”即監(jiān)聽所有地址辕狰。之后當(dāng)調(diào)用 accept() 方法時(shí),將進(jìn)入阻塞狀態(tài)控漠,等待客戶端的請(qǐng)求蔓倍。當(dāng)一個(gè)新的請(qǐng)求到來時(shí),將為這個(gè)連接創(chuàng)建一個(gè)新的套接字?jǐn)?shù)據(jù)結(jié)構(gòu)盐捷,該套接字?jǐn)?shù)據(jù)的信息包含的地址和端口信息正是請(qǐng)求源地址和端口偶翅。這個(gè)新創(chuàng)建的數(shù)據(jù)結(jié)構(gòu)將會(huì)關(guān)聯(lián)到 ServerSocket 實(shí)例的一個(gè)未完成的連接數(shù)據(jù)結(jié)構(gòu)列表中,注意這時(shí)服務(wù)端與之對(duì)應(yīng)的 Socket 實(shí)例并沒有完成創(chuàng)建碉渡,而要等到與客戶端的三次握手完成后聚谁,這個(gè)服務(wù)端的 Socket 實(shí)例才會(huì)返回,并將這個(gè) Socket 實(shí)例對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)從未完成列表中移到已完成列表中滞诺。所以 ServerSocket 所關(guān)聯(lián)的列表中每個(gè)數(shù)據(jù)結(jié)構(gòu)形导,都代表與一個(gè)客戶端的建立的 TCP 連接履羞。
7.數(shù)據(jù)傳輸
傳輸數(shù)據(jù)是我們建立連接的主要目的,如何通過 Socket 傳輸數(shù)據(jù)埋虹,下面將詳細(xì)介紹县好。
當(dāng)連接已經(jīng)建立成功,服務(wù)端和客戶端都會(huì)擁有一個(gè) Socket 實(shí)例爆雹,每個(gè) Socket 實(shí)例都有一個(gè) InputStream 和 OutputStream,正是通過這兩個(gè)對(duì)象來交換數(shù)據(jù)。同時(shí)我們也知道網(wǎng)絡(luò) I/O 都是以字節(jié)流傳輸?shù)陌炙薄.?dāng) Socket 對(duì)象創(chuàng)建時(shí)芬膝,操作系統(tǒng)將會(huì)為 InputStream 和 OutputStream 分別分配一定大小的緩沖區(qū),數(shù)據(jù)的寫入和讀取都是通過這個(gè)緩存區(qū)完成的形娇。寫入端將數(shù)據(jù)寫到 OutputStream 對(duì)應(yīng)的 SendQ 隊(duì)列中锰霜,當(dāng)隊(duì)列填滿時(shí),數(shù)據(jù)將被發(fā)送到另一端 InputStream 的 RecvQ 隊(duì)列中桐早,如果這時(shí) RecvQ 已經(jīng)滿了癣缅,那么 OutputStream 的 write 方法將會(huì)阻塞直到 RecvQ 隊(duì)列有足夠的空間容納 SendQ 發(fā)送的數(shù)據(jù)。值得特別注意的是哄酝,這個(gè)緩存區(qū)的大小以及寫入端的速度和讀取端的速度非常影響這個(gè)連接的數(shù)據(jù)傳輸效率友存,由于可能會(huì)發(fā)生阻塞,所以網(wǎng)絡(luò) I/O 與磁盤 I/O 在數(shù)據(jù)的寫入和讀取還要有一個(gè)協(xié)調(diào)的過程陶衅,如果兩邊同時(shí)傳送數(shù)據(jù)時(shí)可能會(huì)產(chǎn)生死鎖屡立,在后面 NIO 部分將介紹避免這種情況。