UNIX系統的I/O模型
同步阻塞I/O、同步非阻塞I/O、I/O多路復用、信號驅動I/O和異步I/O,
什么是 I/O
就是計算機記憶體與外部設備之間拷貝資料的程序,
為什么需要 I/O
CPU訪問記憶體的速度遠遠高于外部設備,因此CPU是先把外部設備的資料讀到記憶體里,然后再進行處理,
當你的程式通過CPU向外部設備發出一個讀指令,資料從外部設備拷貝到記憶體需要一段時間,這時CPU沒事干,你的程式是:
- 主動把CPU讓給別人
- 還是讓CPU不停查:資料到了嗎?資料到了嗎?…
這就是I/O模型要解決的問題,
Java I/O模型
對于一個網路I/O通信程序,比如網路資料讀取,會涉及兩個物件:
- 呼叫這個I/O操作的用戶執行緒
- 作業系統內核
一個行程的地址空間分為用戶空間和內核空間,用戶執行緒不能直接訪問內核空間,
當用戶執行緒發起I/O操作后(Selector發出的select呼叫就是一個I/O操作),網路資料讀取操作會經歷兩個步驟:
- 用戶執行緒等待內核將資料從網卡拷貝到內核空間
- 內核將資料從內核空間拷貝到用戶空間
有人會好奇,內核資料從內核空間拷貝到用戶空間,這樣會不會有點浪費?
畢竟實際上只有一塊記憶體,能否直接把記憶體地址指向用戶空間可以讀取?
Linux中有個叫mmap的系統呼叫,可以將磁盤檔案映射到記憶體,省去了內核和用戶空間的拷貝,但不支持網路通信場景!
各種I/O模型的區別就是這兩個步驟的方式不一樣,
同步阻塞I/O
用戶執行緒發起read呼叫后就阻塞了,讓出CPU,內核等待網卡資料到來,把資料從網卡拷貝到內核空間,接著把資料拷貝到用戶空間,再把用戶執行緒叫醒,

同步非阻塞I/O
用戶行程主動發起read呼叫,這是個系統呼叫,CPU由用戶態切換到內核態,執行內核代碼,
內核發現該socket上的資料已到內核空間,將用戶執行緒掛起,然后把資料從內核空間拷貝到用戶空間,再喚醒用戶執行緒,read呼叫回傳,
用戶執行緒不斷發起read呼叫,資料沒到內核空間時,每次都回傳失敗,直到資料到了內核空間,這次read呼叫后,在等待資料從內核空間拷貝到用戶空間這段時間里,執行緒還是阻塞的,等資料到了用戶空間再把執行緒叫醒,

I/O多路復用
用戶執行緒的讀取操作分成兩步:
- 執行緒先發起select呼叫,問內核:資料準備好了嗎?
- 等內核把資料準備好了,用戶執行緒再發起read呼叫
在等待資料從內核空間拷貝到用戶空間這段時間里,執行緒還是阻塞的
為什么叫I/O多路復用?
因為一次select呼叫可以向內核查多個資料通道(Channel)的狀態,

NIO API可以不用Selector,就是同步非阻塞,使用了Selector就是IO多路復用,
異步I/O
用戶執行緒發起read呼叫的同時注冊一個回呼函式,read立即回傳,等內核將資料準備好后,再呼叫指定的回呼函式完成處理,在這個程序中,用戶執行緒一直沒有阻塞,

信號驅動I/O
可以把信號驅動I/O理解為“半異步”,非阻塞模式是應用不斷發起read呼叫查詢資料到了內核沒有,而信號驅動把這個程序異步了,應用發起read呼叫時注冊了一個信號處理函式,其實是個回呼函式,資料到了內核后,內核觸發這個回呼函式,應用在回呼函式里再發起一次read呼叫去讀內核的資料,
所以是半異步,
NioEndpoint組件
Tomcat的NioEndpoint實作了I/O多路復用模型,
作業流程
Java的多路復用器的使用:
- 創建一個Selector,在其上注冊感興趣的事件,然后呼叫select方法,等待感興趣的事情發生
- 感興趣的事情發生了,比如可讀了,就創建一個新的執行緒從Channel中讀資料
NioEndpoint包含LimitLatch、Acceptor、Poller、SocketProcessor和Executor共5個組件,

LimitLatch
連接控制器,控制最大連接數,NIO模式下默認是8192,

當連接數到達最大時阻塞執行緒,直到后續組件處理完一個連接后將連接數減1,
到達最大連接數后,os底層還是會接收客戶端連接,但用戶層已不再接收,
核心代碼:
public class LimitLatch {
private class Sync extends AbstractQueuedSynchronizer {
@Override
protected int tryAcquireShared() {
long newCount = count.incrementAndGet();
if (newCount > limit) {
count.decrementAndGet();
return -1;
} else {
return 1;
}
}
@Override
protected boolean tryReleaseShared(int arg) {
count.decrementAndGet();
return true;
}
}
private final Sync sync;
private final AtomicLong count;
private volatile long limit;
// 執行緒呼叫該方法,獲得接收新連接的許可,執行緒可能被阻塞
public void countUpOrAwait() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// 呼叫這個方法來釋放一個連接許可,則前面阻塞的執行緒可能被喚醒
public long countDown() {
sync.releaseShared(0);
long result = getCount();
return result;
}
}
用戶執行緒呼叫LimitLatch#countUpOrAwait拿到鎖,若無法獲取,則該執行緒會被阻塞在AQS佇列,
AQS又是怎么知道是阻塞還是不阻塞用戶執行緒的呢?
由AQS的使用者決定,即內部類Sync決定,因為Sync類重寫了AQS#tryAcquireShared():若當前連接數count < limit,執行緒能獲取鎖,回傳1,否則回傳-1,
如何用戶執行緒被阻塞到了AQS的佇列,由Sync內部類決定什么時候喚醒,Sync重寫AQS#tryReleaseShared(),當一個連接請求處理完了,又可以接收新連接,這樣前面阻塞的執行緒將會被喚醒,
LimitLatch用來限制應用接收連接的數量,Acceptor用來限制系統層面的連接數量,首先是LimitLatch限制,應用層處理不過來了,連接才會堆積在作業系統的Queue,而Queue的大小由acceptCount控制,
Acceptor
Acceptor實作了Runnable介面,因此可以跑在單獨執行緒里,在這個死回圈里呼叫accept接收新連接,一旦有新連接請求到達,accept方法回傳一個Channel物件,接著把Channel物件交給Poller去處理,
一個埠號只能對應一個ServerSocketChannel,因此這個ServerSocketChannel是在多個Acceptor執行緒之間共享的,它是Endpoint的屬性,由Endpoint完成初始化和埠系結,
可以同時有過個Acceptor呼叫accept方法,accept是執行緒安全的,
初始化
protected void initServerSocket() throws Exception {
if (!getUseInheritedChannel()) {
serverSock = ServerSocketChannel.open();
socketProperties.setProperties(serverSock.socket());
InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
serverSock.socket().bind(addr,getAcceptCount());
} else {
// Retrieve the channel provided by the OS
Channel ic = System.inheritedChannel();
if (ic instanceof ServerSocketChannel) {
serverSock = (ServerSocketChannel) ic;
}
if (serverSock == null) {
throw new IllegalArgumentException(sm.getString("endpoint.init.bind.inherited"));
}
}
// 阻塞模式
serverSock.configureBlocking(true); //mimic APR behavior
}
- bind方法的 getAcceptCount() 引數表示os的等待佇列長度,當應用層的連接數到達最大值時,os可以繼續接收連接,os能繼續接收的最大連接數就是這個佇列長度,可以通過acceptCount引數配置,默認是100

ServerSocketChannel通過accept()接受新的連接,accept()方法回傳獲得SocketChannel物件,然后將SocketChannel物件封裝在一個PollerEvent物件中,并將PollerEvent物件壓入Poller的Queue里,
這是個典型的“生產者-消費者”模式,Acceptor與Poller執行緒之間通過Queue通信,
Poller
本質是一個Selector,也跑在單獨執行緒里,
Poller在內部維護一個Channel陣列,它在一個死回圈里不斷檢測Channel的資料就緒狀態,一旦有Channel可讀,就生成一個SocketProcessor任務物件扔給Executor去處理,
內核空間的接收連接是對每個連接都產生一個channel,該channel就是Acceptor里accept方法得到的scoketChannel,后面的Poller在用selector#select監聽內核是否準備就緒,才知道監聽內核哪個channel,
維護了一個 Queue:

SynchronizedQueue的方法比如offer、poll、size和clear都使用synchronized修飾,即同一時刻只有一個Acceptor執行緒讀寫Queue,
同時有多個Poller執行緒在運行,每個Poller執行緒都有自己的Queue,
每個Poller執行緒可能同時被多個Acceptor執行緒呼叫來注冊PollerEvent,
Poller的個數可以通過pollers引數配置,
職責
-
Poller不斷的通過內部的Selector物件向內核查詢Channel狀態,一旦可讀就生成任務類SocketProcessor交給Executor處理

-
Poller回圈遍歷檢查自己所管理的SocketChannel是否已超時,若超時就關閉該SocketChannel
SocketProcessor
Poller會創建SocketProcessor任務類交給執行緒池處理,而SocketProcessor實作了Runnable介面,用來定義Executor中執行緒所執行的任務,主要就是呼叫Http11Processor組件處理請求:Http11Processor讀取Channel的資料來生成ServletRequest物件,
Http11Processor并非直接讀取Channel,因為Tomcat支持同步非阻塞I/O、異步I/O模型,在Java API中,對應Channel類不同,比如有AsynchronousSocketChannel和SocketChannel,為了對Http11Processor屏蔽這些差異,Tomcat設計了一個包裝類叫作SocketWrapper,Http11Processor只呼叫SocketWrapper的方法去讀寫資料,
Executor
執行緒池,負責運行SocketProcessor任務類,SocketProcessor的run方法會呼叫Http11Processor來讀取和決議請求資料,我們知道,Http11Processor是應用層協議的封裝,它會呼叫容器獲得回應,再把回應通過Channel寫出,
Tomcat定制的執行緒池,它負責創建真正干活的作業執行緒,就是執行SocketProcessor#run,即決議請求并通過容器來處理請求,最終呼叫Servlet,
Tomcat的高并發設計
高并發就是能快速地處理大量請求,需合理設計執行緒模型讓CPU忙起來,盡量不要讓執行緒阻塞,因為一阻塞,CPU就閑了,
有多少任務,就用相應規模執行緒數去處理,
比如NioEndpoint要完成三件事情:接收連接、檢測I/O事件和處理請求,關鍵就是把這三件事情分別定制執行緒數處理:
- 專門的執行緒組去跑Acceptor,并且Acceptor的個數可以配置
- 專門的執行緒組去跑Poller,Poller的個數也可以配置
- 具體任務的執行也由專門的執行緒池來處理,也可以配置執行緒池的大小
總結
I/O模型是為了解決記憶體和外部設備速度差異,
- 所謂阻塞或非阻塞是指應用程式在發起I/O操作時,是立即回傳還是等待
- 同步和異步,是指應用程式在與內核通信時,資料從內核空間到應用空間的拷貝,是由內核主動發起還是由應用程式來觸發,
Tomcat#Endpoint組件的主要作業就是處理I/O,而NioEndpoint利用Java NIO API實作了多路復用I/O模型,
讀寫資料的執行緒自己不會阻塞在I/O等待上,而是把這個作業交給Selector,
當客戶端發起一個HTTP請求時,首先由Acceptor#run中的
socket = endpoint.serverSocketAccept();
接收連接,然后傳遞給名稱為Poller的執行緒去偵測I/O事件,Poller執行緒會一直select,選出內核將資料從網卡拷貝到內核空間的 channel(也就是內核已經準備好資料)然后交給名稱為Catalina-exec的執行緒去處理,這個程序也包括內核將資料從內核空間拷貝到用戶空間這么一個程序,所以對于exec執行緒是阻塞的,此時用戶空間(也就是exec執行緒)就接收到了資料,可以決議然后做業務處理了,
參考
- https://blog.csdn.net/historyasamirror/article/details/5778378
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/290703.html
標籤:其他
下一篇:C語言實作井字棋
