從OS的層次理解網絡I/O模型

基本概念

傳統IO的種類linux

  • InputStream、OutputStream 基於字節流操做的 IO
  • Write、Reader基於字符流的IO
  • File基於磁盤操做的IO
  • Socket基於網絡操做的IO

內核空間與用戶空間
設計模式

  • 內核負責網絡與文件數據的讀寫
  • 用戶程序經過系統調用得到網絡和文件的數據

內核態與用戶態的切換安全

//當前線程處於用戶態
String str = "string";
int x = 2;
//切換至內核態
FileOutputStream fop = new FileOutputStream(new File("a.txt"));
OutputStreamWrite out = new OutputStreamWrite(fop, "GBK");
out.write("....");
out.append('\r\n');
out.close();
//用戶態
int y = x + 2;

                     image

  • 程序爲讀寫數據不得不發生系統調用。
  • 經過系統調用接口,線程從用戶態切換到內核態,內核讀寫數據後,再切換回來。
  • 進程或線程的不一樣空間狀態。

socket通訊
                    服務器

  1. 服務器調用socket()、bind()、listen()完成初始化後,調用accept()阻塞等待,處於監聽端口的狀態,客戶端調用socket()初始化後,調用connect()發出SYN段並阻塞等待服務器應答,服務器應答一個SYN-ACK段,客戶端收到後從connect()返回,同時應答一個ACK段,服務器收到後從accept()返回。
  2. 數據傳輸的過程:
    創建鏈接後,TCP協議提供全雙工的通訊服務,可是通常的客戶端/服務器程序的流程是由客戶端主動發起請求,服務器被動處理請求,一問一答的方式。所以,服務器從accept()返回後馬上調用read(),讀socket就像讀管道同樣,若是沒有數據到達就阻塞等待,這時客戶端調用write()發送請求給服務器,服務器收到後從read()返回,對客戶端的請求進行處理,在此期間客戶端調用read()阻塞等待服務器的應答,服務器調用write()將處理結果發回給客戶端,再次調用read()阻塞等待下一條請求,客戶端收到後從read()返回,發送下一條請求,如此循環下去。
  3. 若是客戶端沒有更多的請求了,就調用close()關閉鏈接,就像寫端關閉的管道同樣,服務器的read()返回0,這樣服務器就知道客戶端關閉了鏈接,也調用close()關閉鏈接。注意,任何一方調用close()後,鏈接的兩個傳輸方向都關閉,不能再發送數據了。若是一方調用shutdown()則鏈接處於半關閉狀態,仍可接收對方發來的數據。
  • 客戶端
public class EchoClient {
	public static int DEFAULT_PORT = 9999;
	public static void main(String[] args) throws IOException {
		int port;
        try {
			port = Integer.parseInt(args[0]);
		} catch(RuntimeException e) {
        	port = DEFAULT_PORT;
		}
		Socket socket = new Socket("127.0.0.1", port);
		//鍵盤輸入
		BufferedReader buff = new BufferedReader(new InputStreamReader(System.in));
		//Socket輸出流,自動刷新
		PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
        //Socket輸入流,讀取服務端的數據並返回的大寫數據
		BufferedReader buffin = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String line = null;
        while((line = buff.readLine()) != null) {
        	if("stop".equals(line)) {
        		break;
			}
        	out.println(line);
			// 讀取服務端返回的一行大寫數據
			System.out.println(buffin.readLine());
		}

	}
}

也可以使用linux下的nc命令代替客戶端網絡

  • 服務端
public class EchoServer {
	public static int DEFAULT_PORT = 9999;

	public static void main(String[] args){
		int port;
		try {
			port = Integer.parseInt(args[0]);
		} catch(RuntimeException e){
			port = DEFAULT_PORT;
		}

		try {
			ServerSocket serverSocket = new ServerSocket(port);
			Socket clientSocket = serverSocket.accept();
			String ip = clientSocket.getInetAddress().getHostAddress();
			System.out.println("port : " + port + '\t' + "ipaddress : " + ip);
			//server 輸出流對應client輸入流,反之亦然
			PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
			BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
			String inputline ;
			while((inputline = in.readLine()) != null){
				System.out.println(inputline);
				out.println(inputline.toUpperCase());
			}
		} catch (IOException e) {
			System.out.println("Exception caught when trying to listen on port" + port + "or listening for a connection");
			e.printStackTrace();
		}
	}
}

同步與異步多線程

描述的是用戶線程與內核的交互方式或者說關注的是消息通訊機制:app

  • 同步是指用戶線程發起 I/O 請求後須要等待(堵塞)或者輪詢(非堵塞)內核 I/O 操做完成後才能繼續執行;
  • 異步是指用戶線程發起 I/O 請求後仍繼續執行,當內核 I/O 操做完成後會通知用戶線程,或者調用用戶線程註冊的回調函數。

堵塞與非堵塞異步

關注的是用戶線程調用內核 I/O 操做時,用戶線程等待I/O操做完成前是否能作其餘的事情:socket

  • 阻塞是指 I/O 操做須要完全完成後才返回到用戶空間;
  • 非阻塞是指 I/O 操做被調用後當即返回給用戶一個狀態值,無需等到 I/O 操做完全完成。

阻塞與非阻塞主要是程序(線程)等待消息通知時的狀態角度,同步與異步主要是從消息機制角度來講,這兩組概念組合爲四種狀況,下面舉幾個網上的例子:函數

  • 同步堵塞 李華點火燒水,中間啥事也沒幹,並一直等到水開(阻塞),水開了山治關火(同步)
  • 同步非堵塞(輪詢方式) 李華點火燒水,中間去看了電視,時不時看看水開了嘛(非阻塞),水開了山治關火(同步)
  • 異步堵塞 李華使用電水壺燒水,並一直等待電水壺燒水(阻塞),中間啥也沒幹,水開了自動斷電(異步)。
  • 異步非堵塞 李華使用電水壺燒水,而後去看電視了(非阻塞),沒有再管燒水壺,水開了自動斷電(異步)。

IO模型演進

IO操做發生時會經歷兩個階段:

  1. 用戶進程等待系統內核數據準備
  2. 將數據從內核拷貝到用戶進程中

下面簡單介紹常見的五種 I/O 模型:

  1. 阻塞 I/O
  2. 非阻塞 I/O
  3. I/O 複用(select 和 poll)
  4. 信號驅動I/O(SIGIO)
  5. 異步 I/O

本節中將recvfrom函數視爲系統調用。通常recvfrom函數的實現都有一個從應用程序進程中運行到內核中運行的切換,一段時間後再跟一個返回應用進程的切換。

阻塞 I/O

請求沒法當即完成則保持阻塞。

                 

  • 等待數據就緒。網絡I/O的狀況就是等待遠端數據陸續抵達;磁盤I/O的狀況就是等待磁盤數據從磁盤上讀取到內核態內存中。
  • 數據複製。出於系統安全考慮,用戶態的程序沒有權限直接讀取內核態內存,所以內核負責把內核態內存中的數據複製一份到用戶態內存中。

進程阻塞的整段時間是指從調用recvfrom函數開始到它返回的這段時間,當進程返回成功提示時,應用進程開始處理數據報。

非阻塞 I/O
               

  • socket設置爲NONBLOCK(非阻塞)就是告訴內核,當所請求的I/O操做沒法完成時,不要讓進程進入睡眠狀態,而是馬上返回一個錯誤碼(EWOULDBLOCK),這樣請求就不會阻塞;
  • I/O操做函數將不斷地測試數據是否已經準備好,若是沒有準備好,則繼續測試,直到數據準備好爲止。在整個I/O請求的過程當中,雖然用戶線程每次發起I/O請求後能夠當即返回,可是爲了等到數據,仍需輪詢、重複請求,而這是對CPU時間的極大浪費。
  • 數據準備好了,從內核複製到用戶空間。

I/O 複用(異步堵塞I/O)

I/O 多路複用會用到 select 或者 poll 函數,這兩個函數也會使進程阻塞,可是和阻塞 I/O 所不一樣的的,這兩個函數能夠同時阻塞多個 I/O 操做。並且能夠同時對多個讀操做,多個寫操做的 I/O 函數進行檢測,直到有數據可讀或可寫時,才真正調用 I/O 操做函數。

                  

從流程上看,使用select函數進行I/O請求和同步阻塞模型沒有太大的區別,甚至還多了添加監視socket,以及調用select函數的額外操做,效率更差。可是,使用select函數的優點是用戶能夠在一個線程內同時處理多個socket的I/O請求。用戶能夠註冊多個socket,而後不斷地調用select來讀取被激活的socket,達到在同一個線程內同時處理多個I/O請求的目的。而在同步阻塞模型中,必須經過多線程的方式才能達到這個目的。

I/O複用模型使用Reactor設計模式實現了這一機制。

調用select或poll函數的方法由一個用戶態線程負責輪詢多個socket,直到階段1的數據就緒,再通知實際的用戶線程執行階段2的複製操做。經過一個專職的用戶態線程執行非阻塞I/O輪詢,模擬實現階段1的異步化。

信號驅動I/O
                  

首先,咱們容許socket進行信號驅動I/O,並經過調用sigaction來安裝一個信號處理函數,進程繼續運行並不阻塞。當數據準備好後,進程會收到一個SIGIO信號,能夠在信號處理函數中調用recvfrom來讀取數據報,並通知主循環數據已準備好被處理,也能夠通知主循環,讓它來讀取數據報。

異步 I/O

調用 aio_read 函數,告訴內核描述字,緩衝區指針,緩衝區大小,文件偏移以及通知的方式,而後當即返回。當內核將數據拷貝到緩衝區後,再通知應用程序。
異步I/O模型使用Proactor設計模式實現了這一機制。
                  

異步I/O模型告知內核:當整個過程(包括階段1和階段2)所有完成時,通知應用程序來讀數據。

幾種 I/O 模型的比較

前四種模型的區別是階段1不相同,階段2基本相同,都是將數據從內核拷貝到調用者的緩衝區。而異步 I/O 的兩個階段都不一樣於前四個模型。

同步 I/O 操做引發請求進程阻塞,直到 I/O 操做完成。異步 I/O 操做不引發請求進程阻塞。

                  

關於這些模型的具體實現我打算放到Java I/O中進行討論。

相關文章
相關標籤/搜索