什么是IO
io是數據的接收和發送操作,linux進程無法直接操作io設備,需要通過系統調用請求內核來完成io操作,內核為每個設備維護一個緩沖區。用戶進程發送操作的一個完整io包括兩部分:用戶空間將數據發送到內核,內核將數據發送到io設備。用戶進程接收操作的一個完整io也是包括兩部分:內核從io設備中接收數據到緩沖區,從內核緩沖區復制數據到進程空間
5種io模型
阻塞io:進程發起io操作后,進程被阻塞,轉到內核空間處理,整個io處理完后返回進程。特點:需要為每一個io請求分配一個進程或線程來處理
非阻塞io:進程發起IO系統調用后,如果內核緩沖區沒有數據,需要到IO設備中讀取,進程返回一個錯誤而不會被阻塞;進程發起IO系統調用后,如果內核緩沖區有數據,內核就會把數據返回進程。需要進程主動去輪詢。
io多路復用:?linuxIO多路復用技術提供一個單進程、單線程監聽多個IO讀寫時間的機制。其基本原理是各個IO將句柄設置為非阻塞IO,然后將各個IO句柄注冊到linux提供的IO復用函數上(select,poll或者epoll),如果某個句柄的IO數據就緒,則函數返回通知io ready。調用者進行后續的read write操作。多路復用函數幫我們進行了多個非阻塞IO數據是否就緒的輪詢操作,只不過IO多路復用函數的輪詢更有效率,因為函數一次性傳遞文件描述符到內核態,在內核態中進行輪詢(epoll則是進行等待邊緣事件的觸發),不必反復進行用戶態和內核態的切換。io多路復用的特點:內核輪詢多個io在內核緩沖區是否ready,適合高并發網絡服務應用。高并發網絡服務應用如果采用阻塞io怎么實現?需要為每個socket連接啟動一個線程,頻繁的線程切換會導致性能低下。如果采用非阻塞io怎么實現?需要進程輪詢每個io,如果有一萬個socket連接,確定哪個連接ready需要進行1萬次從用戶態到內核態的切換,性能低下。
信號驅動io:進程發起io操作,會向內核注冊一個信號處理函數,然后進程返回不阻塞,當內核數據就緒時發送一個信號給進程,進程在信號處理函數中調用io函數處理。特點:回調機制,開發難度大
異步io:進程發起一個io操作后,進程返回。內核把整個io處理完后(包括將數據從內核緩沖區復制到用戶態)通知進程,進程只需要在指定的數組中引用數據即可
同步io和異步io
同步IO:用戶進程發出IO調用,去獲取IO設備數據,雙方的數據要經過內核緩沖區同步,完全準備好后,再復制返回到用戶進程。而復制返回到用戶進程會導致請求進程阻塞,直到I/O操作完成。
異步IO:用戶進程發出IO調用,去獲取IO設備數據,并不需要同步,內核直接復制到進程,整個過程不導致請求進程阻塞。
阻塞io、非阻塞io、io多路復用、信號驅動io都是同步io
同步IO最終需要應用程序調用系統調用從內核來讀取數據、異步IO由系統來負責將數據從內核讀取到應用程序,應用程序直接使用。
select
1 可以一次性從用戶態向內核態傳遞多個fd
2 內核把當前進程掛到相應io設備的等待隊列中
3 內核逐個io判斷是否就緒,如果有就緒的就返回(select返回),如果全部未就緒,調用schedule_timeout將進程睡眠
4 當設備驅動發生自身資源可讀寫后,喚醒其隊列上的睡眠進程,進程返回(select返回)
5 如果超過schedule_timeout還沒人喚醒,進程喚醒重新遍歷fd重復上邊流程
優點:
1 一次性傳遞多個fd
2 在內核中遍歷,不必反復進行用戶態和內核態的切換
3 具有喚醒機制
缺點:
總結一句話就是:一堆fd從用戶態拷貝到內核態,內核態遍歷一堆fd,內核態返回后用戶態需要遍歷一堆fd,這一堆的個數還限制的比較小
1 每次調用select,都需要把fd集合從用戶態拷貝到內核態返回時還要從內核態拷貝到用戶態,這個開銷在fd很多時會很大
2?每次調用select都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
3?select返回后,用戶不得不自己再遍歷一遍fd集合,以找到哪些fd的IO操作可用
4?再次調用select時,fd數組需要重新被初始化
5?select支持的文件描述符數量太小了,內核中采用位圖存儲,默認是1024
poll
1 通過一個pollfd數組向內核傳遞需要關注的事件,故沒有描述符個數的限制,其他的沒有區別
epoll
epoll三大關鍵要素:mmap、紅黑樹、鏈表
mmap:epoll通過mmap將用戶空間的一塊地址和內核空間的一塊地址映射到相同的一塊物理內存地址,減少用戶態和內核態之間的數據交換,內核可以直接看到監聽的句柄
紅黑樹:epoll采用紅黑樹來存儲監聽的套接字。epoll_ctl添加或者刪除一個套接字時,都在紅黑樹上處理,紅黑樹的插入和刪除性能比較好
雙向鏈表:epoll的rdllist采用的就是雙向鏈表
epoll主要函數
1?int? epoll_create (int size );
采用epoll_create()建立epoll描述符來記錄需要監控的fd(select每次都把fd集合從用戶空間賦值到內核空間)
在epoll早期的實現中,對于監控文件描述符的組織并不是使用紅黑樹,而是hash表。這里的size實際上已經沒有意義。
2?int? epoll_ctl (int epfd,int op,int fd,struct epoll_event *event);
epoll_ctrl用來添加或刪除待監控的fd。當調用epoll_ctrl添加fd和事件時,該事件都會和相應的設備驅動程序建立回調關系,當相應的事件發生后,調用ep_poll_callback回調函數,這個函數把這個事件添加到rdllist雙向鏈表中。
op可以指定操作類型:EPOLL_CTL_ADD(往事件表中注冊fd上的事件)、EPOLL_CTL_MOD(修改fd上的注冊事件)、EPOLL_CTL_DEL(刪除fd上的注冊事件)
?event:指定fd關注的事件
3?int epoll_wait (int epfd,struct epoll_event* events,int maxevents,int timeout );
(1)?epoll_wait調用ep_poll,當rdllist為空(無就緒fd)時掛起當前進程,直到rdlist不空時進程才被喚醒。
(2)?文件fd狀態改變,導致相應fd上的回調函數ep_poll_callback()被調用。
(3)?ep_poll_callback將相應fd對應epitem加入rdlist,導致rdlist不空,進程被喚醒,epoll_wait得以繼續執行。
(4)?ep_events_transfer函數將rdlist中的epitem拷貝到txlist中,并將rdlist清空。
(5)?ep_send_events函數,它掃描txlist中的每個epitem,調用其關聯fd對用的poll方法。此時對poll的調用僅僅是取得fd上較新的events(防止之前events被更新),之后將取得的events和相應的fd發送到用戶空間(封裝在struct?epoll_event,從epoll_wait返回)。
ET模式和LT模式
epoll_wait返回的兩個時機:
時機一:fd狀態改變,ep_poll_callback被調用,加入rdllist。對于讀操作:一是buffer由不可讀狀態變為可讀的時候。二是有新數據到達,buffer中待讀的內容變多的時候。對于寫操作:一是buffer由不可寫變為可寫的時候。二是舊數據發送走,buffer中可寫的空間變大的時候。
時機二:fd的events中有相應的事件位置1時。對于讀操作:buffer中有數據可讀,buffer不為空的時候fd的events的可讀位就置1。對于寫操作:buffer中有空間可寫,buffer不滿的時候fd的可寫位就置1。
對于ET模式,只采用上述時機一。LT模式采用上述時機一和時機二。
ET模式注意事項
ET模式下,讀操作如果一次沒有讀盡buffer中的數據,將得不到讀就緒的通知,造成buffer中已有的數據無機會讀出,除非有新的數據再次到達。因此ET模式下需要注意:1,采用非阻塞io? 2,循環讀寫,保證讀完、寫滿。
ET模式和LT模式如何保證數據可以被讀完?ET模式由用戶程序來循環,LT模式通過多次epoll_wait返回來循環。
建議采用ET模式+非阻塞io+循環讀寫,相比LT模式,省去了多次epoll_wait的調用。
https://blog.csdn.net/daaikuaichuan/article/details/88777274
高并發服務器模型epoll+線程池
這種架構特點如下:
1 基于I/O多路復用的思想,通過單線程I/O多路復用,可以達到高效并發,同時避免了多線程I/O來回切換的各種開銷。
2 由于業務多跟數據庫打交道會造成阻塞,基于線程池的多工作者線程,可以充分發揮和利用多線程的優勢。
創建一個epoll實例;
while(server?running)
{
????epoll等待事件;
????if(新連接到達且是有效連接)
????{
????????accept此連接;
????????將此連接設置為non-blocking;
????????為此連接設置event(EPOLLIN?|?EPOLLET?...);
????????將此連接加入epoll監聽隊列;
????????從線程池取一個空閑工作者線程并處理此連接;
????}
????else?if(讀請求)
????{
????????從線程池取一個空閑工作者線程并處理讀請求;
????}
????else?if(寫請求)
????{
????????從線程池取一個空閑工作者線程并處理寫請求;
????}
????else
????????其他事件;?????
}
golang net庫網絡實現分析
golang中的網絡io全部是非阻塞io
網絡io的read操作如下:
1 golang協程通過系統調用來讀取數據
2 如果數據沒準備好,調用waitRead----->runtime_pollWait----->poll_runtime_pollWait------>netpollblock------>gopark------->park_m--------->schedule? 進行協程的切換。這樣就通過非阻塞io加協程切換模擬出了阻塞io,可以采用阻塞io的簡單開發方式
3 golang中起一個線程定期執行sysmon函數,sysmon---->netpoll----->epollwait? 返回ready的協程列表,sysmon----->injectglist------>casgstatus將ready的協程的狀態設置為可運行
4 讀io的協程繼續運行,再次通過系統調用來讀取數據,最終返回
底層本質是:非阻塞io+epoll+線程池+任務隊列的模型,epoll返回哪個任務的io 已經ready,線程池中的線程取io ready的任務繼續執行,goalng通過協程將這些全部封裝好了,通過協程實現了非阻塞的io可以采用阻塞的方式編程